88 Commits
Author SHA1 Message Date
19904408334 3315f6cf17 Merge branch 'dev未优化' of https://gitea.redpowerfuture.com/red-future/model-gateway into dev未优化 2026-08-14 16:34:15 +08:00
19904408334 c69aed717d fix: 处理 RequestCanceled 模型调用失败 2026-08-14 16:33:54 +08:00
admin 8ac032c12f Merge remote-tracking branch 'origin/dev未优化' into dev未优化 2026-08-06 09:41:11 +08:00
admin f152c23a7e gitea,gitea-runner写入docker-compose 2026-08-06 09:40:32 +08:00
19904408334 84102121c5 refactor: 将 ModelType 类型从 int8 更改为 int 2026-07-23 17:44:38 +08:00
19904408334 8bdae52d4d feat: 新增模型管理模块功能 2026-07-23 17:22:06 +08:00
19904408334 6166c24412 feat: 添加模型并发控制与分布式锁
重新启用任务并发限制逻辑,使用 Redis 计数器控制模型并发数;
引入分布式锁防止重复创建,并优化优雅退出顺序确保请求完成;
移除 video_duration 计费计算,支持外部传入 taskId,为轮询查询添加独立超时上下文。
2026-07-09 13:47:08 +08:00
19904408334 a7f30d1640 fix: 支持解析单个JSON对象为单元素数组 2026-07-08 13:37:09 +08:00
19904408334 b8006329d1 feat: 新增会话与结果管理功能及扩展字段
重构执行列表树状结构,扁平化日期节点下的输出项,新增会话列表查询、结果删除与软删除标记,引入扩展字段与模板配置,升级依赖版本。
2026-07-08 09:22:53 +08:00
WangLiZhao d46f163cfd fix(task): 改进任务失败时的错误信息记录 2026-07-06 17:34:59 +08:00
WangLiZhao dcbdc462bd feat(billing): 添加TTS模型input_chars字符数自动计算功能 2026-07-06 17:00:20 +08:00
WangLiZhao 947a158a52 Merge remote-tracking branch 'origin/dev未优化' into dev未优化 2026-07-06 16:38:16 +08:00
WangLiZhao 4033a2ba69 feat(billing): 添加TTS模型计费功能 2026-07-06 16:31:07 +08:00
19904408334 b9dff27e33 chore: 移除计费相关调试打印日志 2026-07-04 20:05:52 +08:00
19904408334 fe64d178ad chore: 添加计费处理调试日志 2026-07-04 18:55:47 +08:00
19904408334 dc6375707f chore: 添加计费处理调试日志 2026-07-04 18:28:51 +08:00
19904408334 518f666ac6 fix: 提高租户余额不足的检查阈值 2026-07-02 11:20:11 +08:00
19904408334 260d6547f2 fix: 调整租户余额不足判断阈值为100 2026-07-02 08:58:37 +08:00
19904408334 313bb692c7 fix: 修复租户余额响应结构及租户ID引用错误
更新 TenantSurplusResp 结构以匹配实际的嵌套 JSON 响应格式,
并修正 worker 中 billing 逻辑错误引用的租户ID变量。
2026-07-01 19:32:52 +08:00
WangLiZhao 4d95ff75d2 fix(task): 修复任务执行中租户余额检查的参数传递错误
- 修正了 GetTenantSurplus 函数调用中的参数,从 task.TenantId 改为 model.TenantId
- 注释掉了模型调用中重复的余额检查逻辑以避免冗余验证
2026-07-01 18:55:16 +08:00
WangLiZhao a332aa438b refactor(task): 优化任务处理中的余额检查逻辑 2026-07-01 18:47:13 +08:00
WangLiZhao d18016e698 feat(gateway): 添加租户余额管理功能 2026-07-01 18:44:31 +08:00
WangLiZhao 7e849b1c0f fix(task): 修复任务处理中的参数传递和数据解析问题 2026-07-01 18:28:00 +08:00
WangLiZhao 84421b4ee5 refactor(gateway): 调整回调载荷结构定义 2026-07-01 18:04:39 +08:00
WangLiZhao 62003c25a0 fix(billing): 修复计费数据处理逻辑并更新配置
- 在请求阶段将请求数据作为计费记录的基础字段存入数组
- 修改计费处理逻辑,使用请求阶段数据作为基础补充返回数据后进行费用计算
- 重试时使用相同的计费数据处理逻辑确保数据一致性
- 将计费结果直接替换数组第一个元素而非追加
- 更新数据库配置从 192.168.0.83 到 192.168.3.30
- 更新 Redis、Consul、Jaeger 和文件上传服务地址到新 IP
- 修改数据库端口从 15432 到 5432 并更新密码
2026-07-01 18:03:09 +08:00
WangLiZhao 2edbe54fdc fix(task): 修复任务失败时文本结果未保存的问题 2026-07-01 17:51:04 +08:00
WangLiZhao 35e955a617 feat(billing): 实现多条计费数据支持并优化模型计费逻辑 2026-07-01 17:41:01 +08:00
WangLiZhao bdd5f93e2c Merge remote-tracking branch 'origin/dev未优化' into dev未优化 2026-07-01 15:54:11 +08:00
WangLiZhao 326a8acac9 feat(gateway): 添加计费日期字段到回调请求体
- 在CallbackPayload结构体中添加BillingDate字段
- 将任务的BillingData赋值给回调请求体的BillingDate
- 保持现有回调功能的其他字段不变
2026-07-01 15:54:01 +08:00
19904408334 3ca8f93778 fix: 修复视频时长接口响应结构及URL拼写错误
移除VideoDurationResp中多余的嵌套Data字段,直接访问TotalDuration等属性;
修正API路径中缺失的字母a(medi -> media)。
2026-07-01 15:53:28 +08:00
WangLiZhao 8ef6a180f7 refactor(billing): 优化条件判断逻辑并移除未使用的常量 2026-07-01 15:39:25 +08:00
WangLiZhao 29d3f7ffb2 Merge remote-tracking branch 'origin/dev未优化' into dev未优化 2026-07-01 15:17:26 +08:00
WangLiZhao 2bc9d76e18 feat(billing): 添加计费系统功能 2026-07-01 15:17:15 +08:00
admin b00e5d34f4 common版本更新 2026-06-25 17:05:13 +08:00
admin 85dd4e8e84 common版本更新 2026-06-25 14:49:02 +08:00
19904408334 3feb912d86 fix: 增加模型调用重试等待时间 2026-06-25 11:57:57 +08:00
19904408334 63092be679 fix: 忽略 "download failed" 错误避免误判任务失败 2026-06-25 11:48:27 +08:00
19904408334 e11155b604 fix: 忽略连接错误以避免任务误判失败 2026-06-25 11:07:07 +08:00
19904408334 c22a38da9a fix: 忽略下载错误以避免任务误失败 2026-06-24 16:17:25 +08:00
admin aee243d4eb common版本升级 2026-06-23 18:31:59 +08:00
19904408334 16f2967569 build: 升级 common 依赖至 v0.0.25 2026-06-23 17:00:05 +08:00
WangLiZhao 73f630636d feat(task): 添加构建模型名称字段支持 2026-06-23 16:18:00 +08:00
WangLiZhao eaa2942957 Merge remote-tracking branch 'origin/dev未优化' into dev未优化 2026-06-23 14:56:03 +08:00
WangLiZhao 525b391f09 feat(task): 添加构建模型名称字段支持动态校验
- 在ModelGatewayTask实体中新增BuildModelName字段
- 修改ParseAndValidate函数参数,支持传入requiredFields
- 在异步工作器中实现构建模型的动态字段校验逻辑
- 添加prompt构建服务中传递构建模型名称的功能
- 实现构建模型不存在时的兜底机制
2026-06-23 14:55:50 +08:00
admin 610481effd 1 2026-06-22 10:57:46 +08:00
19904408334 39b61f1867 chore: 更新配置并优化任务错误处理
将数据库、Redis、Consul及Jaeger配置切换至新环境地址,并在worker中忽略"Invalid video_url"与"Invalid audio track"错误,同时补充模型调用成功日志。
2026-06-22 09:08:12 +08:00
WangLiZhao 4cc44bf57c fix(task): 修正错误信息注入逻辑 2026-06-18 16:27:11 +08:00
WangLiZhao dc06d1bb9a refactor(config): 更新数据库配置和任务处理逻辑 2026-06-18 14:48:42 +08:00
WangLiZhao ecaaa5bdbc refactor(files): 优化文件处理和任务服务逻辑 2026-06-18 13:39:40 +08:00
19904408334 b21d7a8dbf fix: 修复请求头转发与任务状态流转问题
移除 util.ForwardHeaders,改为从原始请求精确提取 Authorization 或全部请求头;
任务创建时直接设为 Running 状态,避免二次更新与查询;
模型调用使用独立超时上下文,防止外层取消影响回调;
增加 OSS 上传耗时日志,调整数据库连接池参数。
2026-06-18 10:08:36 +08:00
WangLiZhao fddaf36f48 refactor(model): 优化模型网关的数据解析和任务处理逻辑 2026-06-17 14:34:48 +08:00
WangLiZhao b3b111995e refactor(task): 重构任务服务和数据结构 2026-06-12 15:29:05 +08:00
WangLiZhao 1c6c9bae14 refactor(service): 重构模型网关服务结构 2026-06-11 17:58:49 +08:00
WangLiZhao afd60caf56 fix(task): 修复任务状态更新和超时处理问题 2026-06-11 11:27:14 +08:00
WangLiZhao 196d2069ac Merge remote-tracking branch 'origin/dev' into dev 2026-06-10 16:47:32 +08:00
WangLiZhao 7596cbde09 feat(task): 添加任务更新功能 2026-06-10 16:47:21 +08:00
admin 7ec18926e3 ci/cd调整 2026-06-10 16:24:29 +08:00
admin a6b32bfeb3 ci/cd调整 2026-06-10 16:16:05 +08:00
WangLiZhao 2dc88ae587 refactor(prompts-core): 重构代码结构和优化工具函数 2026-06-10 14:51:24 +08:00
WangLiZhao e906248b0a feat(session): 重构会话管理和Redis缓存机制 2026-06-09 14:00:00 +08:00
WangLiZhao e5781aca06 Merge remote-tracking branch 'origin/dev' into dev
# Conflicts:
#	go.sum
2026-06-08 18:02:26 +08:00
WangLiZhao 0cf8948cd2 feat: 重构异步模型字段并更新依赖 2026-06-08 18:01:53 +08:00
admin 96e8bdfe62 ci/cd调整 2026-06-08 15:37:11 +08:00
admin 26de41d04e ci/cd调整 2026-06-08 13:44:54 +08:00
admin 0bee3685fb ci/cd调整 2026-06-08 13:39:20 +08:00
WangLiZhao 9049e0d2e8 refactor(prompt): 重构提示词构建服务和回调处理 2026-06-05 11:00:04 +08:00
WangLiZhao aae46a4f29 refactor(model-gateway): 重构代码结构并优化数据库查询 2026-06-03 18:37:17 +08:00
WangLiZhao bcfcc7ed47 refactor(util): 重构映射工具函数并优化异步任务轮询逻辑 2026-06-03 13:30:39 +08:00
WangLiZhao 2c7838807b chore(deps): 初始化项目依赖配置 2026-06-02 20:28:06 +08:00
WangLiZhao 52124385a1 refactor(asynch): 重构异步模型配置和队列管理 2026-06-02 20:26:45 +08:00
WangLiZhao c7e9eb889b feat(model): 添加流式配置支持并优化响应处理 2026-05-30 22:08:46 +08:00
19904408334 558fd49ec1 fix: 修复模型查询条件为空时的异常行为 2026-05-29 18:06:50 +08:00
WangLiZhao d409b84b58 refactor(service): 重构服务模块结构并优化模型配置 2026-05-29 17:54:19 +08:00
WangLiZhao e487b4bb5e refactor(task): 重构异步任务处理流程 2026-05-27 09:36:25 +08:00
WangLiZhao a28fcbaee9 feat: 新增模型扩展映射与查询配置字段 2026-05-23 18:08:08 +08:00
WangLiZhao 5416e7a983 Merge remote-tracking branch 'origin/dev' into dev 2026-05-22 13:03:29 +08:00
WangLiZhao 0e2ac286e9 feat(model): 添加运营商列表功能 2026-05-22 13:03:10 +08:00
19904408334 a88dc84d99 fix: 获取聊天模型时过滤当前用户 2026-05-22 13:02:13 +08:00
19904408334 4d2d4fd93d Merge branch 'dev' of http://116.204.74.41:3000/red-future/model-gateway into dev 2026-05-22 11:22:50 +08:00
19904408334 7129bd2de7 fix: 会话模型查询增加用户隔离 2026-05-22 11:22:35 +08:00
WangLiZhao 09474eb997 feat(consts): 添加视频模型类型常量定义 2026-05-22 11:17:31 +08:00
WangLiZhao 4946220185 feat(prompt): 重构提示词服务并添加模型类型子分类 2026-05-22 09:49:46 +08:00
WangLiZhao b6cdb8ff1d refactor(prompt): 优化任务等待机制并改进数据结构 2026-05-21 14:23:34 +08:00
WangLiZhao 4626d819b5 fix(gateway): 修复文件上传功能中的数据写入和头部设置问题 2026-05-21 11:18:39 +08:00
WangLiZhao 170568e03e refactor(model): 重构模型实体和数据访问层 2026-05-21 10:41:37 +08:00
WangLiZhao a080a5536d Merge remote-tracking branch 'origin/dev' into dev 2026-05-18 19:20:18 +08:00
WangLiZhao 142fea1e91 refactor(service): 重构服务代码结构并更新配置 2026-05-18 19:19:16 +08:00
19904408334 a585233c4d fix: 查询私有模型时增加enabled过滤条件 2026-05-15 16:04:18 +08:00
79 changed files with 5389 additions and 3770 deletions
+1
View File
@@ -0,0 +1 @@
.git
+7 -26
View File
@@ -1,43 +1,24 @@
# 阶段构建 - 第一阶段:编译(使用已安装的镜像)
FROM golang:1.26-alpine3.23 AS builder
# 阶段1: 构建
FROM golang:alpine AS builder
RUN apk add --no-cache git ca-certificates tzdata
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \
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
ENV GOPRIVATE=gitea.com/red-future/common
# 配置git使用私有Gitea仓库(带Token认证)
RUN git config --global url."http://x-token-auth:619679cd366aefea3a50f0622d842a41f2209e08595767bba49c3836ef57d415@116.204.74.41:3000/red-future/common.git".insteadOf "https://gitea.com/red-future/common.git" && \
git config --global credential.helper store
WORKDIR /build
# 复制父目录的 common 模块(因为 go.mod 中使用了本地 replace)
#COPY ../common /build/common
COPY . .
RUN go mod download && go mod tidy
RUN go build -ldflags="-s -w" -o main ./main.go
# 第二阶段:运行
FROM alpine:3.23
ENV TIME_ZONE=Asia/Shanghai
RUN apk add --no-cache ca-certificates tzdata && \
ln -sf /usr/share/zoneinfo/$TIME_ZONE /etc/localtime
WORKDIR /app
# 复制编译好的二进制文件
COPY --from=builder /build/main .
COPY --from=builder /build/config.yml ./
# 创建日志目录
RUN mkdir -p /logs /app/resource/log/run /app/resource/log/server
EXPOSE 3004
+332
View File
@@ -0,0 +1,332 @@
package util
import (
"context"
"fmt"
"math"
"strings"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/util/gconv"
)
// ======================== 计费入口 ========================
func CalculateBilling(config map[string]any, billingData map[string]any) map[string]any {
if len(config) == 0 {
return nil
}
switch config["type"] {
case "inference_tier": //推理模型计费
return calculateInferenceTierBilling(config, billingData)
case "video_resolution": //视频模型计费
return calculateVideoResolutionBilling(config, billingData)
case "tts":
return calculateTTSBilling(config, billingData)
}
return nil
}
// ======================== 推理模型计费 ========================
func calculateInferenceTierBilling(config map[string]any, data map[string]any) map[string]any {
promptTokens := gconv.Int64(data["prompt_tokens"])
completionTokens := gconv.Int64(data["completion_tokens"])
hasAudio := gconv.Bool(data["has_audio"])
inputK := promptTokens / 1000
tiers := config["pricing"].(map[string]any)["tiers"].([]any)
var matched map[string]any
for _, t := range tiers {
tier := t.(map[string]any)
if inputK >= gconv.Int64(tier["input_min"]) && inputK <= gconv.Int64(tier["input_max"]) {
matched = tier
break
}
}
if matched == nil {
return nil
}
var inputPrice float64
if hasAudio && matched["audio_input_price"] != nil {
inputPrice = gconv.Float64(matched["audio_input_price"])
} else {
inputPrice = gconv.Float64(matched["input_price"])
}
outputPrice := gconv.Float64(matched["output_price"])
inputCost := float64(promptTokens) * inputPrice / 1000000
outputCost := float64(completionTokens) * outputPrice / 1000000
// 推理模型
return map[string]any{
"model_name": data["model_name"],
"total_tokens": promptTokens + completionTokens,
"total_fee": inputCost + outputCost,
// 明细
"prompt_tokens": promptTokens,
"completion_tokens": completionTokens,
"has_audio": hasAudio,
"input_tier": fmt.Sprintf("[%v, %v]", matched["input_min"], matched["input_max"]),
"input_unit_price": inputPrice,
"output_unit_price": outputPrice,
"input_cost": inputCost,
"output_cost": outputCost,
}
}
// ======================== 视频模型计费 ========================
func calculateVideoResolutionBilling(config map[string]any, data map[string]any) map[string]any {
pricingPath := buildPricingPath(config, data)
pricing := config["pricing"].(map[string]any)
unitPrice := gconv.Float64(pricing[pricingPath])
var effectiveMinToken float64
if gconv.Bool(data["input_has_video"]) {
effectiveMinToken = matchMinToken(config, data)
}
completionTokens := gconv.Float64(data["actual_tokens"])
realChargeTokens := int64(math.Max(completionTokens, effectiveMinToken))
totalFee := float64(realChargeTokens) * unitPrice / 1000000
if gconv.Bool(config["enable_audio_charge"]) {
totalFee += gconv.Float64(data["audio_word_cnt"]) * gconv.Float64(config["audio_unit_price"])
}
// 视频模型
return map[string]any{
"model_name": data["model_name"],
"total_tokens": realChargeTokens,
"total_fee": totalFee,
// 明细
"prompt_tokens": 0,
"completion_tokens": int64(completionTokens),
"is_online": data["is_online"],
"video_resolution": data["video_resolution"],
"input_has_video": data["input_has_video"],
"output_duration_sec": data["output_duration_sec"],
"aspect_ratio": data["aspect_ratio"],
"resolution": data["resolution"],
"matched_path": pricingPath,
"token_unit_price": unitPrice,
"effective_min_token": effectiveMinToken,
}
}
func calculateTTSBilling(config map[string]any, data map[string]any) map[string]any {
usage := gconv.Float64(data["synthesize_text_length"])
unitPrice := gconv.Float64(config["pricing"])
totalFee := usage * unitPrice
return map[string]any{
"model_name": data["model_name"],
"total_tokens": int64(usage),
"total_fee": totalFee,
// 明细
"prompt_tokens": 0,
"completion_tokens": int64(usage),
"unit_price": unitPrice,
}
}
// ======================== 数据提取 ========================
func ExtractRequestBilling(ctx context.Context, config map[string]any, requestPayload map[string]any) map[string]any {
dimensions := config["dimensions"].(map[string]any)
fields := dimensions["request"].(map[string]any)
data := make(map[string]any)
for key, path := range fields {
val := extractValue(requestPayload, gconv.String(path))
// TTS 模型:input_chars 自动计算字符数
if key == "input_chars" {
if s, ok := val.(string); ok {
data[key] = len([]rune(s))
continue
}
}
data[key] = val
}
if defaults, ok := config["defaults"].(map[string]any); ok {
for k, v := range defaults {
if _, exists := data[k]; !exists || data[k] == nil {
data[k] = v
}
}
}
//if compute, ok := config["compute"].(map[string]any); ok {
// for targetField, rule := range compute {
// r := rule.(map[string]any)
// dependsOn := gconv.String(r["depends_on"])
// dependsValue := gconv.Bool(r["depends_value"])
//
// if gconv.Bool(data[dependsOn]) != dependsValue {
// continue
// }
//
// switch r["service"] {
// case "video_duration":
// urls := extractVideoUrls(requestPayload)
// if len(urls) > 0 {
// resp, err := gateway.GetVideoDuration(ctx, urls)
// if err == nil {
// data[targetField] = resp.TotalDuration
// }
// }
// }
// }
//}
return data
}
func ExtractResponseBilling(config map[string]any, response map[string]any) map[string]any {
dimensions := config["dimensions"].(map[string]any)
fields := dimensions["response"].(map[string]any)
data := make(map[string]any)
for key, path := range fields {
data[key] = gjson.New(response).Get(gconv.String(path)).Val()
}
return data
}
// ======================== 内部辅助 ========================
func buildPricingPath(config map[string]any, data map[string]any) string {
pathConfig := config["pricing_path"].(map[string]any)
segments := pathConfig["segments"].([]any)
mapping := pathConfig["mapping"].(map[string]any)
var parts []string
for _, seg := range segments {
segName := gconv.String(seg)
val := gconv.String(data[segName])
if m, ok := mapping[segName].(map[string]any); ok {
if mapped, exists := m[val]; exists {
val = gconv.String(mapped)
}
}
parts = append(parts, val)
}
return strings.Join(parts, ".")
}
func matchMinToken(config map[string]any, data map[string]any) float64 {
rule, ok := config["min_token_rule"].(map[string]any)
if !ok {
return 0
}
conditionDefs := rule["conditions"].([]any)
rows := rule["rows"].([]any)
for _, row := range rows {
r := row.(map[string]any)
conditions := r["conditions"].([]any)
matched := true
for i, cond := range conditions {
condStr := gconv.String(cond)
condDef := conditionDefs[i].(map[string]any)
condName := gconv.String(condDef["name"])
switch condDef["type"] {
case "range":
if !matchRange(condStr, gconv.Float64(data[condName])) {
matched = false
}
case "enum":
if gconv.String(data[condName]) != condStr {
matched = false
}
}
}
if matched {
return gconv.Float64(r["min_token"])
}
}
return 0
}
func matchRange(rangeStr string, val float64) bool {
rangeStr = strings.Trim(rangeStr, "[]()")
parts := strings.Split(rangeStr, ",")
if len(parts) != 2 {
return false
}
return val >= gconv.Float64(strings.TrimSpace(parts[0])) && val < gconv.Float64(strings.TrimSpace(parts[1]))
}
func extractValue(source map[string]any, path string) any {
// 数组求和:rounds.#.duration
if strings.HasPrefix(path, "rounds.#.") && !strings.Contains(path, "==") {
field := strings.TrimPrefix(path, "rounds.#.")
rounds := gjson.New(source).Get("rounds").Array()
var sum float64
for _, r := range rounds {
if strings.Contains(field, "#") {
parts := strings.SplitN(field, ".#.", 2)
arr := gjson.New(r).Get(parts[0]).Array()
for _, item := range arr {
sum += gconv.Float64(gjson.New(item).Get(parts[1]).Val())
}
} else {
sum += gconv.Float64(gjson.New(r).Get(field).Val())
}
}
return sum
}
// 条件判断:xxx.#.type==yyy 或 rounds.#.content.#.type==yyy
if strings.Contains(path, "==video_url") || strings.Contains(path, "==input_audio") {
parts := strings.Split(path, "==")
basePath := parts[0]
typ := parts[1]
segments := strings.Split(basePath, ".#.")
// 单层:content.#.type → segments = ["content", "type"]
if len(segments) == 2 && !strings.Contains(segments[0], ".") {
arr := gjson.New(source).Get(segments[0]).Array()
for _, item := range arr {
if gconv.String(gjson.New(item).Get(segments[1]).Val()) == typ {
return true
}
}
return false
}
// 嵌套:rounds.#.content.#.type → segments = ["rounds", "content", "type"]
topArr := gjson.New(source).Get(segments[0]).Array()
for _, item := range topArr {
subArr := gjson.New(item).Get(segments[1]).Array()
for _, sub := range subArr {
if gconv.String(gjson.New(sub).Get("type").Val()) == typ {
return true
}
}
}
return false
}
return gjson.New(source).Get(path).Val()
}
func extractVideoUrls(body map[string]any) []string {
var urls []string
content := gjson.New(body).Get("content").Array()
for _, c := range content {
item := c.(map[string]any)
if item["type"] == "video_url" {
if v, ok := item["video_url"].(map[string]any); ok {
if url, ok := v["url"].(string); ok {
urls = append(urls, url)
}
}
}
}
return urls
}
+10
View 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
}
+82
View File
@@ -0,0 +1,82 @@
package util
import (
"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", ".bin"
}
ct := http.DetectContentType(data)
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 "audio/mp4", "audio/x-m4a":
return ct, ".m4a"
case "video/mp4":
return ct, ".mp4"
case "video/webm":
return ct, ".webm"
case "image/png":
return ct, ".png"
case "image/jpeg":
return ct, ".jpg"
case "image/gif":
return ct, ".gif"
case "image/webp":
return ct, ".webp"
case "application/pdf":
return ct, ".pdf"
case "text/plain":
return ct, ".txt"
case "application/json":
return ct, ".json"
case "application/zip":
return ct, ".zip"
case "application/octet-stream":
return ct, ".bin"
default:
if parts := strings.Split(ct, "/"); len(parts) == 2 {
sub := parts[1]
if idx := strings.Index(sub, ";"); idx > 0 {
sub = strings.TrimSpace(sub[:idx])
}
return ct, "." + sub
}
return ct, ".bin"
}
}
// SaveTmpResult 将二进制数据写入临时文件
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 "", fmt.Errorf("创建临时目录失败: %w", 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 "", fmt.Errorf("写入临时文件失败: %w", err)
}
return path, nil
}
+79
View 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
}
+328
View File
@@ -0,0 +1,328 @@
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, requiredFields []string) (map[string]any, error) {
contentStr := gconv.String(raw[entity.ResponseBody])
if strings.TrimSpace(contentStr) == "" {
return raw, fmt.Errorf("字段 %s 为空", entity.ResponseBody)
}
contentStr = strings.Map(func(r rune) rune {
if r < 32 && r != ' ' {
return -1
}
return r
}, contentStr)
// 第一步:先解析为通用 interface{},判断是对象还是数组
var data any
if err := json.Unmarshal([]byte(contentStr), &data); err != nil {
return raw, fmt.Errorf("JSON解析失败: %w", err)
}
var arr []any
switch val := data.(type) {
case []any:
// 本身就是数组,直接赋值
arr = val
case map[string]any:
// 单个对象,包装成单元素数组,统一后续逻辑
arr = []any{val}
default:
return raw, fmt.Errorf("不支持的JSON类型,仅允许对象/数组")
}
if len(arr) == 0 {
return raw, fmt.Errorf("解析后数据数组为空")
}
// 校验每一项的必填字段
for _, field := range requiredFields {
for i, item := range arr {
itemMap, ok := item.(map[string]any)
if !ok {
return raw, fmt.Errorf("rounds[%d] 不是合法JSON对象", i)
}
if gjson.New(itemMap).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"]) {
errMsg := gconv.String(gjson.New(result).Get("error.message").Val())
if errMsg == "" {
errMsg = gconv.String(gjson.New(result).Get("error").Val())
}
if errMsg == "" {
rawBytes, _ := json.Marshal(result)
errMsg = string(rawBytes)
}
g.Log().Errorf(ctx, "[PullTaskResult] 任务失败 taskID=%s err=%s", taskID, errMsg)
return result, fmt.Errorf("任务失败: %s", errMsg)
}
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
View 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
}
+29 -12
View File
@@ -7,7 +7,7 @@ server:
database:
default:
- type: "pgsql"
host: "116.204.74.41"
host: "192.168.0.83"
port: "15432"
user: "postgres"
pass: "Bjang09@686^*^"
@@ -26,20 +26,45 @@ database:
updatedAt: "updated_at" # (可选)自动更新时间字段名称
deletedAt: "deleted_at" # (可选)软删除时间字段名称
timeMaintainDisabled: false # (可选)是否完全关闭时间更新特性,为true时CreatedAt/UpdatedAt/DeletedAt都将失效
model_gateway:
- type: "pgsql"
host: "192.168.0.83"
port: "15432"
user: "postgres"
pass: "Bjang09@686^*^"
name: "model-gateway"
prefix: ""
role: "master"
debug: true
dryRun: false
charset: "utf8"
timezone: "Asia/Shanghai"
maxIdle: 15
maxOpen: 60
maxLifetime: "30s"
maxIdleConnTime: "30s"
createdAt: "created_at"
updatedAt: "updated_at"
deletedAt: "deleted_at"
timeMaintainDisabled: false
redis:
default:
address: 116.204.74.41:6379
address: 192.168.0.83:6379
db: 0
consul:
address: 116.204.74.41:8500
address: 192.168.0.83:8500
jaeger:
addr: 116.204.74.41:4318
addr: 192.168.0.83:4318
# 本地调试用:可选自动执行 worker/cleaner(默认关闭)
asynch:
queryPending:
enabled: false
intervalSeconds: 10 # 每10秒轮询一次
limit: 10 # 每次查10条
worker:
enabled: false
intervalSeconds: 5
@@ -48,11 +73,3 @@ asynch:
cleaner:
enabled: false
intervalSeconds: 30
modelType:
types:
1: "推理模型"
2: "图片模型"
3: "音频模型"
4: "向量化模型"
5: "全模态模型"
+108
View File
@@ -0,0 +1,108 @@
package model
import (
"model-gateway/consts/public"
"github.com/gogf/gf/v2/util/gconv"
)
// 供应商编码常量
const (
SupplierAliyun = 1
SupplierVolcengine = 2
SupplierTencent = 3
SupplierHuawei = 4
SupplierBaidu = 5
SupplierOpenAI = 6
SupplierAzure = 7
SupplierAWS = 8
SupplierGoogle = 9
SupplierDeepSeek = 10
SupplierMoonshot = 11
SupplierZhipu = 12
SupplierBaichuan = 13
SupplierMinimax = 14
SupplierXunfei = 15
SupplierOthers = 16
)
// SupplierType 供应商编码类型
type SupplierType *int8
// SupplierItem 供应商项
type SupplierItem struct {
Code SupplierType `json:"code"`
Desc string `json:"desc"`
}
// 名称映射【唯一文案维护】
var supplierNameMap = map[int]string{
SupplierAliyun: "阿里云百炼",
SupplierVolcengine: "火山引擎",
SupplierTencent: "腾讯云",
SupplierHuawei: "华为云",
SupplierBaidu: "百度智能云",
SupplierOpenAI: "OpenAI",
SupplierAzure: "Azure OpenAI",
SupplierAWS: "AWS Bedrock",
SupplierGoogle: "Google Cloud",
SupplierDeepSeek: "DeepSeek",
SupplierMoonshot: "Moonshot",
SupplierZhipu: "智谱AI",
SupplierBaichuan: "百川智能",
SupplierMinimax: "MiniMax",
SupplierXunfei: "科大讯飞",
SupplierOthers: "其他",
}
// 供应商展示顺序
var supplierOrder = []int{
SupplierAliyun, SupplierVolcengine, SupplierTencent, SupplierHuawei, SupplierBaidu,
SupplierOpenAI, SupplierAzure, SupplierAWS, SupplierGoogle, SupplierDeepSeek,
SupplierMoonshot, SupplierZhipu, SupplierBaichuan, SupplierMinimax, SupplierXunfei, SupplierOthers,
}
// 全局供应商实例
var (
SupplierItemAliyun = newSupplierItem(gconv.PtrInt8(SupplierAliyun))
SupplierItemVolcengine = newSupplierItem(gconv.PtrInt8(SupplierVolcengine))
SupplierItemTencent = newSupplierItem(gconv.PtrInt8(SupplierTencent))
SupplierItemHuawei = newSupplierItem(gconv.PtrInt8(SupplierHuawei))
SupplierItemBaidu = newSupplierItem(gconv.PtrInt8(SupplierBaidu))
SupplierItemOpenAI = newSupplierItem(gconv.PtrInt8(SupplierOpenAI))
SupplierItemAzure = newSupplierItem(gconv.PtrInt8(SupplierAzure))
SupplierItemAWS = newSupplierItem(gconv.PtrInt8(SupplierAWS))
SupplierItemGoogle = newSupplierItem(gconv.PtrInt8(SupplierGoogle))
SupplierItemDeepSeek = newSupplierItem(gconv.PtrInt8(SupplierDeepSeek))
SupplierItemMoonshot = newSupplierItem(gconv.PtrInt8(SupplierMoonshot))
SupplierItemZhipu = newSupplierItem(gconv.PtrInt8(SupplierZhipu))
SupplierItemBaichuan = newSupplierItem(gconv.PtrInt8(SupplierBaichuan))
SupplierItemMinimax = newSupplierItem(gconv.PtrInt8(SupplierMinimax))
SupplierItemXunfei = newSupplierItem(gconv.PtrInt8(SupplierXunfei))
SupplierItemOthers = newSupplierItem(gconv.PtrInt8(SupplierOthers))
)
func newSupplierItem(code SupplierType) SupplierItem {
val := int(*code)
return SupplierItem{
Code: code,
Desc: supplierNameMap[val],
}
}
// GetSupplierDescByCode 根据编码获取供应商名称
func GetSupplierDescByCode(code int) string {
return supplierNameMap[code]
}
// GetSupplierOptionList 获取供应商下拉列表
func GetSupplierOptionList() []*public.Option {
var list []*public.Option
for _, code := range supplierOrder {
list = append(list, &public.Option{
Value: code,
Label: supplierNameMap[code],
})
}
return list
}
+186
View File
@@ -0,0 +1,186 @@
package model
import (
"model-gateway/consts/public"
"github.com/gogf/gf/v2/util/gconv"
)
// 模型类型编码常量
const (
TypeInference = 100 // 推理模型
TypeImage = 200 // 图片模型
TypeAudio = 300 // 音频模型
TypeVector = 400 // 向量化模型
TypeOmni = 500 // 全模态模型
TypeVideo = 600 // 视频模型
// 图片子类型
ImageSubTextToImage = 201
ImageSubImageToImage = 202
ImageSubImageEdit = 203
ImageSubImageVariation = 204
ImageSubImageTextToImage = 205
// 音频子类型
AudioSubTextToSpeech = 301
AudioSubSpeechToText = 302
AudioSubSpeechToSpeech = 303
// 向量化子类型
VectorSubEmbedding = 401
VectorSubRerank = 402
// 全模态子类型
OmniSubTextImageAudio = 501
OmniSubVision = 502
// 视频子类型
VideoSubTextToVideo = 601
VideoSubImageToVideo = 602
VideoSubImageTextToVideo = 603
VideoSubVideoToVideo = 604
)
// ModelType 编码类型
type ModelType *int
// ModelTypeItem 模型类型项
type ModelTypeItem struct {
Code ModelType `json:"code"`
Desc string `json:"desc"`
}
// TypeTree 树形结构
type TypeTree struct {
Value int `json:"value"`
Label string `json:"label"`
Children []*public.Option `json:"children"`
}
// 名称映射表【唯一文案维护入口】
var typeNameMap = map[int]string{
TypeInference: "推理模型",
TypeImage: "图片模型",
TypeAudio: "音频模型",
TypeVector: "向量化模型",
TypeOmni: "全模态模型",
TypeVideo: "视频模型",
ImageSubTextToImage: "文生图",
ImageSubImageToImage: "图生图",
ImageSubImageEdit: "图片编辑",
ImageSubImageVariation: "图片变体",
ImageSubImageTextToImage: "图文生图",
AudioSubTextToSpeech: "文生音",
AudioSubSpeechToText: "音生文",
AudioSubSpeechToSpeech: "音生音",
VectorSubEmbedding: "文本嵌入",
VectorSubRerank: "重排序",
OmniSubTextImageAudio: "文图音",
OmniSubVision: "视觉理解",
VideoSubTextToVideo: "文生视频",
VideoSubImageToVideo: "图生视频",
VideoSubImageTextToVideo: "图文生视频",
VideoSubVideoToVideo: "视频生视频",
}
// 父子级映射(仅存有子项的分类)
var parentChildMap = map[int][]int{
TypeImage: {ImageSubTextToImage, ImageSubImageToImage, ImageSubImageEdit, ImageSubImageVariation, ImageSubImageTextToImage},
TypeAudio: {AudioSubTextToSpeech, AudioSubSpeechToText, AudioSubSpeechToSpeech},
TypeVector: {VectorSubEmbedding, VectorSubRerank},
TypeOmni: {OmniSubTextImageAudio, OmniSubVision},
TypeVideo: {VideoSubTextToVideo, VideoSubImageToVideo, VideoSubImageTextToVideo, VideoSubVideoToVideo},
}
// 一级分类展示顺序
var parentTypeOrder = []int{
TypeInference, TypeImage, TypeAudio, TypeVector, TypeOmni, TypeVideo,
}
// 全局实例:一级 + 全部二级子类型,统一通过 newItem 构造,文案仅维护在 typeNameMap
var (
// 一级类型
ModelTypeInference = newItem(gconv.PtrInt(TypeInference))
ModelTypeImage = newItem(gconv.PtrInt(TypeImage))
ModelTypeAudio = newItem(gconv.PtrInt(TypeAudio))
ModelTypeVector = newItem(gconv.PtrInt(TypeVector))
ModelTypeOmni = newItem(gconv.PtrInt(TypeOmni))
ModelTypeVideo = newItem(gconv.PtrInt(TypeVideo))
// 图片二级子类型
ModelImageSubTextToImage = newItem(gconv.PtrInt(ImageSubTextToImage))
ModelImageSubImageToImage = newItem(gconv.PtrInt(ImageSubImageToImage))
ModelImageSubImageEdit = newItem(gconv.PtrInt(ImageSubImageEdit))
ModelImageSubImageVariation = newItem(gconv.PtrInt(ImageSubImageVariation))
ModelImageSubImageTextToImage = newItem(gconv.PtrInt(ImageSubImageTextToImage))
// 音频二级子类型
ModelAudioSubTextToSpeech = newItem(gconv.PtrInt(AudioSubTextToSpeech))
ModelAudioSubSpeechToText = newItem(gconv.PtrInt(AudioSubSpeechToText))
ModelAudioSubSpeechToSpeech = newItem(gconv.PtrInt(AudioSubSpeechToSpeech))
// 向量化二级子类型
ModelVectorSubEmbedding = newItem(gconv.PtrInt(VectorSubEmbedding))
ModelVectorSubRerank = newItem(gconv.PtrInt(VectorSubRerank))
// 全模态二级子类型
ModelOmniSubTextImageAudio = newItem(gconv.PtrInt(OmniSubTextImageAudio))
ModelOmniSubVision = newItem(gconv.PtrInt(OmniSubVision))
// 视频二级子类型
ModelVideoSubTextToVideo = newItem(gconv.PtrInt(VideoSubTextToVideo))
ModelVideoSubImageToVideo = newItem(gconv.PtrInt(VideoSubImageToVideo))
ModelVideoSubImageTextToVideo = newItem(gconv.PtrInt(VideoSubImageTextToVideo))
ModelVideoSubVideoToVideo = newItem(gconv.PtrInt(VideoSubVideoToVideo))
)
// newItem 构造方法:自动从 typeNameMap 读取描述
func newItem(code ModelType) ModelTypeItem {
val := int(*code)
return ModelTypeItem{
Code: code,
Desc: typeNameMap[val],
}
}
// GetDescByCode 根据编码获取名称
func GetDescByCode(code int) string {
return typeNameMap[code]
}
// GetTypeTreeList 生成树形数据
func GetTypeTreeList() []*TypeTree {
var list []*TypeTree
for _, parentCode := range parentTypeOrder {
tree := &TypeTree{
Value: parentCode,
Label: typeNameMap[parentCode],
Children: make([]*public.Option, 0),
}
if childCodes, ok := parentChildMap[parentCode]; ok {
for _, c := range childCodes {
tree.Children = append(tree.Children, &public.Option{
Value: c,
Label: typeNameMap[c],
})
}
}
list = append(list, tree)
}
return list
}
// GetAllTypeOption 全量平铺选项
func GetAllTypeOption() []*public.Option {
var list []*public.Option
for code, label := range typeNameMap {
list = append(list, &public.Option{Value: code, Label: label})
}
return list
}
+27
View File
@@ -0,0 +1,27 @@
package model
import "github.com/gogf/gf/v2/util/gconv"
var (
ResponseTypeSync = newResponseType(gconv.PtrInt8(1), "sync") // 同步
ResponseTypeAsync = newResponseType(gconv.PtrInt8(2), "async") // 异步
ResponseTypeStream = newResponseType(gconv.PtrInt8(3), "stream") // 流
)
type ResponseType *int8
type responseType struct {
code ResponseType
desc string
}
func (s responseType) Code() ResponseType {
return s.code
}
func (s responseType) Desc() string {
return s.desc
}
func newResponseType(code ResponseType, desc string) responseType {
return responseType{code: code, desc: desc}
}
+129
View File
@@ -0,0 +1,129 @@
package public
// Option 通用下拉选项
type Option struct {
Value int `json:"value"`
Label string `json:"label"`
}
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,
}
+9 -4
View File
@@ -1,8 +1,13 @@
package public
const (
TableNameModel = "asynch_models" // 模型表
TableNameTask = "asynch_task" // 任务表
TableNameOpLog = "logs_model_op" // 操作日志表
TableNameStat = "logs_model_stat" // 按天统计表(请求次数)
DbNameModelGateway = "model_gateway" //数据库名称
)
const (
TableNameModel = "model_gateway_models" // 模型表
TableNameTask = "model_gateway_task" // 任务表
TableNameOpLog = "model_gateway_logs_op" // 操作日志表
TableNameStat = "model_gateway_logs_stat" // 按天统计表
TableNameModelManage = "model_gateway_model_manage"
)
-1
View File
@@ -1 +0,0 @@
package controller
-87
View File
@@ -1,87 +0,0 @@
package controller
import (
"context"
"model-gateway/model/dto"
"model-gateway/model/entity"
"model-gateway/service"
"gitea.com/red-future/common/beans"
)
type model struct{}
// Model 模型配置控制器
var Model = new(model)
// CreateModel 添加配置
func (c *model) CreateModel(ctx context.Context, req *dto.CreateModelReq) (res *dto.CreateModelRes, err error) {
return service.Model.Create(ctx, req)
}
// UpdateModel 更改配置
func (c *model) UpdateModel(ctx context.Context, req *dto.UpdateModelReq) (res *beans.ResponseEmpty, err error) {
err = service.Model.Update(ctx, req)
return
}
// DeleteModel 删除配置
func (c *model) DeleteModel(ctx context.Context, req *dto.DeleteModelReq) (res *beans.ResponseEmpty, err error) {
err = service.Model.Delete(ctx, req.ID)
return
}
// GetModel 获取配置详情(按 modelName
func (c *model) GetModel(ctx context.Context, req *dto.GetModelReq) (res *dto.GetModelRes, err error) {
model, err := service.Model.Get(ctx, req.ID)
if err != nil {
return nil, err
}
if model == nil {
return nil, nil
}
return &dto.GetModelRes{Model: model}, nil
}
// ListModel 配置列表
func (c *model) ListModel(ctx context.Context, req *dto.ListModelReq) (res *dto.ListModelRes, err error) {
list, total, err := service.Model.List(ctx, req)
if err != nil {
return nil, err
}
return &dto.ListModelRes{
List: list,
Total: total,
}, nil
}
// AutoTune 动态调参(由上层定时任务每小时触发一次)
func (c *model) AutoTune(ctx context.Context, req *dto.AutoTuneReq) (res *dto.AutoTuneRes, err error) {
windowSeconds := 3600
if req != nil && req.WindowSeconds > 0 {
windowSeconds = req.WindowSeconds
}
list, err := service.AutoTune(ctx, windowSeconds)
if err != nil {
return nil, err
}
return &dto.AutoTuneRes{List: list}, nil
}
func (c *model) ListType(ctx context.Context, req *dto.ListTypeReq) (res dto.TypeItem, err error) {
modelType := service.GetModelTypesFromConfig(ctx)
res.Type = modelType
return res, nil
}
// UpdateChatModel 更新是否为聊天模型
func (c *model) UpdateChatModel(ctx context.Context, req *dto.UpdateChatModelReq) (res *beans.ResponseEmpty, err error) {
err = service.Model.UpdateChatModel(ctx, req)
return
}
// GetIsChatModel 获取是否为聊天模型
func (c *model) GetIsChatModel(ctx context.Context, req *dto.GetIsChatModelReq) (res *entity.AsynchModel, err error) {
return service.Model.GetIsChatModel(ctx)
}
@@ -2,17 +2,17 @@ package controller
import (
"context"
statService "model-gateway/service/stat"
"model-gateway/model/dto"
"model-gateway/service"
)
// ModelGatewayLogsStat 统计控制器
var ModelGatewayLogsStat = new(stat)
type stat struct{}
// Stat 统计控制器
var Stat = new(stat)
// ListModelStat 统计列表
func (c *stat) ListModelStat(ctx context.Context, req *dto.ListModelStatReq) (res *dto.ListModelStatRes, err error) {
return service.Stat.List(ctx, req)
return statService.ModelGatewayLogsStat.List(ctx, req)
}
@@ -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)
}
@@ -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)
}
+56
View File
@@ -0,0 +1,56 @@
package controller
import (
"context"
"model-gateway/model/dto"
"model-gateway/service"
"gitea.redpowerfuture.com/red-future/common/beans"
)
// ModelManage 模型配置控制器
var ModelManage = new(modelManage)
type modelManage struct{}
// CreateModel 添加配置
func (c *modelManage) CreateModel(ctx context.Context, req *dto.CreateModelManageReq) (res *dto.CreateModelManageRes, err error) {
return service.ModelManage.Create(ctx, req)
}
// UpdateModel 更改配置
func (c *modelManage) UpdateModel(ctx context.Context, req *dto.UpdateModelManageReq) (res *beans.ResponseEmpty, err error) {
err = service.ModelManage.Update(ctx, req)
return
}
// DeleteModel 删除配置
func (c *modelManage) DeleteModel(ctx context.Context, req *dto.DeleteModelManageReq) (res *beans.ResponseEmpty, err error) {
err = service.ModelManage.Delete(ctx, req)
return
}
// GetModel 获取配置
func (c *modelManage) GetModel(ctx context.Context, req *dto.GetModelManageReq) (res *dto.GetModelManageRes, err error) {
return service.ModelManage.Get(ctx, req)
}
// ListModel 配置列表
func (c *modelManage) ListModel(ctx context.Context, req *dto.ListModelManageReq) (res *dto.ListModelManageRes, err error) {
return service.ModelManage.List(ctx, req)
}
// CheckChatModel 检查是否为聊天模型
func (c *modelManage) CheckChatModel(ctx context.Context, req *dto.CheckChatModelReq) (res *dto.CheckChatModelRes, err error) {
return service.ModelManage.CheckChatModel(ctx, req)
}
// ListType 模型类型列表
func (c *modelManage) ListType(ctx context.Context, req *dto.ModelTypeReq) (res *dto.ModelTypeRes, err error) {
return service.ModelManage.GetModelType(ctx, req)
}
// ListOperator 运营商列表
func (c *modelManage) ListOperator(ctx context.Context, req *dto.ModelSupplierReq) (res *dto.ModelSupplierRes, err error) {
return service.ModelManage.GetModelSupplier(ctx, req)
}
-57
View File
@@ -1,57 +0,0 @@
package controller
import (
"context"
"model-gateway/model/dto"
"model-gateway/service"
)
type task struct{}
// Task 任务控制器
var Task = new(task)
// CreateTask 根据 modelName 创建异步任务,返回 taskId
func (c *task) CreateTask(ctx context.Context, req *dto.CreateTaskReq) (res *dto.CreateTaskRes, err error) {
return service.Task.Create(ctx, req)
}
// GetTaskResult 获取任务结果(只返回 oss 地址 + state
func (c *task) GetTaskResult(ctx context.Context, req *dto.GetTaskResultReq) (res *dto.GetTaskResultRes, err error) {
return service.Task.GetResult(ctx, req.TaskID)
}
// GetTaskBatch 批量查询任务(成功任务标记为已下载)
func (c *task) GetTaskBatch(ctx context.Context, req *dto.GetTaskBatchReq) (res *dto.GetTaskBatchRes, err error) {
return service.Task.GetBatch(ctx, req)
}
// ListTask 任务列表分页查询
func (c *task) ListTask(ctx context.Context, req *dto.ListTaskReq) (res *dto.ListTaskRes, err error) {
return service.Task.List(ctx, req)
}
// RunWork 手动触发一次 worker(由上层定时任务调用)
func (c *task) RunWork(ctx context.Context, req *dto.RunWorkReq) (res *dto.RunWorkRes, err error) {
batchSize, goroutines := 10, 1
if req != nil {
if req.BatchSize > 0 {
batchSize = req.BatchSize
}
if req.Goroutines > 0 {
goroutines = req.Goroutines
}
}
n, err := service.AsyncWorker.RunOnce(ctx, batchSize, goroutines)
if err != nil {
return nil, err
}
return &dto.RunWorkRes{Claimed: n}, nil
}
// CleanWork 手动触发一次 cleaner(由上层定时任务调用)
func (c *task) CleanWork(ctx context.Context, req *dto.CleanWorkReq) (res *dto.CleanWorkRes, err error) {
service.Cleaner.RunOnce(ctx)
return &dto.CleanWorkRes{Ok: true}, nil
}
-232
View File
@@ -1,232 +0,0 @@
package dao
import (
"context"
"fmt"
"model-gateway/consts/public"
"model-gateway/model/dto"
"model-gateway/model/entity"
"gitea.com/red-future/common/db/gfdb"
"gitea.com/red-future/common/utils"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/util/gconv"
)
var Model = &modelDao{}
type modelDao struct{}
func (d *modelDao) Insert(ctx context.Context, req *dto.CreateModelReq) (id int64, err error) {
asyncModel := new(entity.AsynchModel)
err = gconv.Struct(req, &asyncModel)
if err != nil {
return
}
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameModel).Data(asyncModel).Insert()
if err != nil {
return 0, err
}
return r.LastInsertId()
}
func (d *modelDao) Update(ctx context.Context, m *dto.UpdateModelReq) (rows int64, err error) {
// 触发 gfdb 的 updateHook 自动填充 updater,需要显式带 updater 字段
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameModel).
OmitEmpty().
Where(entity.AsynchModelCol.Id, m.ID).
Data(m).
Update()
if err != nil {
return 0, err
}
return r.RowsAffected()
}
func (d *modelDao) DeleteByID(ctx context.Context, id string) (rows int64, err error) {
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameModel).
Where(entity.AsynchModelCol.Id, id).
Delete()
if err != nil {
return 0, err
}
return r.RowsAffected()
}
func (d *modelDao) GetByModelName(ctx context.Context, modelName string) (m *entity.AsynchModel, err error) {
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameModel).
Where(entity.AsynchModelCol.ModelName, modelName).
One()
if err != nil {
return nil, err
}
if r.IsEmpty() {
return nil, nil
}
err = r.Struct(&m)
return
}
func (d *modelDao) Get(ctx context.Context, id int64) (m *entity.AsynchModel, err error) {
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameModel).
NoTenantId(ctx).
Where(entity.AsynchModelCol.Id, id).
One()
if err != nil {
return nil, err
}
if r.IsEmpty() {
return nil, nil
}
err = r.Struct(&m)
return
}
func (d *modelDao) Count(ctx context.Context, req *dto.GetModelReq) (count int, err error) {
count, err = gfdb.DB(ctx).Model(ctx, public.TableNameModel).OmitEmpty().
Where(entity.AsynchModelCol.Creator, req.Creator).
Where(entity.AsynchModelCol.Id, req.ID).Count()
return
}
func (d *modelDao) List(ctx context.Context, pageNum, pageSize int, modelNameLike string, modelType int, isPrivate int) (list []*entity.AsynchModel, total int64, err error) {
model := gfdb.DB(ctx).Model(ctx, public.TableNameModel).
OrderDesc(entity.AsynchModelCol.CreatedAt)
if modelNameLike != "" {
model = model.WhereLike(entity.AsynchModelCol.ModelName, "%"+modelNameLike+"%")
}
if modelType != 0 {
model = model.Where(entity.AsynchModelCol.ModelType, modelType)
}
if isPrivate != 0 {
model = model.Where(entity.AsynchModelCol.IsPrivate, isPrivate)
}
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
}
// ListByCreatorAndPlatform 普通用户:平台公共(tenant_id=0) + 自己创建的(creator=xxx)
func (d *modelDao) ListByCreatorAndPlatform(ctx context.Context, creator string, pageNum, pageSize int, modelNameLike string) (list []*entity.AsynchModel, total int64, err error) {
// 构建 Where 条件
whereSQL := "deleted_at IS NULL AND (tenant_id = 1 OR creator = ?)" //1 代表超级管理员
args := []any{creator}
if modelNameLike != "" {
whereSQL += " AND model_name LIKE ?"
args = append(args, "%"+modelNameLike+"%")
}
// 查总数
countSQL := fmt.Sprintf("SELECT COUNT(1) FROM %s WHERE %s", public.TableNameModel, whereSQL)
countResult, err := gfdb.DB(ctx).GetAll(ctx, countSQL, args...)
if err != nil {
return nil, 0, err
}
if len(countResult) > 0 {
total = gconv.Int64(countResult[0]["count"])
}
// 查列表
querySQL := fmt.Sprintf("SELECT * FROM %s WHERE %s ORDER BY created_at DESC", public.TableNameModel, whereSQL)
if pageNum > 0 && pageSize > 0 {
offset := (pageNum - 1) * pageSize
querySQL += fmt.Sprintf(" LIMIT %d OFFSET %d", pageSize, offset)
}
r, err := gfdb.DB(ctx).GetAll(ctx, querySQL, args...)
if err != nil {
return nil, 0, err
}
err = r.Structs(&list)
return
}
func (d *modelDao) GetByCreatorAndPlatform(ctx context.Context, req *dto.ListModelReq) (list []*entity.AsynchModel, total int, err error) {
// 基础 SQL
sql := `
SELECT DISTINCT ON (model_name) *
FROM asynch_models
WHERE deleted_at IS NULL
AND (? = '' OR model_name LIKE ?)
AND (? = 0 OR model_type = ?)
`
args := []any{
req.ModelName, "%" + req.ModelName + "%",
req.ModelType, req.ModelType,
}
if !g.IsEmpty(req.IsPrivate) {
sql += ` AND is_private = ? `
args = append(args, req.IsPrivate)
}
if req.IsOwner != nil && *req.IsOwner == 0 {
sql += ` AND creator = ? AND is_owner = ? `
args = append(args, req.Creator)
args = append(args, 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)
args = append(args, req.IsOwner)
}
// 最后拼接排序
sql += ` ORDER BY model_name, is_owner DESC, created_at DESC`
r, err := gfdb.DB(ctx).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
}
func (d *modelDao) GetByIsChatModel(ctx context.Context) (m *entity.AsynchModel, err error) {
userInfo, err := utils.GetUserInfo(ctx)
if err != nil {
return nil, err
}
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameModel).
Where(entity.AsynchModelCol.IsChatModel, 1).
Where(entity.AsynchModelCol.Creator, userInfo.UserName).
One()
if err != nil {
return nil, err
}
if r.IsEmpty() {
return nil, nil
}
err = r.Struct(&m)
return
}
// ListAll 用于分组展示:查询全部模型(不按类型过滤,类型拆分在 service 层处理)
func (d *modelDao) ListAll(ctx context.Context) (list []*entity.AsynchModel, err error) {
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameModel).
OrderDesc(entity.AsynchModelCol.CreatedAt).
All()
if err != nil {
return nil, err
}
err = r.Structs(&list)
return
}
-32
View File
@@ -1,32 +0,0 @@
package dao
import (
"context"
"model-gateway/consts/public"
"model-gateway/model/entity"
"gitea.com/red-future/common/db/gfdb"
)
// GetByModelNameForTenant 后台任务使用:按 tenant_id + model_name 查询,不依赖 gfdb Hook/Trace/用户上下文
func (d *modelDao) GetByModelNameForTenant(ctx context.Context, tenantId uint64, modelName string) (m *entity.AsynchModel, err error) {
r, err := gfdb.DB(ctx).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.AsynchModel
if err := r.Structs(&list); err != nil {
return nil, err
}
if len(list) == 0 {
return nil, nil
}
return list[0], nil
}
+23
View 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
View 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
}
+202
View File
@@ -0,0 +1,202 @@
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).
Where(entity.ModelGatewayModelCol.IsChatModel, req.IsChatModel).
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 ` + public.TableNameModel + `
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
}
+158
View File
@@ -0,0 +1,158 @@
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/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) {
// 1) 先查任务
var task entity.ModelGatewayTask
r, err := gfdb.DB(ctx, public.DbNameModelGateway).Model(ctx, public.TableNameTask).
Where(entity.ModelGatewayTaskCol.Id, id).
Where(entity.ModelGatewayTaskCol.State, public.TaskStatusPending).
One()
if err != nil {
return nil, err
}
if r.IsEmpty() {
return nil, fmt.Errorf("任务已被抢占或不存在: id=%d", id)
}
if err = r.Struct(&task); err != nil {
return nil, err
}
// 2) 改为执行中
_, err = gfdb.DB(ctx, public.DbNameModelGateway).Model(ctx, public.TableNameTask).
Data(&entity.ModelGatewayTask{State: public.TaskStatusRunning}).
Where(entity.ModelGatewayTaskCol.Id, id).
Where(entity.ModelGatewayTaskCol.State, public.TaskStatusPending). // 防并发
OmitEmpty().
Update()
if err != nil {
return nil, err
}
return &task, nil
}
+193
View File
@@ -0,0 +1,193 @@
package dao
import (
"context"
"fmt"
"model-gateway/consts/public"
"model-gateway/model/dto"
"model-gateway/model/entity"
"gitea.redpowerfuture.com/red-future/common/db/gfdb"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/util/gconv"
)
var ModelManage = &modelManageDao{}
type modelManageDao struct{}
// Insert 插入
func (d *modelManageDao) Insert(ctx context.Context, req *dto.CreateModelManageReq) (id int64, err error) {
var e = new(entity.ModelManage)
err = gconv.Struct(req, &e)
if err != nil {
return
}
r, err := gfdb.DB(ctx, public.DbNameModelGateway).Model(ctx, public.TableNameModelManage).Insert(e)
if err != nil {
return
}
return r.LastInsertId()
}
// Update 更新
func (d *modelManageDao) Update(ctx context.Context, req *dto.UpdateModelManageReq) (rows int64, err error) {
r, err := gfdb.DB(ctx, public.DbNameModelGateway).Model(ctx, public.TableNameModelManage).OmitEmpty().Data(req).Where(entity.ModelManageCol.Id, req.Id).Update()
if err != nil {
return
}
return r.RowsAffected()
}
// Delete 删除
func (d *modelManageDao) Delete(ctx context.Context, req *dto.DeleteModelManageReq) (rows int64, err error) {
r, err := gfdb.DB(ctx, public.DbNameModelGateway).Model(ctx, public.TableNameModelManage).Where(entity.ModelManageCol.Id, req.Id).Delete()
if err != nil {
return
}
return r.RowsAffected()
}
func (d *modelManageDao) Get(ctx context.Context, req *dto.GetModelManage, fields ...string) (res *entity.ModelManage, err error) {
r, err := gfdb.DB(ctx, public.DbNameModelGateway).Model(ctx, public.TableNameModelManage).Cache(ctx).
OmitEmpty().
Where(entity.ModelManageCol.ModelName, req.ModelName).
Where(entity.ModelManageCol.ChatModel, req.ChatModel).
Where(entity.ModelManageCol.Creator, req.Creator).
Fields(fields).One()
if err != nil {
return
}
err = r.Struct(&res)
return
}
func (d *modelManageDao) GetNotTenantId(ctx context.Context, req *dto.GetModelManageReq, fields ...string) (res *entity.ModelManage, err error) {
// 获取表前缀
prefix := g.Cfg().MustGet(ctx, fmt.Sprintf("database.%s.0.prefix", public.DbNameModelGateway)).String()
table := prefix + public.TableNameModelManage
// 动态拼接 SELECT 列
var field string
if !g.IsEmpty(fields) {
for k, v := range fields {
if k == len(fields)-1 {
field = field + v
} else {
field = field + v + ","
}
}
} else {
field = "*"
}
// 动态拼接 WHERE 条件
var whereCondition string
var queryParams []interface{}
if !g.IsEmpty(req.Id) {
whereCondition = fmt.Sprintf(" AND %s=(?) ", entity.ModelManageCol.Id)
queryParams = append(queryParams, req.Id)
}
whereCondition = whereCondition + " AND " + entity.ModelManageCol.DeletedAt + " IS NULL "
sql := `SELECT ` + field + ` FROM ` + table + ` WHERE 1=1 ` + whereCondition + ``
// 执行查询
result, err := gfdb.DB(ctx, public.DbNameModelGateway).GetOne(ctx, sql, queryParams...)
if err != nil {
return nil, err
}
err = result.Struct(&res)
return
}
func (d *modelManageDao) ListNotTenantId(ctx context.Context, req *dto.ListModelManageReq, fields ...string) (res []*entity.ModelManage, total int, err error) {
// 获取表前缀
prefix := g.Cfg().MustGet(ctx, fmt.Sprintf("database.%s.0.prefix", public.DbNameModelGateway)).String()
table := prefix + public.TableNameModelManage
// 动态拼接 SELECT 列
var field string
if !g.IsEmpty(fields) {
for k, v := range fields {
if k == len(fields)-1 {
field = field + v
} else {
field = field + v + ","
}
}
} else {
field = "*"
}
// 动态拼接 WHERE 条件
var whereCondition string
var queryParams []interface{}
if !g.IsEmpty(req.ModelName) {
whereCondition += fmt.Sprintf(" AND %s=(?) ", entity.ModelManageCol.ModelName)
queryParams = append(queryParams, req.ModelName)
}
if !g.IsEmpty(req.ModelType) {
whereCondition += fmt.Sprintf(" AND %s=(?) ", entity.ModelManageCol.ModelType)
queryParams = append(queryParams, req.ModelType)
}
if !g.IsEmpty(req.Creator) {
whereCondition += fmt.Sprintf(" AND (%s=(?) OR %s=true) ", entity.ModelManageCol.Creator, entity.ModelManageCol.SystemModel)
queryParams = append(queryParams, req.Creator)
}
whereCondition = whereCondition + " AND " + entity.ModelManageCol.DeletedAt + " IS NULL "
// 1. 统计去重后总条数
countSql := fmt.Sprintf(
`SELECT COUNT(DISTINCT %s) FROM %s WHERE 1=1 %s`,
entity.ModelManageCol.ModelName,
table,
whereCondition,
)
countResult, err := gfdb.DB(ctx, public.DbNameModelGateway).GetOne(ctx, countSql, queryParams...)
if err != nil {
return nil, 0, err
}
type crr struct {
Count int64 `db:"count"`
}
var cr crr
if err = countResult.Struct(&cr); err != nil {
return nil, 0, err
}
total = int(cr.Count)
// 2. 分页处理
limitSql := ""
if req.Page != nil {
pageNum := int(req.Page.PageNum)
pageSize := int(req.Page.PageSize)
offset := (pageNum - 1) * pageSize
limitSql = fmt.Sprintf(" LIMIT ? OFFSET ? ")
// PG 语法 LIMIT 条数 OFFSET 偏移量
queryParams = append(queryParams, pageSize, offset)
}
// 排序优先级:1.分组字段ModelName 2.SystemModel升序(false在前,保留用户数据) 3.创建时间倒序
orderSql := fmt.Sprintf(
" ORDER BY %s, %s ASC, %s DESC ",
entity.ModelManageCol.ModelName,
entity.ModelManageCol.SystemModel,
entity.ModelManageCol.CreatedAt,
)
// PG DISTINCT ON 按模型名去重,同名只取第一条(用户数据)
sql := fmt.Sprintf(
`SELECT DISTINCT ON (%s) %s FROM %s WHERE 1=1 %s %s %s`,
entity.ModelManageCol.ModelName,
field,
table,
whereCondition,
orderSql,
limitSql,
)
// 执行查询
result, err := gfdb.DB(ctx, public.DbNameModelGateway).GetAll(ctx, sql, queryParams...)
if err != nil {
return nil, 0, err
}
err = result.Structs(&res)
return
}
-22
View File
@@ -1,22 +0,0 @@
package dao
import (
"context"
"model-gateway/consts/public"
"model-gateway/model/entity"
"gitea.com/red-future/common/db/gfdb"
)
type opLogDao struct{}
var OpLog = &opLogDao{}
func (d *opLogDao) Insert(ctx context.Context, log *entity.LogsModelOp) (id int64, err error) {
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameOpLog).Data(log).Insert()
if err != nil {
return 0, err
}
return r.LastInsertId()
}
-60
View File
@@ -1,60 +0,0 @@
package dao
import (
"context"
"fmt"
"time"
"model-gateway/consts/public"
"model-gateway/model/entity"
"gitea.com/red-future/common/db/gfdb"
"github.com/gogf/gf/v2/os/gtime"
)
type statDao struct{}
var Stat = &statDao{}
// IncRequestCount 原子累加(支持分布式/多协程):按天+租户+创建人+模型 +1
func (d *statDao) IncRequestCount(ctx context.Context, day time.Time, tenantId int64, creator, modelName string) error {
sql := fmt.Sprintf(`
INSERT INTO %s(day, tenant_id, creator, model_name, request_count, created_at, updated_at)
VALUES(?, ?, ?, ?, 1, NOW(), NOW())
ON CONFLICT (day, tenant_id, creator, model_name)
DO UPDATE SET request_count = %s.request_count + 1, updated_at = NOW()`,
public.TableNameStat, public.TableNameStat,
)
_, err := gfdb.DB(ctx).Exec(ctx, sql, gtime.New(day).Format("Y-m-d"), tenantId, creator, modelName)
return err
}
func (d *statDao) List(ctx context.Context, pageNum, pageSize int, startDay, endDay string, tenantId *int64, creator, modelName string) (list []*entity.LogsModelStat, total int64, err error) {
m := gfdb.DB(ctx).Model(ctx, public.TableNameStat).Where("1=1")
if startDay != "" {
m = m.Where("day >= ?", startDay)
}
if endDay != "" {
m = m.Where("day <= ?", endDay)
}
if tenantId != nil {
m = m.Where("tenant_id = ?", *tenantId)
}
if creator != "" {
m = m.WhereLike("creator", "%"+creator+"%")
}
if modelName != "" {
m = m.WhereLike("model_name", "%"+modelName+"%")
}
m = m.OrderDesc("day").OrderDesc("request_count")
if pageNum > 0 && pageSize > 0 {
m = m.Page(pageNum, pageSize)
}
r, totalInt, err := m.AllAndCount(false)
if err != nil {
return nil, 0, err
}
total = int64(totalInt)
err = r.Structs(&list)
return
}
-250
View File
@@ -1,250 +0,0 @@
package dao
import (
"context"
"fmt"
"time"
"model-gateway/consts/public"
"model-gateway/model/entity"
"gitea.com/red-future/common/db/gfdb"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/gconv"
)
var Task = &taskDao{}
type taskDao struct{}
func (d *taskDao) Insert(ctx context.Context, t *entity.AsynchTask) (id int64, err error) {
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameTask).Data(t).Insert()
if err != nil {
return 0, err
}
return r.LastInsertId()
}
func (d *taskDao) GetByTaskID(ctx context.Context, taskID string) (t *entity.AsynchTask, err error) {
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameTask).
Where(entity.AsynchTaskCol.TaskID, taskID).
One()
if err != nil {
return nil, err
}
if r.IsEmpty() {
return nil, nil
}
err = r.Struct(&t)
return
}
// ListByTaskIDs 批量查询任务(会受 gfdb 的租户 Hook 影响,只返回当前租户数据)
func (d *taskDao) ListByTaskIDs(ctx context.Context, taskIDs []string) (list []*entity.AsynchTask, err error) {
if len(taskIDs) == 0 {
return nil, nil
}
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameTask).
WhereIn(entity.AsynchTaskCol.TaskID, taskIDs).
All()
if err != nil {
return nil, err
}
err = r.Structs(&list)
return
}
// MarkDownloadedByID 将成功任务标记为已下载(state=4),并写入过期时间
func (d *taskDao) MarkDownloadedByID(ctx context.Context, id int64, expireAt *gtime.Time) error {
data := gdb.Map{
entity.AsynchTaskCol.State: 4,
entity.AsynchTaskCol.ExpireAt: expireAt,
entity.AsynchTaskCol.Updater: "",
}
_, err := gfdb.DB(ctx).Model(ctx, public.TableNameTask).
Where(entity.AsynchTaskCol.Id, id).
Where(entity.AsynchTaskCol.State, 2).
Data(data).
Update()
return err
}
func (d *taskDao) UpdateRunning(ctx context.Context, id int64) error {
now := gtime.Now()
data := gdb.Map{
entity.AsynchTaskCol.State: 1,
entity.AsynchTaskCol.StartedAt: now,
entity.AsynchTaskCol.Updater: "",
}
_, err := gfdb.DB(ctx).Model(ctx, public.TableNameTask).
Where(entity.AsynchTaskCol.Id, id).
Data(data).
Update()
return err
}
func (d *taskDao) UpdateSuccess(ctx context.Context, id int64, ossFile, fileType string, fileSize int64, expireAt *gtime.Time) error {
now := gtime.Now()
data := gdb.Map{
entity.AsynchTaskCol.State: 2,
entity.AsynchTaskCol.OssFile: ossFile,
entity.AsynchTaskCol.FileType: fileType,
entity.AsynchTaskCol.FileSize: fileSize,
entity.AsynchTaskCol.ErrorMsg: "",
entity.AsynchTaskCol.FinishedAt: now,
entity.AsynchTaskCol.ExpireAt: expireAt,
entity.AsynchTaskCol.Updater: "",
}
_, err := gfdb.DB(ctx).Model(ctx, public.TableNameTask).
Where(entity.AsynchTaskCol.Id, id).
Data(data).
Update()
return err
}
func (d *taskDao) UpdateFailed(ctx context.Context, id int64, errorMsg string) error {
now := gtime.Now()
data := gdb.Map{
entity.AsynchTaskCol.State: 3,
entity.AsynchTaskCol.ErrorMsg: errorMsg,
entity.AsynchTaskCol.FinishedAt: now,
entity.AsynchTaskCol.Updater: "",
}
_, err := gfdb.DB(ctx).Model(ctx, public.TableNameTask).
Where(entity.AsynchTaskCol.Id, id).
Data(data).
Update()
return err
}
func (d *taskDao) SoftDeleteByTaskID(ctx context.Context, taskID string) (rows int64, err error) {
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameTask).
Where(entity.AsynchTaskCol.TaskID, taskID).
Delete()
if err != nil {
return 0, err
}
return r.RowsAffected()
}
// CountActiveByModel 统计某模型排队中/执行中的任务数,用于 queue_limit 限制(近似值)
func (d *taskDao) CountActiveByModel(ctx context.Context, modelName string) (int64, error) {
n, err := gfdb.DB(ctx).Model(ctx, public.TableNameTask).
Where(entity.AsynchTaskCol.ModelName, modelName).
WhereIn(entity.AsynchTaskCol.State, []int{0, 1}).
Count()
return int64(n), err
}
// List 任务分页查询(受 gfdb 租户 Hook 影响)
func (d *taskDao) List(ctx context.Context, pageNum, pageSize int, modelNameLike, taskIDLike string, state *int) (list []*entity.AsynchTask, total int64, err error) {
m := gfdb.DB(ctx).Model(ctx, public.TableNameTask).Where("deleted_at IS NULL")
if modelNameLike != "" {
m = m.WhereLike(entity.AsynchTaskCol.ModelName, "%"+modelNameLike+"%")
}
if taskIDLike != "" {
m = m.WhereLike(entity.AsynchTaskCol.TaskID, "%"+taskIDLike+"%")
}
if state != nil {
m = m.Where(entity.AsynchTaskCol.State, *state)
}
m = m.OrderDesc(entity.AsynchTaskCol.CreatedAt)
if pageNum > 0 && pageSize > 0 {
m = m.Page(pageNum, pageSize)
}
r, totalInt, err := m.AllAndCount(false)
if err != nil {
return nil, 0, err
}
total = gconv.Int64(totalInt)
err = r.Structs(&list)
return
}
// ClaimPending 抢占 pending 任务(state=0),并在同一事务中更新为 running(state=1
// 使用 PostgreSQL: FOR UPDATE SKIP LOCKED 避免多 worker 重复消费
func (d *taskDao) ClaimPending(ctx context.Context, batchSize int) (tasks []*entity.AsynchTask, err error) {
if batchSize <= 0 {
batchSize = 1
}
err = gfdb.DB(ctx).Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
sql := fmt.Sprintf(
`SELECT id, tenant_id, model_name, task_id, input_ref, request_payload
FROM %s
WHERE deleted_at IS NULL AND state = 0
ORDER BY created_at ASC
LIMIT %d
FOR UPDATE SKIP LOCKED`,
public.TableNameTask,
batchSize,
)
r, err := tx.GetAll(sql)
if err != nil {
return err
}
if r.IsEmpty() {
tasks = nil
return nil
}
if err := r.Structs(&tasks); err != nil {
return err
}
// 更新为 running
now := time.Now()
for _, t := range tasks {
// tx.Model 不走 gfdb Hook,这里手动更新必要字段
_, err = tx.Exec(
fmt.Sprintf(`UPDATE %s SET state=1, started_at=?, updated_at=? WHERE id=?`, public.TableNameTask),
now, now, t.Id,
)
if err != nil {
return err
}
}
return nil
})
return
}
// ListExpiredSuccess 获取已成功且过期的任务
func (d *taskDao) ListExpiredSuccess(ctx context.Context, limit int) (list []*entity.AsynchTask, err error) {
if limit <= 0 {
limit = 100
}
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameTask).
Where(entity.AsynchTaskCol.State, 2).
Where(entity.AsynchTaskCol.ExpireAt+" IS NOT NULL").
Where(entity.AsynchTaskCol.ExpireAt+" < ?", gtime.Now()).
Limit(limit).
All()
if err != nil {
return nil, err
}
err = r.Structs(&list)
return
}
// ListTimeoutTasks 获取超时的排队/执行中任务
func (d *taskDao) ListTimeoutTasks(ctx context.Context, timeout time.Duration, limit int) (list []*entity.AsynchTask, err error) {
if limit <= 0 {
limit = 100
}
deadline := gtime.New(time.Now().Add(-timeout))
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameTask).
WhereIn(entity.AsynchTaskCol.State, []int{0, 1}).
Where(entity.AsynchTaskCol.UpdatedAt+" < ?", deadline).
Limit(limit).
All()
if err != nil {
return nil, err
}
err = r.Structs(&list)
return
}
// DebugPing 用于启动时检测数据库连通性(可选)
func (d *taskDao) DebugPing(ctx context.Context) error {
_, err := gfdb.DB(ctx).GetAll(ctx, "SELECT 1")
return err
}
-286
View File
@@ -1,286 +0,0 @@
package dao
import (
"context"
"fmt"
"time"
"model-gateway/consts/public"
"model-gateway/model/entity"
"gitea.com/red-future/common/db/gfdb"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/os/gtime"
)
// ClaimPendingGlobal 后台任务使用:全局抢占 pending 任务(不加 tenant 过滤)
func (d *taskDao) ClaimPendingGlobal(ctx context.Context, batchSize int) (tasks []*entity.AsynchTask, err error) {
if batchSize <= 0 {
batchSize = 1
}
err = gfdb.DB(ctx).Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
sql := fmt.Sprintf(
`SELECT id, tenant_id, creator, model_name, task_id, biz_name, callback_url, model_key, input_ref, request_payload, phase, tmp_file
FROM %s
WHERE deleted_at IS NULL AND state = 0
ORDER BY enqueue_at ASC
LIMIT %d
FOR UPDATE SKIP LOCKED`,
public.TableNameTask,
batchSize,
)
r, err := tx.GetAll(sql)
if err != nil {
return err
}
if r.IsEmpty() {
tasks = nil
return nil
}
if err := r.Structs(&tasks); err != nil {
return err
}
now := time.Now()
for _, t := range tasks {
_, err = tx.Exec(
fmt.Sprintf(`UPDATE %s SET state=1, started_at=?, updated_at=? WHERE id=?`, public.TableNameTask),
now, now, t.Id,
)
if err != nil {
return err
}
}
return nil
})
return
}
// ClaimPendingByTaskIDGlobal 按 task_id 定向抢占单个 pending 任务(不加 tenant 过滤)
// 用于 createTask 创建成功后立即异步尝试执行当前任务,避免只依赖后续 runWork 扫描队列。
func (d *taskDao) ClaimPendingByTaskIDGlobal(ctx context.Context, taskID string) (task *entity.AsynchTask, err error) {
if taskID == "" {
return nil, nil
}
err = gfdb.DB(ctx).Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
sql := fmt.Sprintf(
`SELECT id, tenant_id, creator, model_name, task_id, biz_name, callback_url, model_key, input_ref, request_payload, phase, tmp_file
FROM %s
WHERE deleted_at IS NULL AND state = 0 AND task_id = ?
LIMIT 1
FOR UPDATE SKIP LOCKED`,
public.TableNameTask,
)
r, err := tx.GetOne(sql, taskID)
if err != nil {
return err
}
if r.IsEmpty() {
task = nil
return nil
}
if err := r.Struct(&task); err != nil {
return err
}
now := time.Now()
_, err = tx.Exec(
fmt.Sprintf(`UPDATE %s SET state=1, started_at=?, updated_at=? WHERE id=?`, public.TableNameTask),
now, now, task.Id,
)
return err
})
return
}
func (d *taskDao) UpdateSuccessGlobal(ctx context.Context, id int64, ossFile, fileType, textResult string, fileSize int64, expireAt *gtime.Time, expendTokens int) error {
now := gtime.Now()
_, err := gfdb.DB(ctx).Exec(ctx,
fmt.Sprintf(`UPDATE %s
SET state=2,
oss_file=?,
file_type=?,
text_result=?,
expend_tokens=?,
file_size=?,
error_msg='',
finished_at=?,
duration_seconds=EXTRACT(EPOCH FROM (? - created_at))::BIGINT,
expire_at=NULL,
phase=0,
tmp_file='',
updated_at=?
WHERE id=?`, public.TableNameTask),
ossFile, fileType, textResult, expendTokens, fileSize, now, now, now, id,
)
return err
}
func (d *taskDao) UpdateFailedGlobal(ctx context.Context, id int64, errorMsg string) error {
now := gtime.Now()
_, err := gfdb.DB(ctx).Exec(ctx,
fmt.Sprintf(`UPDATE %s
SET state=3,
error_msg=?,
finished_at=?,
duration_seconds=EXTRACT(EPOCH FROM (? - created_at))::BIGINT,
phase=0,
tmp_file='',
updated_at=?
WHERE id=?`, public.TableNameTask),
errorMsg, now, now, now, id,
)
return err
}
// UpdateFailedKeepTmpGlobal OSS 上传失败:保留 phase/tmp_file,下一轮仅重试 OSS 上传
func (d *taskDao) UpdateFailedKeepTmpGlobal(ctx context.Context, id int64, errorMsg string) error {
now := gtime.Now()
_, err := gfdb.DB(ctx).Exec(ctx,
fmt.Sprintf(`UPDATE %s SET state=3, error_msg=?, finished_at=?, phase=1, updated_at=? WHERE id=?`, public.TableNameTask),
errorMsg, now, now, id,
)
return err
}
// UpdateTmpAfterModelGlobal 模型调用成功后,写入临时文件路径并标记 phase=1
func (d *taskDao) UpdateTmpAfterModelGlobal(ctx context.Context, id int64, tmpFile string) error {
_, err := gfdb.DB(ctx).Exec(ctx,
fmt.Sprintf(`UPDATE %s SET phase=1, tmp_file=?, updated_at=NOW() WHERE id=?`, public.TableNameTask),
tmpFile, id,
)
return err
}
func (d *taskDao) SoftDeleteByTaskIDGlobal(ctx context.Context, taskID string) error {
_, err := gfdb.DB(ctx).Exec(ctx,
fmt.Sprintf(`UPDATE %s SET deleted_at=NOW(), updated_at=NOW() WHERE task_id=? AND deleted_at IS NULL`, public.TableNameTask),
taskID,
)
return err
}
func (d *taskDao) RollbackToPendingGlobal(ctx context.Context, id int64) error {
_, err := gfdb.DB(ctx).Exec(ctx,
fmt.Sprintf(`UPDATE %s SET state=0, enqueue_at=NOW(), updated_at=NOW() WHERE id=? AND state=1`, public.TableNameTask),
id,
)
return err
}
// ListExpiredDownloadedGlobal 获取已下载(state=4)且过期的任务,用于清理
func (d *taskDao) ListExpiredDownloadedGlobal(ctx context.Context, limit int) (list []*entity.AsynchTask, err error) {
if limit <= 0 {
limit = 200
}
r, err := gfdb.DB(ctx).GetAll(ctx,
fmt.Sprintf(`SELECT * FROM %s WHERE deleted_at IS NULL AND state=4 AND expire_at IS NOT NULL AND expire_at < ? LIMIT ?`, public.TableNameTask),
gtime.Now(), limit,
)
if err != nil {
return nil, err
}
err = r.Structs(&list)
return
}
// ListFailedRetryableGlobal 获取失败(state=3)且仍可重试的任务
// retry_count 不含首次执行;retry_times 表示失败后最多再重试 N 次
func (d *taskDao) ListFailedRetryableGlobal(ctx context.Context, limit int) (list []*entity.AsynchTask, err error) {
if limit <= 0 {
limit = 200
}
r, err := gfdb.DB(ctx).GetAll(ctx,
fmt.Sprintf(`
SELECT t.*,
m.retry_queue_max_seconds AS retry_queue_max_seconds
FROM %s t
JOIN %s m
ON t.tenant_id = m.tenant_id
AND t.model_name = m.model_name
WHERE t.deleted_at IS NULL
AND t.state = 3
AND t.retry_count < m.retry_times
ORDER BY t.updated_at ASC
LIMIT ?`, public.TableNameTask, public.TableNameModel),
limit,
)
if err != nil {
return nil, err
}
err = r.Structs(&list)
return
}
// RequeueForRetryGlobal 将任务重新入队(state=0),并将 retry_count +1
// enqueueAt 用于控制重试任务在队列中的位置:
// - enqueueAt 越早,越靠前(ClaimPendingGlobal 按 enqueue_at ASC 抢占)
func (d *taskDao) RequeueForRetryGlobal(ctx context.Context, id int64, enqueueAt time.Time) error {
_, err := gfdb.DB(ctx).Exec(ctx,
fmt.Sprintf(`UPDATE %s SET state=0, retry_count=retry_count+1, enqueue_at=?, updated_at=NOW() WHERE id=? AND state=3 AND deleted_at IS NULL`, public.TableNameTask),
enqueueAt, id,
)
return err
}
// ListFailedExhaustedGlobal 获取失败(state=3)且超过重试次数的任务,用于硬删除
func (d *taskDao) ListFailedExhaustedGlobal(ctx context.Context, limit int) (list []*entity.AsynchTask, err error) {
if limit <= 0 {
limit = 200
}
r, err := gfdb.DB(ctx).GetAll(ctx,
fmt.Sprintf(`
SELECT t.*
FROM %s t
JOIN %s m
ON t.tenant_id = m.tenant_id
AND t.model_name = m.model_name
WHERE t.deleted_at IS NULL
AND t.state = 3
AND t.retry_count >= m.retry_times
ORDER BY t.updated_at ASC
LIMIT ?`, public.TableNameTask, public.TableNameModel),
limit,
)
if err != nil {
return nil, err
}
err = r.Structs(&list)
return
}
// HardDeleteByIDGlobal 硬删除任务记录
func (d *taskDao) HardDeleteByIDGlobal(ctx context.Context, id int64) error {
_, err := gfdb.DB(ctx).Exec(ctx,
fmt.Sprintf(`DELETE FROM %s WHERE id=?`, public.TableNameTask),
id,
)
return err
}
// ListTimeoutTasksGlobal 根据模型配置 expected_seconds 判定超时任务:
// - state in (0,1)
// - 模型 expected_seconds > 0
// - now - created_at >= expected_seconds
func (d *taskDao) ListTimeoutTasksGlobal(ctx context.Context, limit int) (list []*entity.AsynchTask, err error) {
if limit <= 0 {
limit = 200
}
r, err := gfdb.DB(ctx).GetAll(ctx,
fmt.Sprintf(`
SELECT t.*
FROM %s t
JOIN %s m
ON t.tenant_id = m.tenant_id
AND t.model_name = m.model_name
WHERE t.deleted_at IS NULL
AND t.state IN (0,1)
AND m.expected_seconds > 0
AND t.created_at < (NOW() - (m.expected_seconds || ' seconds')::interval)
LIMIT ?`, public.TableNameTask, public.TableNameModel),
limit,
)
if err != nil {
return nil, err
}
err = r.Structs(&list)
return
}
+52 -48
View File
@@ -1,41 +1,36 @@
module model-gateway
go 1.26.0
go 1.26.1
require (
gitea.com/red-future/common v0.0.19
github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.0
github.com/gogf/gf/contrib/nosql/redis/v2 v2.10.0
github.com/gogf/gf/v2 v2.10.0
gitea.redpowerfuture.com/red-future/common v0.0.30
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.14.2
github.com/tidwall/gjson v1.19.0
)
require (
github.com/r3labs/diff/v2 v2.15.1 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
google.golang.org/appengine v1.6.7 // indirect
)
require (
github.com/BurntSushi/toml v1.5.0 // indirect
github.com/BurntSushi/toml v1.6.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/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/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/fatih/color v1.19.0 // indirect
github.com/fsnotify/fsnotify v1.10.1 // 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/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/goccy/go-json v0.10.6 // 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
@@ -44,53 +39,62 @@ require (
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/google/btree v1.1.3 // indirect
github.com/google/flatbuffers v25.12.19+incompatible // 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/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
github.com/hashicorp/consul/api v1.33.5 // 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/go-version v1.9.0 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/hashicorp/serf v0.10.1 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/klauspost/compress v1.18.6 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/lib/pq v1.12.3 // 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/mattn/go-colorable v0.1.15 // indirect
github.com/mattn/go-isatty v0.0.22 // indirect
github.com/mattn/go-runewidth v0.0.24 // indirect
github.com/miekg/dns v1.1.72 // 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/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
github.com/olekukonko/errors v1.3.0 // indirect
github.com/olekukonko/ll v0.1.8 // indirect
github.com/olekukonko/tablewriter v1.1.4 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/redis/go-redis/v9 v9.12.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/tidwall/sjson v1.2.5
github.com/r3labs/diff/v2 v2.15.1 // indirect
github.com/redis/go-redis/v9 v9.21.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/tidwall/match v1.2.0 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tiger1103/gfast-token v1.0.10 // indirect
github.com/vcaesar/cedar v0.30.0 // indirect
go.mongodb.org/mongo-driver/v2 v2.4.0 // indirect
go.opencensus.io v0.23.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel v1.38.0 // indirect
github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
go.mongodb.org/mongo-driver/v2 v2.6.0 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel v1.44.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/otel/metric v1.44.0 // indirect
go.opentelemetry.io/otel/sdk v1.44.0 // indirect
go.opentelemetry.io/otel/trace v1.44.0 // indirect
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect
google.golang.org/grpc v1.75.0 // indirect
google.golang.org/protobuf v1.36.8 // indirect
go.uber.org/atomic v1.11.0 // indirect
golang.org/x/exp v0.0.0-20260611194520-c48552f49976 // indirect
golang.org/x/net v0.56.0 // indirect
golang.org/x/sys v0.46.0 // indirect
golang.org/x/text v0.38.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
google.golang.org/grpc v1.79.3 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+118 -104
View File
@@ -1,9 +1,11 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
gitea.com/red-future/common v0.0.19 h1:9/WrfCFUCeFUYwuhBYF+JOQi5F5xuOy+gVnf2ZvHZu4=
gitea.com/red-future/common v0.0.19/go.mod h1:6/nqIucVzmjOyqDTIq71feYBXXFNBy0rFwzaQ0/Ueoo=
gitea.redpowerfuture.com/red-future/common v0.0.29 h1:5McaN5pSewvrLUHQzWMX6EaUvD+B5I5bMYoU+clHJk4=
gitea.redpowerfuture.com/red-future/common v0.0.29/go.mod h1:50U1Xi+Ie56z09S5LQbZvaken0Mxv3OeS9LgR7U/ZRY=
gitea.redpowerfuture.com/red-future/common v0.0.30 h1:UkWYubUsLPJQUhEhc9Ca2UPg5iLC6jzURo3ngztINYg=
gitea.redpowerfuture.com/red-future/common v0.0.30/go.mod h1:zuhqbWHd/YICalYJnmecY8Vqo5j4dtZOD63P96fiFeU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
@@ -36,6 +38,10 @@ github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -47,8 +53,6 @@ github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWa
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=
@@ -61,10 +65,10 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
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/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
github.com/go-ego/gse v1.0.2 h1:+27lYFPhQEhA9igtdOsJPRKYL/k3TwYsxBF5jr6KFv4=
github.com/go-ego/gse v1.0.2/go.mod h1:Fy35G+q7VV7Et1zIKO8o/sW1kkugV3znXap/lF/11zc=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
@@ -77,16 +81,20 @@ github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ4
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.0 h1:39+jbTenm7KBj4hO2C8ANAxVHpX/7OuRDs1VcGC9ylA=
github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.0/go.mod h1:B0s0fVzn0W220E8UTpSGzrrGKsop5KcB90twBeLCiz0=
github.com/gogf/gf/contrib/nosql/redis/v2 v2.10.0 h1:N/F9CuDdUZLoM1nVRqrDE/33pDZuhVxpNY4wYdeIaBs=
github.com/gogf/gf/contrib/nosql/redis/v2 v2.10.0/go.mod h1:x6uoJGfZOtirIRQls8xUlYzC6f7T/eULPUa9er368X0=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gogf/gf/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.0 h1:rzDROlyqGMe/eM6dCalSR8dZOuMIdLhmxKSH1DGhbFs=
github.com/gogf/gf/v2 v2.10.0/go.mod h1:Svl1N+E8G/QshU2DUbh/3J/AJauqCgUnxHurXWR4Qx0=
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=
@@ -114,10 +122,10 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6
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/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/flatbuffers v25.12.19+incompatible h1:haMV2JRRJCe1998HeW/p0X9UaMTK6SDo0ffLn2+DbLs=
github.com/google/flatbuffers v25.12.19+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -134,12 +142,12 @@ github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5T
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/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
github.com/hashicorp/consul/api v1.33.5 h1:Nn6q87zudRU1rLBTJEgaWxz9STCNadilLCD7B8OA5aI=
github.com/hashicorp/consul/api v1.33.5/go.mod h1:pa6fJOSHKLOzNHpUVeqLDtxA5+J1D7NNzLasuk8eRXA=
github.com/hashicorp/consul/sdk v0.17.3 h1:oZMMxzQGSsiT+ToOH50y3Qcs0nc9Ud+7L5lRx+EmMU0=
github.com/hashicorp/consul/sdk v0.17.3/go.mod h1:jnOmYjiNfVRpBaujQ1DFFVs0N6g3S1y6wygSjLTzYfc=
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=
@@ -169,8 +177,8 @@ github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b
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/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA=
github.com/hashicorp/go-version v1.9.0/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=
@@ -185,8 +193,10 @@ github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
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=
@@ -196,8 +206,8 @@ 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/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
@@ -205,39 +215,39 @@ github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVc
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-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
github.com/mattn/go-colorable v0.1.15/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/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU=
github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs=
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
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/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
github.com/olekukonko/errors v1.3.0 h1:teJvgLGUEqMzBUms+Dj3/3szNqCG/Jdw9iDbum8fR6U=
github.com/olekukonko/errors v1.3.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/ll v0.1.8 h1:ysHCJRGHYKzmBSdz9w5AySztx7lG8SQY+naTGYUbsz8=
github.com/olekukonko/ll v0.1.8/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw=
github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I=
github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
@@ -264,13 +274,10 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT
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/redis/go-redis/v9 v9.21.0 h1:FPBE4hhbAke+TLmcY3WkpbDffJEomdqPn3HYiqAtL9E=
github.com/redis/go-redis/v9 v9.21.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
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=
@@ -278,24 +285,26 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx
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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
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.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tidwall/gjson v1.14.2 h1:6BBkirS0rAHjumnjHF6qgy5d2YAJ1TLIaFE2lzfOLqo=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/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.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/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=
@@ -307,28 +316,32 @@ github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaU
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=
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
go.mongodb.org/mongo-driver/v2 v2.6.0 h1:b9sJOYrkmt4l8bY43ZenFBcPlhYIjaOfYHLtbB/5qi8=
go.mongodb.org/mongo-driver/v2 v2.6.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
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/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
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/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@@ -337,15 +350,15 @@ golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3
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/exp v0.0.0-20260611194520-c48552f49976 h1:X8Hz2ImujgbmetVuW+w2YkyZChE3cBpZi2P158rTG9M=
golang.org/x/exp v0.0.0-20260611194520-c48552f49976/go.mod h1:vnf4pv9iKZXY58sQE1L86zmNWJ4159e1RkcWiLCkeEY=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
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=
@@ -361,8 +374,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
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=
@@ -371,8 +384,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
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=
@@ -396,16 +409,15 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
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=
@@ -415,8 +427,8 @@ golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
golang.org/x/tools v0.46.0 h1:7jTurBkPZu4moS/Uy4OQT1M+QBlsj3wejyZwsT8Z7rk=
golang.org/x/tools v0.46.0/go.mod h1:FrD85F8l+NWL+9XWBSyVSHO6Ne4jutsfIFba7AWQ5Ys=
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=
@@ -426,20 +438,22 @@ 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/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0=
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
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/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
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=
@@ -449,8 +463,8 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+19 -40
View File
@@ -2,17 +2,18 @@ package main
import (
"context"
"model-gateway/model/dto"
"model-gateway/service/task"
"os"
"os/signal"
"syscall"
"time"
"model-gateway/controller"
"model-gateway/service"
"gitea.com/red-future/common/http"
"gitea.com/red-future/common/jaeger"
_ "gitea.com/red-future/common/swagger"
"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"
@@ -25,34 +26,32 @@ func main() {
// 注册路由
http.RouteRegister([]interface{}{
controller.Model,
controller.Task,
controller.Stat,
controller.ModelManage,
controller.ModelGatewayModels,
controller.ModelGatewayTask,
controller.ModelGatewayLogsStat,
})
// 本地调试:可选自动触发 worker/cleaner(由配置文件控制)
startAutoRunner(ctx)
// 监听退出信号,确保 Ctrl+C 能完整退出(停止 worker/cleaner 并关闭 http server
// 监听退出信号,确保 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()
// 关闭 http serverRouteRegister 内部是 go Httpserver.Run() 启动的)
// 先关闭 gateway server,等待 in-flight 请求处理完成
_ = http.Httpserver.Shutdown()
// 再取消上下文,避免活跃请求被中断
cancel()
}
func startAutoRunner(ctx context.Context) {
// worker
if g.Cfg().MustGet(ctx, "asynch.worker.enabled").Bool() {
interval := g.Cfg().MustGet(ctx, "asynch.worker.intervalSeconds").Int()
if interval <= 0 {
interval = 5
}
batchSize := g.Cfg().MustGet(ctx, "asynch.worker.batchSize").Int()
goroutines := g.Cfg().MustGet(ctx, "asynch.worker.goroutines").Int()
// 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()
@@ -61,31 +60,11 @@ func startAutoRunner(ctx context.Context) {
case <-ctx.Done():
return
case <-ticker.C:
if _, err := service.AsyncWorker.RunOnce(ctx, batchSize, goroutines); err != nil {
g.Log().Warningf(ctx, "[auto-worker] run once failed: %v", err)
if _, err := task.ModelGatewayTask.QueryPendingTasks(ctx, &dto.QueryPendingTasksReq{Limit: limit}); err != nil {
g.Log().Warningf(ctx, "[auto-queryPending] run once failed: %v", err)
}
}
}
}()
}
// cleaner
if g.Cfg().MustGet(ctx, "asynch.cleaner.enabled").Bool() {
interval := g.Cfg().MustGet(ctx, "asynch.cleaner.intervalSeconds").Int()
if interval <= 0 {
interval = 30
}
ticker := time.NewTicker(time.Duration(interval) * time.Second)
go func() {
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
service.Cleaner.RunOnce(ctx)
}
}
}()
}
}
-134
View File
@@ -1,134 +0,0 @@
package dto
import (
"gitea.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#modelName不能为空" dc:"模型名称(唯一标识)"`
ModelType int `p:"modelType" json:"modelType" v:"required#modelType不能为空" dc:"模型类型:1-文本生成 2-图像生成 3-语音 4-视频 5-多模态"`
BaseURL string `p:"baseUrl" json:"baseUrl" v:"required#baseUrl不能为空" dc:"模型服务基础地址(如 http(s)://host:port"`
HttpMethod string `p:"httpMethod" json:"httpMethod" dc:"请求方式:GET/POST(默认POST"`
HeadMsg string `p:"headMsg" json:"headMsg" dc:"请求头绑定(支持多个,逗号分隔),示例:Authorization:Bearer xxx,Content-Type:application/json"`
IsPrivate *int `p:"isPrivate" json:"isPrivate" v:"in:0,1#私有化参数只能为0或1" dc:"是否私有化:0-私有(默认) 1-公共"`
Enabled *int `p:"enabled" json:"enabled" v:"in:0,1#启用参数只能为0或1" dc:"是否启用:0-禁用,1-启用(默认1)"`
IsChatModel *int `p:"isChatModel" json:"isChatModel" v:"in:0,1#对话模型参数只能为0或1" dc:"是否为对话模型:0-否,1-是(默认0)"`
IsOwner *int `p:"isOwner" json:"isOwner" v:"in:0,1#是否为所有者参数只能为0或1" dc:"是否为所有者:0-否,1-是(默认0)"`
ApiKey string `p:"apiKey" json:"apiKey" dc:"调用凭证/密钥,用于模型认证"`
Form any `p:"form" json:"form" dc:"动态表单配置(JSON),用于前端渲染配置项"`
RequestMapping any `p:"requestMapping" json:"requestMapping" dc:"请求映射"`
ResponseMapping any `p:"responseMapping" json:"responseMapping" dc:"返回映射"`
ResponseBody any `p:"responseBody" json:"responseBody" dc:"返回主体"`
TokenMapping string `p:"tokenMapping" json:"tokenMapping" dc:"token映射"`
MaxConcurrency int `p:"maxConcurrency" json:"maxConcurrency" dc:"最大并发数(默认10"`
QueueLimit int `p:"queueLimit" json:"queueLimit" dc:"排队队列上限(默认1000"`
TimeoutSeconds int `p:"timeoutSeconds" json:"timeoutSeconds" dc:"请求超时时间(秒,默认600"`
ExpectedSeconds int `p:"expectedSeconds" json:"expectedSeconds" dc:"模型预计执行时间(秒,默认600)"`
RetryTimes int `p:"retryTimes" json:"retryTimes" dc:"失败重试次数(默认3"`
RetryQueueMaxSeconds int `p:"retryQueueMaxSeconds" json:"retryQueueMaxSeconds" dc:"失败重试最大排队时间(秒,默认600)"`
AutoCleanSeconds int `p:"autoCleanSeconds" json:"autoCleanSeconds" dc:"任务完成后自动清理时间(秒,默认86400)"`
Remark string `p:"remark" json:"remark" dc:"备注说明"`
}
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:"模型类型ID列表(逗号分隔)(可选更新)"`
BaseURL string `p:"baseUrl" json:"baseUrl" dc:"模型服务基础地址"`
HttpMethod string `p:"httpMethod" json:"httpMethod" dc:"请求方式:GET/POST(可选更新)"`
HeadMsg string `p:"headMsg" json:"headMsg" dc:"请求头绑定(可选更新)"`
ApiKey string `p:"apiKey" json:"apiKey" dc:"调用凭证/密钥,用于模型认证(可选更新)"`
Form any `p:"form" json:"form" dc:"动态表单配置(JSON)(可选更新)"`
RequestMapping any `p:"requestMapping" json:"requestMapping" dc:"请求参数映射(可选更新)"`
ResponseMapping any `p:"responseMapping" json:"responseMapping" dc:"返回参数映射(可选更新)"`
ResponseBody any `p:"responseBody" json:"responseBody" dc:"返回主体(可选更新)"`
TokenMapping string `p:"tokenMapping" json:"tokenMapping" dc:"token映射(可选更新)"`
Enabled *int `p:"enabled" json:"enabled" dc:"是否启用:0-禁用,1-启用(可选更新)"`
IsPrivate *int `p:"isPrivate" json:"isPrivate" v:"in:0,1#私有化参数只能为0或1" dc:"是否私有化:0-私有(默认) 1-公共"`
IsChatModel *int `p:"isChatModel" json:"isChatModel" v:"in:0,1#对话模型参数只能为0或1" dc:"是否为对话模型:0-否,1-是(默认0)"`
IsOwner *int `p:"isOwner" json:"isOwner" v:"in:0,1#是否为所有者参数只能为0或1" dc:"是否为所有者:0-否,1-是(默认0)"`
MaxConcurrency int `p:"maxConcurrency" json:"maxConcurrency" dc:"最大并发数(可选更新)"`
QueueLimit int `p:"queueLimit" json:"queueLimit" dc:"排队队列上限(可选更新)"`
TimeoutSeconds int `p:"timeoutSeconds" json:"timeoutSeconds" dc:"请求超时时间(秒)(可选更新)"`
ExpectedSeconds int `p:"expectedSeconds" json:"expectedSeconds" dc:"模型预计执行时间(秒)(可选更新)"`
RetryTimes int `p:"retryTimes" json:"retryTimes" dc:"失败重试次数(可选更新)"`
RetryQueueMaxSeconds int `p:"retryQueueMaxSeconds" json:"retryQueueMaxSeconds" dc:"失败重试最大排队时间(秒)(可选更新)"`
AutoCleanSeconds int `p:"autoCleanSeconds" json:"autoCleanSeconds" dc:"自动清理间隔(秒)(可选更新)"`
Remark string `p:"remark" json:"remark" dc:"备注说明(可选更新)"`
}
// DeleteModelReq 删除模型配置
type DeleteModelReq struct {
g.Meta `path:"/deleteModel" method:"delete" tags:"模型管理" summary:"删除模型配置" dc:"删除指定ID的模型配置"`
ID string `p:"id" json:"id,string" v:"required#id不能为空" dc:"配置ID"`
}
// GetModelReq 获取模型配置详情
type GetModelReq struct {
g.Meta `path:"/getModel" method:"get" tags:"模型管理" summary:"获取模型配置" dc:"根据模型ID获取配置详情"`
ID int64 `p:"id" json:"id,string" v:"required#id不能为空" dc:"配置ID"`
Creator string `p:"creator" json:"creator" dc:"创建人"`
}
type GetModelRes struct {
Model any `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 默认 36001小时)"`
}
type AutoTuneRes struct {
List any `json:"list" dc:"调参结果列表"`
}
type ModelTypeModelItem struct {
ID int64 `json:"id" dc:"模型主键ID"`
Name string `json:"name" dc:"模型名称"`
Form any `json:"form" dc:"动态表单配置(JSON数组),用于前端渲染"`
}
// ListModelTypeReq 模型类型列表(分页)
type ListTypeReq struct {
g.Meta `path:"/listType" method:"get" tags:"模型类型列表" summary:"模型类型列表" dc:"分页获取模型类型列表"`
}
type TypeItem struct {
Type map[int]string `json:"type" dc:"模型类型ID到名称的映射"`
}
type UpdateChatModelReq struct {
g.Meta `path:"/updateChatModel" method:"post" tags:"模型管理" summary:"更新聊天模型" dc:"更新指定模型的聊天模型"`
Id int64 `p:"id" json:"id" v:"required#model不能为空" dc:"模型id"`
}
type GetIsChatModelReq struct {
g.Meta `path:"/getIsChatModel" method:"get" tags:"模型管理" summary:"获取模型是否为聊天模型" dc:"根据模型ID获取是否为聊天模型"`
}
+20
View 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:"总数"`
}
+190
View File
@@ -0,0 +1,190 @@
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:"返回映射"`
ResponseBody string `p:"responseBody" json:"responseBody" dc:"返回主体"`
ResponseTokenField string `p:"responseTokenField" json:"responseTokenField" dc:"响应中消耗token的字段映射"`
OperatorName string `p:"operatorName" json:"operatorName" dc:"运营商名称"`
TokenConfig map[string]any `p:"tokenConfig" json:"tokenConfig" dc:"token计算配置"`
ExtendMapping map[string]any `p:"extendMapping" json:"extendMapping" dc:"附加映射"`
QueryConfig map[string]any `p:"queryConfig" json:"queryConfig" dc:"查询/回调配置"`
StreamConfig map[string]any `p:"streamConfig" json:"streamConfig" dc:"流式输出配置"`
FirstFrame string `p:"firstFrame" json:"firstFrame" dc:"首帧图片参数"`
LastFrame string `p:"lastFrame" json:"lastFrame" dc:"尾帧图片参数"`
MaxConcurrency int `p:"maxConcurrency" json:"maxConcurrency" dc:"最大并发数(默认10"`
TimeoutSeconds int `p:"timeoutSeconds" json:"timeoutSeconds" dc:"请求超时时间(秒,默认600"`
RetryTimes int `p:"retryTimes" json:"retryTimes" dc:"失败重试次数(默认3"`
AutoCleanSeconds int `p:"autoCleanSeconds" json:"autoCleanSeconds" dc:"任务完成后自动清理时间(秒,默认86400)"`
CallbackUrl string `p:"callbackUrl" json:"callbackUrl" dc:"回调地址"`
}
type CreateModelRes struct {
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:"返回映射"`
ResponseBody string `p:"responseBody" json:"responseBody" dc:"返回主体"`
ResponseTokenField string `p:"responseTokenField" json:"responseTokenField" dc:"响应中消耗token的字段映射"`
OperatorName string `p:"operatorName" json:"operatorName" dc:"运营商名称"`
TokenConfig map[string]any `p:"tokenConfig" json:"tokenConfig" dc:"token计算配置"`
ExtendMapping map[string]any `p:"extendMapping" json:"extendMapping" dc:"附加映射"`
QueryConfig map[string]any `p:"queryConfig" json:"queryConfig" dc:"查询/回调配置"`
StreamConfig map[string]any `p:"streamConfig" json:"streamConfig" dc:"流式输出配置"`
FirstFrame string `p:"firstFrame" json:"firstFrame" dc:"首帧图片参数"`
LastFrame string `p:"lastFrame" json:"lastFrame" dc:"尾帧图片参数"`
MaxConcurrency int `p:"maxConcurrency" json:"maxConcurrency" dc:"最大并发数"`
TimeoutSeconds int `p:"timeoutSeconds" json:"timeoutSeconds" dc:"请求超时时间(秒)"`
RetryTimes int `p:"retryTimes" json:"retryTimes" dc:"失败重试次数"`
AutoCleanSeconds int `p:"autoCleanSeconds" json:"autoCleanSeconds" dc:"任务完成后自动清理时间(秒)"`
CallbackUrl string `p:"callbackUrl" json:"callbackUrl" dc:"回调地址"`
}
type UpdateModelRes struct {
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 默认 36001小时)"`
}
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:"选项值"`
}
@@ -1,26 +1,62 @@
package dto
import "github.com/gogf/gf/v2/frame/g"
import (
"github.com/gogf/gf/v2/frame/g"
)
// CreateTaskReq 创建异步任务
type CreateTaskReq struct {
g.Meta `path:"/createTask" method:"post" tags:"任务管理" summary:"创建异步任务" dc:"创建异步任务并返回任务ID;创建成功后会立即异步尝试执行当前任务,执行成功后按回调配置触发钩子"`
ModelName string `p:"modelName" json:"modelName" v:"required#modelName不能为空" dc:"模型名称"`
BizName string `p:"bizName" json:"bizName" dc:"业务名称(调用方模块/系统,用于统计)"`
CallbackUrl string `p:"callbackUrl" json:"callbackUrl" dc:"回调地址(可选,用于后续业务通知)"`
InputRef string `p:"inputRef" json:"inputRef" dc:"输入引用(如OSS/文件引用等"`
RequestPayload any `p:"requestPayload" json:"requestPayload" dc:"请求负载(透传给模型服务)"`
EpicycleId int64 `json:"epicycleId" dc:"轮次ID"`
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-节点构建"`
BuildModelName string `json:"buildModelName" json:"buildModelName" dc:"构建模型名称"`
TaskId string `json:"taskId" dc:"任务ID"`
}
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#taskId不能为空" dc:"任务ID"`
TaskID string `p:"taskId" json:"taskId" v:"required#taskwId不能为空" dc:"任务ID"`
}
type GetTaskResultRes struct {
@@ -34,24 +70,26 @@ type GetTaskBatchReq struct {
TaskIDs []string `p:"taskIds" json:"taskIds" v:"required#taskIds不能为空" dc:"任务ID列表"`
}
type GetTaskBatchItem struct {
TaskID string `json:"taskId" dc:"任务ID"`
State int `json:"state" dc:"任务状态"`
OssFile string `json:"ossFile" dc:"结果文件OSS地址"`
}
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,可选)"`
State int `p:"state" json:"state" dc:"任务状态(0/1/2/3/4,可选)"`
}
type ListTaskRes struct {
@@ -69,12 +107,3 @@ type RunWorkReq struct {
type RunWorkRes struct {
Claimed int `json:"claimed" dc:"本次抢占并处理的任务数"`
}
// CleanWorkReq 手动触发 cleaner 执行一次(由上层定时任务调用)
type CleanWorkReq struct {
g.Meta `path:"/cleanWork" method:"post" tags:"任务管理" summary:"执行一次Cleaner" dc:"手动触发一次清理/重试(用于由上层定时任务控制)"`
}
type CleanWorkRes struct {
Ok bool `json:"ok" dc:"是否执行成功"`
}
+127
View File
@@ -0,0 +1,127 @@
package dto
import (
"model-gateway/consts/model"
"model-gateway/consts/public"
"model-gateway/model/entity"
"gitea.redpowerfuture.com/red-future/common/beans"
"github.com/gogf/gf/v2/frame/g"
)
// CreateModelManageReq 添加模型配置
type CreateModelManageReq struct {
g.Meta `path:"/createModelManage" method:"post" tags:"new模型管理" summary:"new创建模型配置" dc:"new添加新的模型配置"`
ModelSupplier model.SupplierType `json:"modelSupplier" v:"required#模型供应商不能为空" dc:"模型供应商"`
ModelName string `json:"modelName" v:"required#模型名称不能为空" dc:"模型名称"`
ModelType model.ModelType `json:"modelType" v:"required#模型类型不能为空" dc:"模型类型"`
BaseURL string `json:"baseUrl" v:"required#模型服务地址不能为空" dc:"模型服务地址"`
SystemModel *bool `json:"systemModel" dc:"系统模型"`
HttpMethod string `json:"httpMethod" dc:"请求方式:GET/POST" d:"POST"`
ChatModel *bool `json:"chatModel" dc:"对话模型"`
ResponseType model.ResponseType `json:"responseType" v:"required#调用模式不能为空" dc:"调用模式:0-同步 1-异步 2-流式"`
ApiKey string `json:"apiKey" dc:"调用凭证/密钥"`
Enabled *bool `json:"enabled" dc:"启用"`
RequestHeadMapping map[string]string `json:"requestHeadMapping" dc:"请求头映射"`
RequestBodyMapping map[string]any `json:"requestBodyMapping" dc:"请求体映射"`
ResponseMapping map[string]any `json:"responseMapping" dc:"返回映射"`
ResponseBodyMapping map[string]string `json:"responseBodyMapping" dc:"返回体映射"`
MaxConcurrency int `json:"maxConcurrency" dc:"最大并发数(默认10"`
TokenMapping *entity.TokenMapping `json:"tokenMapping" dc:"token映射"`
AsyncTaskMapping *entity.AsyncTaskMapping `json:"asyncTaskMapping" dc:"异步任务映射"`
TokenPredictPrice float64 `json:"tokenPredictPrice" dc:"模型Token预估价格"`
TokenPredictPriceUnit string `json:"tokenPredictPriceUnit" dc:"模型Token预估价格单位"`
MaxTokens int `json:"maxTokens" dc:"最大token数"`
MaxDuration int `json:"maxDuration" dc:"最大时长"`
LastFrame string `json:"lastFrame" dc:"视频的尾帧图像"`
}
type CreateModelManageRes struct {
Id int64 `json:"id,string" dc:"配置ID"`
}
type UpdateModelManageReq struct {
g.Meta `path:"/updateModelManage" method:"put" tags:"new模型管理" summary:"new更新模型配置" dc:"new更新指定ID的模型配置"`
Id int64 `json:"id" v:"required#id不能为空" dc:"配置ID"`
ModelSupplier model.SupplierType `json:"modelSupplier" dc:"模型供应商"`
ModelName string `json:"modelName" dc:"模型名称"`
ModelType model.ModelType `json:"modelType" dc:"模型类型"`
BaseURL string `json:"baseUrl" dc:"模型服务地址"`
SystemModel *bool `json:"systemModel" dc:"系统模型"`
HttpMethod string `json:"httpMethod" dc:"请求方式:GET/POST"`
ChatModel *bool `json:"chatModel" dc:"对话模型"`
ResponseType model.ResponseType `json:"responseType" dc:"调用模式:0-同步 1-异步 2-流式"`
ApiKey string `json:"apiKey" dc:"调用凭证/密钥"`
Enabled *bool `json:"enabled" dc:"启用"`
RequestHeadMapping map[string]string `json:"requestHeadMapping" dc:"请求头映射"`
RequestBodyMapping map[string]any `json:"requestBodyMapping" dc:"请求体映射"`
ResponseMapping map[string]any `json:"responseMapping" dc:"返回映射"`
ResponseBodyMapping map[string]string `json:"responseBodyMapping" dc:"返回主体映射"`
MaxConcurrency int `json:"maxConcurrency" dc:"最大并发数(默认10"`
TokenMapping *entity.TokenMapping `json:"tokenMapping" dc:"token映射"`
AsyncTaskMapping *entity.AsyncTaskMapping `json:"asyncTaskMapping" dc:"异步任务映射"`
TokenPredictPrice float64 `json:"tokenPredictPrice" dc:"模型Token预估价格"`
TokenPredictPriceUnit string `json:"tokenPredictPriceUnit" dc:"模型Token预估价格单位"`
MaxTokens int `json:"maxTokens" dc:"最大token数"`
MaxDuration int `json:"maxDuration" dc:"最大时长"`
LastFrame string `json:"lastFrame" dc:"视频的尾帧图像"`
}
type DeleteModelManageReq struct {
g.Meta `path:"/deleteModelManage" method:"delete" tags:"new模型管理" summary:"new删除模型配置" dc:"new删除指定ID的模型配置"`
Id int64 `p:"id" json:"id,string" v:"required#id不能为空" dc:"配置ID"`
}
type GetModelManage struct {
ChatModel *bool `json:"chatModel" dc:"对话模型"`
Creator string `json:"creator" dc:"创建人"`
ModelName string `json:"modelName" dc:"模型名称"`
}
type GetModelManageReq struct {
g.Meta `path:"/getModelManage" method:"get" tags:"new模型管理" summary:"new获取模型配置" dc:"new获取指定ID的模型配置"`
Id int64 `p:"id" json:"id,string" v:"required#id不能为空" dc:"配置ID"`
}
type GetModelManageRes struct {
*entity.ModelManage `json:"modelManage"`
}
// ListModelManageReq 配置列表
type ListModelManageReq struct {
g.Meta `path:"/listModelManage" method:"get" tags:"new模型管理" summary:"new模型配置列表" dc:"new分页获取模型配置列表"`
*beans.Page `json:"page"`
ModelName string `p:"modelName" json:"modelName" dc:"模型名称(模糊查询,可选)"`
ModelType int `p:"modelType" json:"modelType" dc:"模型类型"`
Creator string `json:"creator" dc:"创建人"`
}
type ListModelManageRes struct {
List []*entity.ModelManage `json:"list" dc:"列表数据"`
Total int `json:"total" dc:"总数"`
}
type CheckChatModelReq struct {
g.Meta `path:"/checkChatModel" method:"get" tags:"new模型管理" summary:"new检查是否为聊天模型" dc:"new检查是否为聊天模型"`
}
type CheckChatModelRes struct {
IsChatModel bool `json:"isChatModel" dc:"是否为聊天模型"`
}
// ModelTypeReq 模型类型列表(分页)
type ModelTypeReq struct {
g.Meta `path:"/modelType" method:"get" tags:"new模型管理" summary:"new模型类型列表" dc:"new分页获取模型类型列表"`
}
type ModelTypeRes struct {
List []*model.TypeTree `json:"list" dc:"模型类型ID到名称的映射"`
}
type ModelSupplierReq struct {
g.Meta `path:"/modelSupplier" method:"get" tags:"new模型管理" summary:"new获取运营商列表" dc:"new获取运营商列表"`
}
type ModelSupplierRes struct {
List []*public.Option `json:"list" dc:"运营商名称到ID的映射"`
}
-20
View File
@@ -1,20 +0,0 @@
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:"总数"`
}
-88
View File
@@ -1,88 +0,0 @@
package entity
import "gitea.com/red-future/common/beans"
type asynchModelCol struct {
beans.SQLBaseCol
ModelName string
ModelType string
BaseURL string
HttpMethod string
HeadMsg string
FormJSON string
RequestMapping string
ResponseMapping string
ResponseBody string
TokenMapping string
Prompt string
IsPrivate string
IsChatModel string
ApiKey string
Enabled string
MaxConcurrency string
QueueLimit string
TimeoutSeconds string
ExpectedSeconds string
RetryTimes string
RetryQueueMaxSecs string
AutoCleanSeconds string
Remark string
IsOwner string
}
var AsynchModelCol = asynchModelCol{
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",
ResponseBody: "response_body",
TokenMapping: "token_mapping",
Prompt: "prompt",
IsPrivate: "is_private",
IsChatModel: "is_chat_model",
ApiKey: "api_key",
Enabled: "enabled",
MaxConcurrency: "max_concurrency",
QueueLimit: "queue_limit",
TimeoutSeconds: "timeout_seconds",
ExpectedSeconds: "expected_seconds",
RetryTimes: "retry_times",
RetryQueueMaxSecs: "retry_queue_max_seconds",
AutoCleanSeconds: "auto_clean_seconds",
Remark: "remark",
IsOwner: "is_owner",
}
// AsynchModel 异步模型配置
type AsynchModel struct {
beans.SQLBaseDO `orm:",inline"`
ModelName string `orm:"model_name" json:"modelName"`
ModelType int `orm:"model_type" json:"modelType"`
BaseURL string `orm:"base_url" json:"baseUrl"`
HttpMethod string `orm:"http_method" json:"httpMethod"`
HeadMsg string `orm:"head_msg" json:"headMsg"`
Form any `orm:"form_json" json:"form"`
RequestMapping any `orm:"request_mapping" json:"requestMapping"`
ResponseMapping any `orm:"response_mapping" json:"responseMapping"`
ResponseBody any `orm:"response_body" json:"responseBody"`
TokenMapping string `orm:"token_mapping" json:"tokenMapping"`
Prompt string `orm:"prompt" json:"prompt"`
IsPrivate *int `orm:"is_private" json:"isPrivate"`
IsChatModel *int `orm:"is_chat_model" json:"isChatModel"`
ApiKey string `orm:"api_key" json:"apiKey"`
Enabled *int `orm:"enabled" json:"enabled"`
MaxConcurrency int `orm:"max_concurrency" json:"maxConcurrency"`
QueueLimit int `orm:"queue_limit" json:"queueLimit"`
TimeoutSeconds int `orm:"timeout_seconds" json:"timeoutSeconds"`
ExpectedSeconds int `orm:"expected_seconds" json:"expectedSeconds"`
RetryTimes int `orm:"retry_times" json:"retryTimes"`
RetryQueueMaxSeconds int `orm:"retry_queue_max_seconds" json:"retryQueueMaxSeconds"`
AutoCleanSeconds int `orm:"auto_clean_seconds" json:"autoCleanSeconds"`
Remark string `orm:"remark" json:"remark"`
IsOwner *int `json:"isOwner" orm:"is_owner"` // 1=当前用户创建的,0=超级管理员的
}
-26
View File
@@ -1,26 +0,0 @@
package entity
import "gitea.com/red-future/common/beans"
type asynchModelTypeCol struct {
beans.SQLBaseCol
TypeID string
TypeName string
Remark string
}
var AsynchModelTypeCol = asynchModelTypeCol{
SQLBaseCol: beans.DefSQLBaseCol,
TypeID: "type_id",
TypeName: "type_name",
Remark: "remark",
}
// AsynchModelType 模型类型(图片/音频/视频等)
type AsynchModelType struct {
beans.SQLBaseDO `orm:",inline"`
TypeID int `orm:"type_id" json:"typeId"`
TypeName string `orm:"type_name" json:"type"`
Remark string `orm:"remark" json:"remark"`
}
-89
View File
@@ -1,89 +0,0 @@
package entity
import (
"gitea.com/red-future/common/beans"
"github.com/gogf/gf/v2/os/gtime"
)
type asynchTaskCol struct {
beans.SQLBaseCol
ModelName string
TaskID string
BizName string
CallbackURL string
ModelKey string
State string
OssFile string
FileType string
FileSize string
ErrorMsg string
StartedAt string
FinishedAt string
DurationSeconds string
ExpireAt string
RetryCount string
EnqueueAt string
Phase string
TmpFile string
InputRef string
RequestPayload string
TextResult string
EpicycleId string
ExpendTokens string
}
var AsynchTaskCol = asynchTaskCol{
SQLBaseCol: beans.DefSQLBaseCol,
ModelName: "model_name",
TaskID: "task_id",
BizName: "biz_name",
CallbackURL: "callback_url",
ModelKey: "model_key",
State: "state",
OssFile: "oss_file",
FileType: "file_type",
FileSize: "file_size",
ErrorMsg: "error_msg",
StartedAt: "started_at",
FinishedAt: "finished_at",
DurationSeconds: "duration_seconds",
ExpireAt: "expire_at",
RetryCount: "retry_count",
EnqueueAt: "enqueue_at",
Phase: "phase",
TmpFile: "tmp_file",
InputRef: "input_ref",
RequestPayload: "request_payload",
TextResult: "text_result",
EpicycleId: "epicycle_id",
ExpendTokens: "expend_tokens",
}
// AsynchTask 异步任务
type AsynchTask struct {
beans.SQLBaseDO `orm:",inline"`
ModelName string `orm:"model_name" json:"modelName"`
TaskID string `orm:"task_id" json:"taskId"`
BizName string `orm:"biz_name" json:"bizName"`
CallbackURL string `orm:"callback_url" json:"callbackUrl"`
ModelKey string `orm:"model_key" json:"modelKey"`
State int `orm:"state" json:"state"` // 0排队中/1执行中/2成功/3失败/4已下载
OssFile string `orm:"oss_file" json:"ossFile"`
FileType string `orm:"file_type" json:"fileType"`
FileSize int64 `orm:"file_size" json:"fileSize"`
ErrorMsg string `orm:"error_msg" json:"errorMsg"`
StartedAt *gtime.Time `orm:"started_at" json:"startedAt"`
FinishedAt *gtime.Time `orm:"finished_at" json:"finishedAt"`
DurationSeconds int64 `orm:"duration_seconds" json:"durationSeconds"`
ExpireAt *gtime.Time `orm:"expire_at" json:"expireAt"` // 已下载(state=4)后的过期时间
RetryCount int `orm:"retry_count" json:"retryCount"`
EnqueueAt *gtime.Time `orm:"enqueue_at" json:"enqueueAt"`
Phase int `orm:"phase" json:"phase"` // 0模型阶段/1OSS阶段
TmpFile string `orm:"tmp_file" json:"tmpFile"` // 临时结果文件路径
InputRef string `orm:"input_ref" json:"inputRef"`
RequestPayload any `orm:"request_payload" json:"requestPayload"`
TextResult string `orm:"text_result" json:"text"`
EpicycleId int64 `orm:"epicycle_id" json:"epicycleId"` // 轮次ID(用于标识同一轮次的任务)
ExpendTokens int64 `orm:"expend_tokens" json:"expendTokens"` // 消耗 token 数
RetryQueueMaxSeconds int `orm:"retry_queue_max_seconds" json:"-"`
}
-57
View File
@@ -1,57 +0,0 @@
package entity
import (
"gitea.com/red-future/common/beans"
)
type LogsModelPpCol 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 LogsModelOpCol = LogsModelPpCol{
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",
}
// LogsModelOp 操作日志(创建任务等)
type LogsModelOp 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 any `orm:"request_payload" json:"requestPayload"`
ResponsePayload any `orm:"response_payload" json:"responsePayload"`
}
-38
View File
@@ -1,38 +0,0 @@
package entity
import (
"github.com/gogf/gf/v2/os/gtime"
)
// LogsModelStatCol 字段常量
type LogsModelStatCol struct {
Day string
TenantId string
Creator string
ModelName string
RequestCount string
CreatedAt string
UpdatedAt string
}
var LogsModelStatCols = LogsModelStatCol{
Day: "day",
TenantId: "tenant_id",
Creator: "creator",
ModelName: "model_name",
RequestCount: "request_count",
CreatedAt: "created_at",
UpdatedAt: "updated_at",
}
// LogsModelStat 按天统计:某天/租户/创建人/模型的请求次数
// 注:这里不走通用 SQLBaseDO,采用联合唯一键(day,tenant_id,creator,model_name)做 UPSERT 原子累加。
type LogsModelStat struct {
Day *gtime.Time `orm:"day" json:"day"` // 日期(建议仅使用日期部分)
TenantId int64 `orm:"tenant_id" json:"tenantId"` // 租户ID
Creator string `orm:"creator" json:"creator"` // 创建人/操作人
ModelName string `orm:"model_name" json:"modelName"` // 模型名称
RequestCount int64 `orm:"request_count" json:"requestCount"` // 请求次数
CreatedAt *gtime.Time `orm:"created_at" json:"createdAt"` // 创建时间
UpdatedAt *gtime.Time `orm:"updated_at" json:"updatedAt"` // 更新时间
}
+56
View 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
View 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"`
}
+101
View File
@@ -0,0 +1,101 @@
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
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
BillingConfig 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",
BillingConfig: "billing_config",
}
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"`
BillingConfig map[string]any `orm:"billing_config" json:"billingConfig"`
}
const (
ResponseBody = "content" //返回主体(必填)
TotalTokens = "total_tokens" //总token数
)
+82
View File
@@ -0,0 +1,82 @@
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
BuildModelName string
BillingData 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",
BuildModelName: "build_model_name",
BillingData: "billing_data",
}
// 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"`
BuildModelName string `orm:"build_model_name" json:"buildModelName"`
BillingData []map[string]any `orm:"billing_data" json:"billingData"`
}
// 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"`
}
+102
View File
@@ -0,0 +1,102 @@
package entity
import (
"model-gateway/consts/model"
"gitea.redpowerfuture.com/red-future/common/beans"
)
type modelManageCol struct {
beans.SQLBaseCol
ModelSupplier string
ModelName string
ModelType string
BaseURL string
SystemModel string
HttpMethod string
ChatModel string
ResponseType string
ApiKey string
Enabled string
RequestHeadMapping string
RequestBodyMapping string
ResponseMapping string
ResponseBodyMapping string
MaxConcurrency string
TokenPredictPrice string
TokenPredictPriceUnit string
MaxTokens string
MaxDuration string
LastFrame string
}
var ModelManageCol = modelManageCol{
SQLBaseCol: beans.DefSQLBaseCol,
ModelSupplier: "model_supplier",
ModelName: "model_name",
ModelType: "model_type",
BaseURL: "base_url",
SystemModel: "system_model",
HttpMethod: "http_method",
ChatModel: "chat_model",
ResponseType: "response_type",
ApiKey: "api_key",
Enabled: "enabled",
RequestHeadMapping: "request_head_mapping",
RequestBodyMapping: "request_body_mapping",
ResponseMapping: "response_mapping",
ResponseBodyMapping: "response_body_mapping",
MaxConcurrency: "max_concurrency",
TokenPredictPrice: "token_predict_price",
TokenPredictPriceUnit: "token_predict_price_unit",
MaxTokens: "max_tokens",
MaxDuration: "max_duration",
LastFrame: "last_frame",
}
type ModelManage struct {
beans.SQLBaseDO `orm:",inline"`
ModelSupplier model.SupplierType `orm:"model_supplier" json:"modelSupplier" description:"模型供应商"`
ModelName string `orm:"model_name" json:"modelName" description:"模型名称"`
ModelType model.ModelType `orm:"model_type" json:"modelType" description:"模型类型"`
BaseURL string `orm:"base_url" json:"baseUrl" description:"模型地址"`
SystemModel *bool `orm:"system_model" json:"systemModel" description:"系统模型"`
HttpMethod string `orm:"http_method" json:"httpMethod" description:"http方法"`
ChatModel *bool `orm:"chat_model" json:"ChatModel" description:"是否聊天模型"`
ResponseType model.ResponseType `orm:"response_type" json:"responseType" description:"返回类型:1同步,2异步,3流"`
ApiKey string `orm:"api_key" json:"apiKey" description:"api key"`
Enabled *bool `orm:"enabled" json:"enabled" description:"是否启用"`
RequestHeadMapping map[string]string `orm:"request_head_mapping" json:"requestHeadMapping" description:"请求头映射"`
RequestBodyMapping map[string]any `orm:"request_body_mapping" json:"requestBodyMapping" description:"请求体映射"`
ResponseMapping map[string]any `orm:"response_mapping" json:"responseMapping" description:"响应映射"`
ResponseBodyMapping map[string]string `orm:"response_body_mapping" json:"responseBodyMapping" description:"响应主体映射"`
MaxConcurrency int `orm:"max_concurrency" json:"maxConcurrency" description:"最大并发数"`
TokenMapping *TokenMapping `orm:"token_mapping" json:"tokenMapping" description:"token映射"`
AsyncTaskMapping *AsyncTaskMapping `orm:"async_task_mapping" json:"asyncTaskMapping" description:"异步任务映射"`
TokenPredictPrice float64 `orm:"token_predict_price" json:"tokenPredictPrice" description:"模型Token预估价格"`
TokenPredictPriceUnit string `orm:"token_predict_price_unit" json:"tokenPredictPriceUnit" description:"模型token预估价格单位(秒,百万Token,千Token,字数)"`
MaxTokens int `orm:"max_tokens" json:"maxTokens" description:"最大token数"`
MaxDuration int `orm:"max_duration" json:"maxDuration" description:"最大时长(秒)"`
LastFrame string `orm:"last_frame" json:"lastFrame" description:"视频的尾帧图像"`
}
type TokenMapping struct {
PromptTokens string `json:"promptTokens" dc:"输入token"`
CompletionTokens string `json:"completionTokens" dc:"输出token"`
TotalTokens string `json:"totalTokens" dc:"总token"`
}
type AsyncTaskMapping struct {
Url string `json:"url" dc:"url"`
HttpMethod string `json:"httpMethod" dc:"http方法" d:"POST"`
RequestHeadMapping map[string]string `json:"requestHeadMapping" description:"请求头映射"`
ResponseMapping map[string]any `json:"responseMapping" description:"响应映射"`
TaskId string `json:"taskId" dc:"任务id"`
TaskStatus string `json:"taskStatus" dc:"任务状态"`
TaskStatusPending string `json:"taskStatusPending" dc:"任务状态-待处理"`
TaskStatusRunning string `json:"taskStatusRunning" dc:"任务状态-运行中"`
TaskStatusSuccess string `json:"taskStatusSuccess" dc:"任务状态-成功"`
TaskStatusFailed string `json:"taskStatusFailed" dc:"任务状态-失败"`
TaskStatusCancel string `json:"taskStatusCancel" dc:"任务状态-取消"`
TaskStatusUnknown string `json:"taskStatusUnknown" dc:"任务状态-未知"`
}
-67
View File
@@ -1,67 +0,0 @@
package service
import (
"context"
"encoding/json"
"model-gateway/model/entity"
"gitea.com/red-future/common/http"
"github.com/gogf/gf/v2/frame/g"
)
// triggerCallback 任务成功后的回调:
// - JSON body 参数:task_id/state/oss_file/file_type/text(可选)
func triggerCallback(ctx context.Context, t *entity.AsynchTask) {
callbackURL := t.BizName + t.CallbackURL
headers := forwardHeaders(ctx)
var req struct{}
payload := map[string]interface{}{
"task_id": t.TaskID,
"state": t.State,
"oss_file": t.OssFile,
"file_type": t.FileType,
"text": t.TextResult,
"error_msg": t.ErrorMsg,
}
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, callbackURL, len(headers), len(jsonData))
err = http.Post(ctx, callbackURL, headers, &req, jsonData)
if err != nil {
g.Log().Warningf(ctx, "[回调] 发送失败 taskId=%s 回调地址=%s 错误=%v", t.TaskID, callbackURL, err)
return
}
g.Log().Infof(ctx, "[回调] 发送成功 taskId=%s 回调地址=%s 消息体大小=%d字节", t.TaskID, callbackURL, len(jsonData))
}
// triggerPromptsCallback 任务成功后的提示词回调
// - JSON body 参数:epicycleId(轮次id/textResult(模型回答消息)
func triggerPromptsCallback(ctx context.Context, t *entity.AsynchTask, epicycleId int64) {
callbackURL := "prompts-core/session/sessionCallback"
headers := forwardHeaders(ctx)
var req struct{}
payload := map[string]interface{}{
"epicycleId": epicycleId,
"text": t.TextResult,
}
jsonData, err := json.Marshal(payload)
if err != nil {
g.Log().Warningf(ctx, "[提示词回调] JSON序列化失败 epicycleId=%d 错误=%v", epicycleId, err)
return
}
g.Log().Infof(ctx, "[提示词回调] 开始发送 epicycleId=%d 回调地址=%s 请求头数量=%d 消息体大小=%d字节",
t.EpicycleId, callbackURL, len(headers), len(jsonData))
err = http.Post(ctx, callbackURL, headers, &req, jsonData)
if err != nil {
g.Log().Warningf(ctx, "[提示词回调] 发送失败 epicycleId=%d 回调地址=%s 错误=%v", t.EpicycleId, callbackURL, err)
return
}
g.Log().Infof(ctx, "[提示词回调] 发送成功 epicycleId=%d 回调地址=%s 消息体大小=%d字节", t.EpicycleId, callbackURL, len(jsonData))
}
-92
View File
@@ -1,92 +0,0 @@
package service
import (
"context"
"time"
"model-gateway/dao"
"github.com/gogf/gf/v2/frame/g"
)
var Cleaner = &cleaner{}
type cleaner struct{}
// RunOnce 由上层定时任务触发:执行一次清理/重试
func (c *cleaner) RunOnce(ctx context.Context) {
// 1) 清理已下载(state=4)且过期的任务(硬删除 + OSS)
expired, err := dao.Task.ListExpiredDownloadedGlobal(ctx, 200)
if err != nil {
g.Log().Errorf(ctx, "[cleaner] list expired(downloaded) error: %v", err)
} else {
for _, t := range expired {
deleteTmpResult(t.TmpFile)
_ = dao.Task.HardDeleteByIDGlobal(ctx, t.Id)
}
g.Log().Infof(ctx, "[cleaner] expired(downloaded) cleaned, count=%d", len(expired))
}
// 2) 超时任务标失败
list, err := dao.Task.ListTimeoutTasksGlobal(ctx, 200)
if err != nil {
g.Log().Errorf(ctx, "[cleaner] list timeout error: %v", err)
} else {
for _, t := range list {
_ = dao.Task.UpdateFailedGlobal(ctx, t.Id, "任务超时自动失败")
ReleaseQueueSlot(ctx, t.ModelName, t.TaskID)
}
g.Log().Infof(ctx, "[cleaner] timeout cleaned, count=%d", len(list))
}
// 3) 失败(state=3)的任务按模型配置 retry_times 重新入队(放到队尾)
retryable, err := dao.Task.ListFailedRetryableGlobal(ctx, 200)
if err != nil {
g.Log().Errorf(ctx, "[cleaner] list failed retryable error: %v", err)
} else {
for _, t := range retryable {
// 失败任务重新入队(state=3 -> 0)前,先严格占用 queue_limit slot;占用失败则留在失败态,下一轮再尝试
// 获取模型配置以得到 queue_limit / expected_seconds
m, err := dao.Model.GetByModelNameForTenant(ctx, t.TenantId, t.ModelName)
if err != nil || m == nil {
continue
}
limit := GetRuntimeQueueLimit(ctx, t.ModelName, m.QueueLimit)
if limit > 0 {
ok, _ := AcquireQueueSlot(ctx, t.ModelName, t.TaskID, limit, m.ExpectedSeconds)
if !ok {
continue
}
}
// retry_queue_max_seconds 控制失败重试的排队策略:
// - =0:失败重试插队到队首
// - >0:当任务从创建到现在的排队时长 >= maxSeconds,则插队到队首;否则仍放到队尾
now := time.Now()
enqueueAt := now
maxSeconds := t.RetryQueueMaxSeconds
if maxSeconds == 0 {
enqueueAt = now.Add(-100 * 365 * 24 * time.Hour)
} else if maxSeconds > 0 && t.CreatedAt != nil {
if now.Sub(t.CreatedAt.Time) >= time.Duration(maxSeconds)*time.Second {
enqueueAt = now.Add(-100 * 365 * 24 * time.Hour)
}
}
_ = dao.Task.RequeueForRetryGlobal(ctx, t.Id, enqueueAt)
}
g.Log().Infof(ctx, "[cleaner] failed retryable cleaned, count=%d", len(retryable))
}
// 4) 超过重试次数仍失败(state=3)的任务:硬删除
exhausted, err := dao.Task.ListFailedExhaustedGlobal(ctx, 200)
if err != nil {
g.Log().Errorf(ctx, "[cleaner] list failed exhausted error: %v", err)
} else {
for _, t := range exhausted {
deleteTmpResult(t.TmpFile)
// 重试耗尽硬删除:释放闸门占位(兜底,若此前已释放则幂等)
ReleaseQueueSlot(ctx, t.ModelName, t.TaskID)
_ = dao.Task.HardDeleteByIDGlobal(ctx, t.Id)
}
g.Log().Infof(ctx, "[cleaner] failed exhausted cleaned, count=%d", len(exhausted))
}
}
-47
View File
@@ -1,47 +0,0 @@
package service
import (
"net/http"
"strings"
)
// DetectFileType 根据返回的二进制内容推断 contentType + 扩展名(尽量稳定)
func DetectFileType(data []byte) (contentType string, ext string) {
if len(data) == 0 {
return "application/octet-stream", ""
}
ct := http.DetectContentType(data)
// http.DetectContentType 可能带 charset 等参数:text/plain; charset=utf-8
if idx := strings.Index(ct, ";"); idx > 0 {
ct = strings.TrimSpace(ct[:idx])
}
switch ct {
case "audio/mpeg":
return ct, ".mp3"
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, ""
}
}
+333
View File
@@ -0,0 +1,333 @@
package gateway
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"mime/multipart"
"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
headers := make(map[string]string)
headers["Content-Type"] = writer.FormDataContentType()
if r := g.RequestFromCtx(ctx); r != nil {
if auth := r.Header.Get("Authorization"); auth != "" {
headers["Authorization"] = auth
}
}
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"`
BillingDate []map[string]any `json:"billing_data"`
}
// TriggerCallback 任务的回调
func TriggerCallback(ctx context.Context, t *entity.ModelGatewayTask) {
//headers := util.ForwardHeaders(ctx)
headers := make(map[string]string)
if r := g.RequestFromCtx(ctx); r != nil {
for k, v := range r.Request.Header {
if len(v) > 0 {
headers[k] = v[0]
}
}
}
var resp struct{}
payload := CallbackPayload{
TaskId: t.TaskID,
State: t.State,
ErrorMsg: t.ErrorMsg,
BillingDate: t.BillingData,
}
if !g.IsEmpty(t.ResultFile) {
payload.OssFile = t.ResultFile.OssFile
payload.FileType = t.ResultFile.FileType
}
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)
headers := make(map[string]string)
if r := g.RequestFromCtx(ctx); r != nil {
for k, v := range r.Request.Header {
if len(v) > 0 {
headers[k] = v[0]
}
}
}
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)
headers := make(map[string]string)
if r := g.RequestFromCtx(ctx); r != nil {
for k, v := range r.Request.Header {
if len(v) > 0 {
headers[k] = v[0]
}
}
}
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
}
// VideoDurationResp 视频时长接口返回
type VideoDurationResp struct {
Videos []VideoInfo `json:"videos"`
Count int `json:"count"`
TotalDuration float64 `json:"totalDuration"`
TotalDurationStr string `json:"totalDurationStr"`
}
type VideoInfo struct {
Index int `json:"index"`
VideoUrl string `json:"videoUrl"`
Duration float64 `json:"duration"`
DurationStr string `json:"durationStr"`
}
// GetVideoDuration 获取视频时长
func GetVideoDuration(ctx context.Context, urls []string) (VideoDurationResp, error) {
apiURL := "media/video/duration"
headers := make(map[string]string)
if r := g.RequestFromCtx(ctx); r != nil {
for k, v := range r.Request.Header {
if len(v) > 0 {
headers[k] = v[0]
}
}
}
body := map[string]any{"video_urls": urls}
jsonData, _ := json.Marshal(body)
var resp VideoDurationResp
err := commonHttp.Post(ctx, apiURL, headers, &resp, jsonData)
if err != nil {
g.Log().Warningf(ctx, "[视频时长] 获取失败 err=%v", err)
return resp, err
}
g.Log().Infof(ctx, "[视频时长] 获取成功 count=%d totalDuration=%.2f", resp.Count, resp.TotalDuration)
return resp, nil
}
// DeductBalanceReq 扣减余额请求
type DeductBalanceReq struct {
Id uint64 `json:"id"`
Surplus float64 `json:"surplus"`
}
// DeductBalance 扣减租户余额
func DeductBalance(ctx context.Context, tenantId uint64, amount float64) error {
apiURL := "admin-go/api/v1/system/tenant/edit"
headers := make(map[string]string)
if r := g.RequestFromCtx(ctx); r != nil {
for k, v := range r.Request.Header {
if len(v) > 0 {
headers[k] = v[0]
}
}
}
body := DeductBalanceReq{
Id: tenantId,
Surplus: amount,
}
jsonData, _ := json.Marshal(body)
var resp struct{}
err := commonHttp.Put(ctx, apiURL, headers, &resp, jsonData)
if err != nil {
g.Log().Warningf(ctx, "[扣减余额] 失败 tenantId=%d amount=%.6f err=%v", tenantId, amount, err)
return err
}
g.Log().Infof(ctx, "[扣减余额] 成功 tenantId=%d amount=%.6f", tenantId, amount)
return nil
}
// TenantSurplusResp 租户余额返回
type TenantSurplusResp struct {
Tenant struct {
Surplus float64 `json:"surplus"`
} `json:"tenant"`
}
// GetTenantSurplus 获取租户余额
func GetTenantSurplus(ctx context.Context, tenantId uint64) (float64, error) {
apiURL := fmt.Sprintf("admin-go/api/v1/system/tenant/getTenantDetails?tenantId=%d", tenantId)
headers := make(map[string]string)
if r := g.RequestFromCtx(ctx); r != nil {
for k, v := range r.Request.Header {
if len(v) > 0 {
headers[k] = v[0]
}
}
}
var resp TenantSurplusResp
err := commonHttp.Get(ctx, apiURL, headers, &resp, nil)
if err != nil {
g.Log().Warningf(ctx, "[获取余额] 失败 tenantId=%d err=%v", tenantId, err)
return 0, err
}
return resp.Tenant.Surplus, nil
}
//// 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))
//}
-53
View File
@@ -1,53 +0,0 @@
package service
import (
"context"
"gitea.com/red-future/common/utils"
"github.com/gogf/gf/v2/frame/g"
)
// asyncCtx 固化异步执行所需的 token/user,避免请求结束后丢失(仅在“同请求内起 goroutine”有用)。
// 本项目当前是“落库 + 后台 worker”模式,因此还会把必要信息持久化到任务表的 request_payload 中。
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 里固化的 token / xUserInfo)。
func forwardHeaders(ctx context.Context) map[string]string {
headers := make(map[string]string)
if token, ok := ctx.Value("token").(string); ok && token != "" {
headers["Authorization"] = token
}
if x, ok := ctx.Value("xUserInfo").(string); ok && x != "" {
headers["X-User-Info"] = x
}
// 兜底:从请求头拿
if r := g.RequestFromCtx(ctx); r != nil {
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
}
}
}
return headers
}
+256
View 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 || model == 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
}
-417
View File
@@ -1,417 +0,0 @@
package service
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"model-gateway/model/entity"
"github.com/gogf/gf/v2/container/gvar"
"github.com/gogf/gf/v2/frame/g"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)
// parseHeadMsgHeaders 支持多个 header 绑定,逗号分隔:
// 示例:
// - X-API-Key:qwen3-tts-key,operation:true,count:123
// - X-API-Key:"qwen3-tts-key",operation:"true"
//
// 说明:
// - HTTP Header 最终都是字符串,这里做的是“值的字符串化表达”。
// - 若 value 用双引号包裹,会去掉外层引号再注入,便于在配置中区分字符串/布尔/数字等表达(以及避免值中包含特殊字符时歧义)。
func parseHeadMsgHeaders(headMsg string) map[string]string {
headMsg = strings.TrimSpace(headMsg)
if headMsg == "" {
return nil
}
out := map[string]string{}
parts := strings.Split(headMsg, ",")
for _, p := range parts {
p = strings.TrimSpace(p)
if p == "" {
continue
}
// HeaderName:HeaderValue(推荐) / HeaderName=HeaderValue(兼容)
if strings.Contains(p, ":") {
kv := strings.SplitN(p, ":", 2)
k := strings.TrimSpace(kv[0])
v := strings.TrimSpace(kv[1])
v = strings.Trim(v, "\"")
if k != "" && v != "" {
out[k] = v
}
continue
}
if strings.Contains(p, "=") {
kv := strings.SplitN(p, "=", 2)
k := strings.TrimSpace(kv[0])
v := strings.TrimSpace(kv[1])
v = strings.Trim(v, "\"")
if k != "" && v != "" {
out[k] = v
}
continue
}
}
if len(out) == 0 {
return nil
}
return out
}
func payloadToQuery(payload any) (url.Values, error) {
if payload == nil {
return url.Values{}, nil
}
// 统一转成 map[string]any
b, err := json.Marshal(payload)
if err != nil {
return nil, err
}
m := map[string]any{}
if err := json.Unmarshal(b, &m); err != nil {
return nil, err
}
q := url.Values{}
for k, v := range m {
if v == nil {
continue
}
// 复杂类型直接 json 字符串化
switch vv := v.(type) {
case string:
q.Set(k, vv)
case float64, bool, int, int64, uint64:
q.Set(k, fmt.Sprintf("%v", vv))
default:
bs, _ := json.Marshal(v)
q.Set(k, string(bs))
}
}
return q, nil
}
// InvokeModel 调用模型服务,返回二进制结果
// modelKey 用于覆盖/补充模型配置 head_msg(例如每次请求携带不同的 X-API-Key)。
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)
}
url := strings.TrimRight(m.BaseURL, "/")
timeout := time.Duration(m.TimeoutSeconds) * time.Second
if timeout <= 0 {
timeout = 60 * time.Second
}
client := &http.Client{Timeout: timeout}
method := strings.ToUpper(strings.TrimSpace(m.HttpMethod))
if method == "" {
method = http.MethodPost
}
var (
req *http.Request
)
switch method {
case http.MethodGet:
q, err := payloadToQuery(mappedPayload) // 使用映射后的payload
if err != nil {
return nil, err
}
if len(q) > 0 {
if strings.Contains(url, "?") {
url = url + "&" + q.Encode()
} else {
url = url + "?" + q.Encode()
}
}
req, err = http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
default:
bodyBytes, err := json.Marshal(mappedPayload) // 使用映射后的payload
if err != nil {
return nil, err
}
req, err = http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(bodyBytes))
}
if err != nil {
return nil, err
}
// 先注入模型配置 head_msg(静态头部,适合公共模型固定 API Key)
for hk, hv := range parseHeadMsgHeaders(m.HeadMsg) {
req.Header.Set(hk, hv)
}
// 最后注入动态 modelKey(允许覆盖/补充静态 head_msg),适合按请求动态传密钥。
for hk, hv := range parseHeadMsgHeaders(modelKey) {
req.Header.Set(hk, hv)
}
if method != http.MethodGet {
req.Header.Set("Content-Type", "application/json")
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
b, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
msg := string(b)
if len(msg) > 2000 {
msg = msg[:2000]
}
return nil, fmt.Errorf("模型服务返回非2xx: %d, body=%s", resp.StatusCode, msg)
}
// ============ 新增:响应参数映射 ============
mappedResponse, err := mapResponsePayload(m.ResponseMapping, b)
if err != nil {
// 响应映射失败不阻塞,返回原始数据
g.Log().Warningf(ctx, "响应参数映射失败: %v,返回原始数据", err)
return b, nil
}
// =========================================
return mappedResponse, nil
}
// ============================================
// 映射相关函数
// ============================================
// mapRequestPayload 将标准请求映射为模型特定格式
func mapRequestPayload(mappingAny any, payload any) (any, error) {
// 1. 解析请求映射配置(值是any类型,支持bool、number等)
mapping, err := parseRequestMapping(mappingAny)
if err != nil {
return nil, err
}
// 如果没有映射配置,直接返回原始payload
if len(mapping) == 0 {
return payload, nil
}
// 2. 将payload转为map
var payloadMap map[string]any
switch v := payload.(type) {
case map[string]any:
payloadMap = v
case []map[string]any:
// 如果传进来的是纯messages数组,包装成标准格式
payloadMap = map[string]any{
"messages": v,
}
default:
// 通过JSON转换
jsonBytes, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("序列化payload失败: %w", err)
}
if err := json.Unmarshal(jsonBytes, &payloadMap); err != nil {
return nil, fmt.Errorf("反序列化payload失败: %w", err)
}
}
// 3. 用数据库固定参数覆盖/补充
for key, value := range mapping {
if existingValue, exists := payloadMap[key]; !exists || isEmptyValue(existingValue) {
payloadMap[key] = value
}
}
return payloadMap, nil
}
// mapResponsePayload 将模型响应映射为标准格式
func mapResponsePayload(mappingAny any, responseBytes []byte) ([]byte, error) {
mapping, err := parseResponseMapping(mappingAny)
if err != nil {
return nil, err
}
if len(mapping) == 0 {
return responseBytes, nil
}
responseStr := string(responseBytes)
resultStr := `{}`
for standardField, modelPath := range mapping {
value := gjson.Get(responseStr, modelPath)
if !value.Exists() {
continue
}
resultStr, err = sjson.SetRaw(resultStr, standardField, value.Raw)
if err != nil {
return nil, fmt.Errorf("提取字段 %s <- %s 失败: %w", standardField, modelPath, err)
}
}
return []byte(resultStr), nil
}
func parseRequestMapping(mappingAny any) (map[string]any, error) {
if mappingAny == nil {
return nil, nil
}
result := make(map[string]any)
switch v := mappingAny.(type) {
case *gvar.Var:
if v == nil || v.IsNil() || v.IsEmpty() {
return nil, nil
}
// 尝试转成 map
if m := v.Map(); m != nil {
for k, val := range m {
result[k] = val
}
return result, nil
}
// 尝试转成 string
if s := v.String(); s != "" && s != "{}" && s != "null" {
if err := json.Unmarshal([]byte(s), &result); err != nil {
return nil, fmt.Errorf("解析请求映射字符串失败: %w", err)
}
return result, nil
}
return nil, nil
// =======================================================
case map[string]interface{}:
result = v
case string:
if v == "" || v == "{}" || v == "null" {
return nil, nil
}
if err := json.Unmarshal([]byte(v), &result); err != nil {
return nil, fmt.Errorf("解析请求映射字符串失败: %w", err)
}
case []byte:
if len(v) == 0 {
return nil, nil
}
if err := json.Unmarshal(v, &result); err != nil {
return nil, fmt.Errorf("解析请求映射字节失败: %w", err)
}
default:
jsonBytes, err := json.Marshal(mappingAny)
if err != nil {
return nil, fmt.Errorf("序列化映射配置失败: %w", err)
}
if err := json.Unmarshal(jsonBytes, &result); err != nil {
return nil, fmt.Errorf("解析映射配置失败: %w", err)
}
}
return result, nil
}
// parseResponseMapping 解析响应映射配置
// 返回值类型为 map[string]string,值都是JSON路径字符串
func parseResponseMapping(mappingAny any) (map[string]string, error) {
if mappingAny == nil {
return nil, nil
}
mapping := make(map[string]string)
switch v := mappingAny.(type) {
case *gvar.Var:
if v == nil || v.IsNil() || v.IsEmpty() {
return nil, nil
}
if m := v.Map(); m != nil {
for k, val := range m {
if strVal, ok := val.(string); ok {
mapping[k] = strVal
}
}
return mapping, nil
}
if s := v.String(); s != "" && s != "{}" && s != "null" {
if err := json.Unmarshal([]byte(s), &mapping); err != nil {
return nil, fmt.Errorf("解析响应映射字符串失败: %w", err)
}
return mapping, nil
}
return nil, nil
case string:
if v == "" || v == "{}" || v == "null" {
return nil, nil
}
if err := json.Unmarshal([]byte(v), &mapping); err != nil {
return nil, fmt.Errorf("解析响应映射字符串失败: %w", err)
}
case map[string]interface{}:
// 数据库JSONB直接返回的map
for k, val := range v {
if strVal, ok := val.(string); ok {
mapping[k] = strVal
}
}
case []byte:
if len(v) == 0 {
return nil, nil
}
if err := json.Unmarshal(v, &mapping); err != nil {
return nil, fmt.Errorf("解析响应映射字节失败: %w", err)
}
default:
jsonBytes, err := json.Marshal(mappingAny)
if err != nil {
return nil, fmt.Errorf("序列化响应映射配置失败: %w", err)
}
if err := json.Unmarshal(jsonBytes, &mapping); err != nil {
return nil, fmt.Errorf("解析响应映射配置失败: %w", err)
}
}
return mapping, nil
}
// isEmptyValue 判断值是否为空
func isEmptyValue(v any) bool {
if v == nil {
return true
}
switch val := v.(type) {
case string:
return val == ""
case []any:
return len(val) == 0
case map[string]any:
return len(val) == 0
default:
return false
}
}
+204
View File
@@ -0,0 +1,204 @@
package service
import (
"context"
"fmt"
"model-gateway/consts/model"
"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 ModelManage = &modelManageService{}
type modelManageService struct{}
// Create 创建模型
func (s *modelManageService) Create(ctx context.Context, req *dto.CreateModelManageReq) (res *dto.CreateModelManageRes, err error) {
err = gfdb.DB(ctx, public.DbNameModelGateway).Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
// 1)检查是否是超管
var isSuperAdmin bool
isSuperAdmin, err = gateway.IsSuperAdmin(ctx)
if err != nil {
return
}
req.SystemModel = &isSuperAdmin
// 1)如果设为会话模型,先把该用户旧会话模型取消
err = s.CancelChatModel(ctx, req.ModelType, req.ChatModel, isSuperAdmin)
if err != nil {
return
}
// 2)插入数据
id, err := dao.ModelManage.Insert(ctx, req)
if err != nil {
return
}
res = &dto.CreateModelManageRes{Id: id}
return
})
return
}
// Update 更新模型配置
func (s *modelManageService) Update(ctx context.Context, req *dto.UpdateModelManageReq) (err error) {
err = gfdb.DB(ctx, public.DbNameModelGateway).Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
var get *entity.ModelManage
get, err = dao.ModelManage.GetNotTenantId(ctx, &dto.GetModelManageReq{
Id: req.Id,
})
if err != nil {
return
}
var user *beans.User
user, err = utils.GetUserInfo(ctx)
if err != nil {
return
}
// 1)如果不是创建者,且是系统模型,则需要拷贝
if get.Creator != user.UserName {
if get.SystemModel != nil && *get.SystemModel {
if g.IsEmpty(req.ApiKey) {
return fmt.Errorf("模型apiKey不能为空")
}
d := new(dto.CreateModelManageReq)
err = gconv.Struct(req, d)
if err != nil {
return
}
_, err = s.Create(ctx, d)
if err != nil {
return err
}
return
}
return fmt.Errorf("无权限操作")
}
// 1)检查是否是超管
var isSuperAdmin bool
isSuperAdmin, err = gateway.IsSuperAdmin(ctx)
if err != nil {
return
}
// 1)如果设为会话模型,先把该用户旧会话模型取消
err = s.CancelChatModel(ctx, req.ModelType, req.ChatModel, isSuperAdmin)
if err != nil {
return
}
// 2)更新数据
_, err = dao.ModelManage.Update(ctx, req)
return
})
return
}
func (s *modelManageService) CancelChatModel(ctx context.Context, modelType model.ModelType, chatModel *bool, isSuperAdmin bool) (err error) {
if !g.IsEmpty(chatModel) && *chatModel {
if *modelType == *model.ModelTypeInference.Code {
if isSuperAdmin {
return fmt.Errorf("超级管理员不能设置会话模型")
}
// 2)获取该用户信息
var user *beans.User
user, err = utils.GetUserInfo(ctx)
if err != nil {
return
}
// 3)取消该用户之前的会话模型
var get *entity.ModelManage
get, err = dao.ModelManage.Get(ctx, &dto.GetModelManage{
Creator: user.UserName,
ChatModel: chatModel,
})
if err != nil {
return
}
_, err = dao.ModelManage.Update(ctx, &dto.UpdateModelManageReq{
Id: get.Id,
ChatModel: gconv.PtrBool(false),
})
if err != nil {
return
}
} else {
return fmt.Errorf("只有推理模型可以设置成会话模型")
}
}
return
}
// Delete 删除模型
func (s *modelManageService) Delete(ctx context.Context, req *dto.DeleteModelManageReq) error {
_, err := dao.ModelManage.Delete(ctx, req)
return err
}
func (s *modelManageService) Get(ctx context.Context, req *dto.GetModelManageReq) (res *dto.GetModelManageRes, err error) {
get, err := dao.ModelManage.GetNotTenantId(ctx, req)
if err != nil {
return
}
err = gconv.Struct(get, &res)
return
}
// List 获取模型列表
func (s *modelManageService) List(ctx context.Context, req *dto.ListModelManageReq) (res *dto.ListModelManageRes, err error) {
var user *beans.User
user, err = utils.GetUserInfo(ctx)
if err != nil {
return
}
req.Creator = user.UserName
list, total, err := dao.ModelManage.ListNotTenantId(ctx, req)
if err != nil {
return
}
res = &dto.ListModelManageRes{
Total: total,
}
err = gconv.Struct(list, &res.List)
return
}
func (s *modelManageService) CheckChatModel(ctx context.Context, req *dto.CheckChatModelReq) (res *dto.CheckChatModelRes, err error) {
user, err := utils.GetUserInfo(ctx)
if err != nil {
return
}
get, err := dao.ModelManage.Get(ctx, &dto.GetModelManage{
Creator: user.UserName,
ChatModel: gconv.PtrBool(true),
})
if err != nil {
return
}
res = &dto.CheckChatModelRes{
IsChatModel: !g.IsEmpty(get),
}
return
}
// GetModelType 获取模型类型
func (s *modelManageService) GetModelType(ctx context.Context, req *dto.ModelTypeReq) (res *dto.ModelTypeRes, err error) {
res = &dto.ModelTypeRes{
List: model.GetTypeTreeList(),
}
return res, nil
}
// GetModelSupplier 获取运营商列表
func (s *modelManageService) GetModelSupplier(ctx context.Context, req *dto.ModelSupplierReq) (res *dto.ModelSupplierRes, err error) {
return &dto.ModelSupplierRes{
List: model.GetSupplierOptionList(),
}, nil
}
-255
View File
@@ -1,255 +0,0 @@
package service
import (
"context"
"errors"
"model-gateway/dao"
"model-gateway/model/dto"
"model-gateway/model/entity"
"gitea.com/red-future/common/beans"
"gitea.com/red-future/common/db/gfdb"
"gitea.com/red-future/common/http"
"gitea.com/red-future/common/utils"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/util/gconv"
)
var Model = &modelService{}
type modelService struct{}
// IsSuperAdmin 调用admin-go服务检查是否是超级管理员
func (s *modelService) IsSuperAdmin(ctx context.Context) (res bool, err error) {
headers := forwardHeaders(ctx)
var r = make(map[string]bool)
if err = http.Get(ctx, "admin-go/api/v1/system/user/checkIsSuperAdmin", headers, &r); err != nil {
return false, err
}
return r["isSuperAdmin"], err
}
func (s *modelService) Create(ctx context.Context, req *dto.CreateModelReq) (res *dto.CreateModelRes, err error) {
// 获取当前会话模型
if !g.IsEmpty(req.IsChatModel) && *req.IsChatModel == 1 {
var model *entity.AsynchModel
model, err = dao.Model.GetByIsChatModel(ctx)
if err != nil {
return nil, err
}
// 如果有会话模型,那就改变为 0
if model != nil {
_, err = dao.Model.Update(ctx, &dto.UpdateModelReq{
ID: model.Id,
IsChatModel: gconv.PtrInt(0),
})
if err != nil {
return nil, err
}
}
}
req.IsOwner = gconv.PtrInt(1)
admin, err := s.IsSuperAdmin(ctx)
if err != nil {
return
}
if admin {
req.IsOwner = gconv.PtrInt(0)
}
id, err := dao.Model.Insert(ctx, req)
if err != nil {
return nil, err
}
return &dto.CreateModelRes{ID: id}, nil
}
func (s *modelService) Update(ctx context.Context, req *dto.UpdateModelReq) error {
//根据当前 isChatModel 来判断是否更新模型
if req.IsChatModel == gconv.PtrInt(1) {
//判断当前用户是否有会话模型
model, err := dao.Model.GetByIsChatModel(ctx)
if err != nil {
return err
}
if model != nil {
return errors.New("用户已存在会话模型,不能创建")
}
}
req.IsOwner = gconv.PtrInt(1)
admin, err := s.IsSuperAdmin(ctx)
if err != nil {
return err
}
if admin {
req.IsOwner = gconv.PtrInt(0)
_, err = dao.Model.Update(ctx, req)
if err != nil {
return err
}
return nil
}
var user *beans.User
user, err = utils.GetUserInfo(ctx)
if err != nil {
return err
}
// 判断当前传过来的模型id的模型是否是超级管理员的。如果是超管的进行创建,否则更新
var count int
count, err = dao.Model.Count(ctx, &dto.GetModelReq{
ID: req.ID,
Creator: user.UserName,
})
if err != nil {
return err
}
if count == 0 {
insertDto := new(dto.CreateModelReq)
err = gconv.Struct(req, insertDto)
if err != nil {
return err
}
_, err = dao.Model.Insert(ctx, insertDto)
return err
}
_, err = dao.Model.Update(ctx, req)
return err
}
func (s *modelService) Delete(ctx context.Context, id string) error {
_, err := dao.Model.DeleteByID(ctx, id)
return err
}
func (s *modelService) Get(ctx context.Context, id int64) (*entity.AsynchModel, error) {
model, err := dao.Model.Get(ctx, id)
if err != nil {
return nil, err
}
model.Form = ParseJSONField(model.Form)
model.RequestMapping = ParseJSONField(model.RequestMapping)
model.ResponseMapping = ParseJSONField(model.ResponseMapping)
model.ResponseBody = ParseJSONField(model.ResponseBody)
return model, nil
}
func (s *modelService) List(ctx context.Context, req *dto.ListModelReq) (list []*entity.AsynchModel, total int, err error) {
var models []*entity.AsynchModel
req.IsOwner = gconv.PtrInt(1)
admin, err := s.IsSuperAdmin(ctx)
if err != nil {
return
}
if admin {
req.IsOwner = gconv.PtrInt(0)
}
var user *beans.User
user, err = utils.GetUserInfo(ctx)
if err != nil {
return nil, 0, err
}
req.Creator = user.UserName
models, total, err = dao.Model.GetByCreatorAndPlatform(ctx, req)
if err != nil {
return
}
// 处理列表中每条记录的 JSONB 字段
for _, m := range models {
m.Form = ParseJSONField(m.Form)
m.RequestMapping = ParseJSONField(m.RequestMapping)
m.ResponseMapping = ParseJSONField(m.ResponseMapping)
m.ResponseBody = ParseJSONField(m.ResponseBody)
}
return models, total, nil
}
// GetModelTypesFromConfig 从配置文件读取模型类型
func GetModelTypesFromConfig(ctx context.Context) map[int]string {
typeMap := make(map[int]string)
// 读取配置
configMap := g.Cfg().MustGet(ctx, "modelType.types").Map()
for k, v := range configMap {
typeID := gconv.Int(k)
typeName := gconv.String(v)
if typeID > 0 && typeName != "" {
typeMap[typeID] = typeName
}
}
// 如果配置为空,使用默认值
if len(typeMap) == 0 {
typeMap = map[int]string{
1: "推理模型",
2: "图片模型",
3: "音频模型",
4: "向量化模型",
5: "全模态模型",
}
}
return typeMap
}
func (s *modelService) UpdateChatModel(ctx context.Context, req *dto.UpdateChatModelReq) error {
// 校验新会话模型是否存在
newModel, err := dao.Model.Get(ctx, req.Id)
if err != nil {
return err
}
if newModel == nil {
return errors.New("新会话模型不存在")
}
// 获取当前用户会话模型
currentModel, err := dao.Model.GetByIsChatModel(ctx)
if err != nil {
return err
}
err = gfdb.DB(ctx).Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
if !g.IsEmpty(currentModel) {
if currentModel.ModelType != 1 {
return errors.New("当前模型为非推理模型,不能设置为会话模型")
}
// 如果点击的就是当前会话模型(已经是1),取消它(设为0)
if currentModel.Id != req.Id {
_, err = dao.Model.Update(ctx, &dto.UpdateModelReq{
ID: currentModel.Id,
IsChatModel: gconv.PtrInt(0),
})
if err != nil {
return err
}
}
}
// 设置当前为会话模型(设为1
_, err = dao.Model.Update(ctx, &dto.UpdateModelReq{
ID: req.Id,
IsChatModel: gconv.PtrInt(1),
})
return err
})
return err
}
func (s *modelService) GetIsChatModel(ctx context.Context) (*entity.AsynchModel, error) {
model, err := dao.Model.GetByIsChatModel(ctx)
if err != nil {
return nil, err
}
if model == nil {
return nil, nil
}
model.Form = ParseJSONField(model.Form)
model.RequestMapping = ParseJSONField(model.RequestMapping)
model.ResponseMapping = ParseJSONField(model.ResponseMapping)
model.ResponseBody = ParseJSONField(model.ResponseBody)
return model, nil
}
-25
View File
@@ -1,25 +0,0 @@
package service
import "github.com/gogf/gf/v2/util/gconv"
// parseStoredPayload 解析入库的 request_payload,拆出模型调用 payload 与透传 headers
// 入库格式:{"payload": <any>, "headers": {"Authorization": "...", "X-User-Info":"..."}}
func parseStoredPayload(v any) (payload any, headers map[string]string) {
if v == nil {
return nil, nil
}
m := gconv.Map(v)
if len(m) == 0 {
return v, nil
}
if h, ok := m["headers"]; ok {
headers = gconv.MapStrStr(h)
}
if p, ok := m["payload"]; ok {
payload = p
} else {
payload = v
}
return
}
@@ -1,14 +1,16 @@
package service
package queue
import (
"context"
"errors"
"fmt"
"math"
"model-gateway/model/dto"
"model-gateway/consts/public"
"model-gateway/model/entity"
"gitea.com/red-future/common/db/gfdb"
"gitea.redpowerfuture.com/red-future/common/db/gfdb"
"github.com/gogf/gf/v2/frame/g"
)
@@ -26,7 +28,6 @@ type AutoTuneResult struct {
OldQueueLimit int `json:"oldQueueLimit"` // 调参前运行时值(Redis),若无则等于 cap
NewQueueLimit int `json:"newQueueLimit"` // 本次计算出的运行时值(将写入 Redis),受 ±50% 约束且不超过 cap
ExpectedSeconds int `json:"expectedSeconds"` // 模型预计执行时间(秒):asynch_models.expected_seconds(用于 queue_limit 计算绑定)
}
// AutoTune 由上层定时任务通过接口触发:
@@ -34,19 +35,22 @@ type AutoTuneResult struct {
// - 基于吞吐与 P90 执行耗时估算 max_concurrency 的运行时值(不超过 cap)
// - queue_limit 与 expected_seconds 绑定(允许排队时间 = expected_seconds * 2),生成运行时值(不超过 cap)
// - 单次调整幅度限制 ±50%,写入 Redis(带 TTL
func AutoTune(ctx context.Context, windowSeconds int) ([]AutoTuneResult, error) {
if windowSeconds <= 0 {
windowSeconds = 3600
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.AsynchModel
var modelRows []*entity.ModelGatewayModel
if err := gfdb.DB(ctx).Model(ctx, public.TableNameModel).
Where("deleted_at IS NULL").
Where(entity.AsynchModelCol.Enabled, 1).
Where(entity.ModelGatewayModelCol.Enabled, 1).
Scan(&modelRows); err != nil {
return nil, err
}
modelMap := make(map[string]*entity.AsynchModel)
modelMap := make(map[string]*entity.ModelGatewayModel)
for _, m := range modelRows {
if m == nil || m.ModelName == "" {
continue
@@ -60,15 +64,15 @@ func AutoTune(ctx context.Context, windowSeconds int) ([]AutoTuneResult, error)
if m.MaxConcurrency > cur.MaxConcurrency {
cur.MaxConcurrency = m.MaxConcurrency
}
if m.QueueLimit > cur.QueueLimit {
cur.QueueLimit = m.QueueLimit
if m.MaxConcurrency*2 > cur.MaxConcurrency*2 {
cur.MaxConcurrency = m.MaxConcurrency
}
if m.ExpectedSeconds > cur.ExpectedSeconds {
cur.ExpectedSeconds = m.ExpectedSeconds
if m.TimeoutSeconds > cur.TimeoutSeconds {
cur.TimeoutSeconds = m.TimeoutSeconds
}
}
if len(modelMap) == 0 {
return []AutoTuneResult{}, nil
return nil, errors.New("no models found")
}
// 2) 统计指定窗口:按 model_name 计算 cnt 和 P90 执行耗时
@@ -89,7 +93,7 @@ SELECT model_name,
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, windowSeconds)
r, err := gfdb.DB(ctx).GetAll(ctx, sql, req.WindowSeconds)
if err != nil {
return nil, err
}
@@ -108,7 +112,7 @@ SELECT model_name,
for modelName, m := range modelMap {
s := statMap[modelName]
capMax := m.MaxConcurrency
capQueue := m.QueueLimit
capQueue := m.MaxConcurrency * 2
oldMax := GetRuntimeMaxConcurrency(ctx, modelName, capMax)
oldQueue := GetRuntimeQueueLimit(ctx, modelName, capQueue)
@@ -124,7 +128,6 @@ SELECT model_name,
CapQueueLimit: capQueue,
OldQueueLimit: oldQueue,
NewQueueLimit: oldQueue,
ExpectedSeconds: m.ExpectedSeconds,
})
continue
}
@@ -150,7 +153,7 @@ SELECT model_name,
setRuntimeInt(ctx, runtimeMaxConcurrencyKey(modelName), newMax)
// queue_limitW_target = expected_seconds * queueFactor
exp := m.ExpectedSeconds
exp := m.TimeoutSeconds
if exp <= 0 {
exp = 60
}
@@ -185,10 +188,11 @@ SELECT model_name,
CapQueueLimit: capQueue,
OldQueueLimit: oldQueue,
NewQueueLimit: newQueue,
ExpectedSeconds: m.ExpectedSeconds,
})
}
g.Log().Infof(ctx, "[auto_tune] done models=%d windowSeconds=%d", len(out), windowSeconds)
return out, nil
g.Log().Infof(ctx, "[auto_tune] done models=%d windowSeconds=%d", len(out), req.WindowSeconds)
return &dto.AutoTuneRes{
List: out,
}, nil
}
@@ -1,4 +1,4 @@
package service
package queue
import (
"context"
@@ -1,4 +1,4 @@
package service
package queue
import (
"context"
@@ -11,9 +11,9 @@ import (
// 上层每小时调用 /model/autoTune 写入运行时值;Worker/CreateTask 读取运行时值生效。
const (
runtimeMaxCKeyPrefix = "asynch:runtime:max_concurrency:" // + model_name
runtimeQueueKeyPrefix = "asynch:runtime:queue_limit:" // + model_name
runtimeTTLSeconds = 2 * 3600 // 2小时,避免一次调参失败导致立即回退
runtimeMaxCKeyPrefix = "asynch:runtime:max_concurrency:" // + model_name
runtimeQueueKeyPrefix = "asynch:runtime:queue_limit:" // + model_name
runtimeTTLSeconds = 2 * 3600 // 2小时,避免一次调参失败导致立即回退
)
func runtimeMaxConcurrencyKey(modelName string) string {
@@ -80,4 +80,3 @@ func clampInt(v, minV, maxV int) int {
}
return v
}
@@ -1,4 +1,4 @@
package service
package queue
import (
"context"
@@ -34,7 +34,8 @@ end
return 1
`
func acquireSemaphore(ctx context.Context, key string, max int, ttlSeconds int64) (bool, error) {
// AcquireSemaphore 获取并发令牌
func AcquireSemaphore(ctx context.Context, key string, max int, ttlSeconds int64) (bool, error) {
if max <= 0 {
// 不限制
return true, nil
@@ -49,8 +50,8 @@ func acquireSemaphore(ctx context.Context, key string, max int, ttlSeconds int64
return gconv.Int(r) == 1, nil
}
func releaseSemaphore(ctx context.Context, key string) error {
// ReleaseSemaphore 释放并发令牌
func ReleaseSemaphore(ctx context.Context, key string) error {
_, err := g.Redis().Do(ctx, "EVAL", releaseLua, 1, key)
return err
}
+34
View 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
}
-39
View File
@@ -1,39 +0,0 @@
package service
import (
"context"
"model-gateway/dao"
"model-gateway/model/dto"
)
type statService struct{}
var Stat = &statService{}
func (s *statService) List(ctx context.Context, req *dto.ListModelStatReq) (res *dto.ListModelStatRes, err error) {
pageNum, pageSize := 1, 10
if req != nil {
if req.PageNum > 0 {
pageNum = req.PageNum
}
if req.PageSize > 0 {
pageSize = req.PageSize
}
}
startDay, endDay := "", ""
var tenantID *int64
creator, modelName := "", ""
if req != nil {
startDay = req.StartDay
endDay = req.EndDay
tenantID = req.TenantID
creator = req.Creator
modelName = req.ModelName
}
list, total, err := dao.Stat.List(ctx, pageNum, pageSize, startDay, endDay, tenantID, creator, modelName)
if err != nil {
return nil, err
}
return &dto.ListModelStatRes{List: list, Total: total}, nil
}
-18
View File
@@ -1,18 +0,0 @@
package service
import (
"context"
"errors"
"model-gateway/model/entity"
)
// StorageService 结果存储(OSS/MinIO)抽象
type StorageService interface {
UploadByTask(ctx context.Context, t *entity.AsynchTask, data []byte, fileExt string, contentType string) (ossURL string, err error)
}
// Storage 默认存储实现(优先对接你们的 oss 文件服务;必要时也可以切到 MinIO)
var Storage StorageService = &ossStorage{}
var ErrStorageNotConfigured = errors.New("存储未配置")
-81
View File
@@ -1,81 +0,0 @@
package service
import (
"bytes"
"context"
"fmt"
"mime/multipart"
"time"
"model-gateway/model/entity"
commonHttp "gitea.com/red-future/common/http"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/util/gconv"
"github.com/gogf/gf/v2/util/guid"
)
// 对接你们的 oss 文件服务:POST oss/file/uploadFile (multipart/form-data)
type ossStorage struct{}
type uploadFileResponse struct {
FileURL string `json:"fileURL"` // 文件 URL
FileSize int `json:"fileSize"` // 文件大小(字节)
FileName string `json:"fileName"` // 文件名
FileFormat string `json:"fileFormat"` // 文件格式
FileAddressPrefix string `json:"fileAddressPrefix"` // 文件地址前缀
}
func (s *ossStorage) UploadByTask(ctx context.Context, _ *entity.AsynchTask, data []byte, fileExt string, _ string) (ossURL string, 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 "", err
}
if _, err := part.Write(data); err != nil {
return "", err
}
contentType := writer.FormDataContentType()
if err := writer.Close(); err != nil {
return "", err
}
headers := 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 "", err
}
g.Log().Infof(ctx, "[OSS] upload success url=%s size=%d format=%s", resp.FileURL, resp.FileSize, resp.FileFormat)
return resp.FileURL, nil
}
// 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
}
+412
View File
@@ -0,0 +1,412 @@
package task
import (
"context"
"errors"
"fmt"
"model-gateway/common/util"
"model-gateway/consts/public"
"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/database/gredis"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/glog"
"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 := req.TaskId
if taskID == "" {
taskID = uuid.NewString()
}
startAt := time.Now()
// 1) 获取用户信息
userInfo, err := utils.GetUserInfo(ctx)
if err != nil {
return nil, err
}
// 2) 检查模型配置
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("模型不存在或未启用")
}
lockKey := fmt.Sprintf("lock:tenantId-%s:model-%s", gconv.String(userInfo.TenantId), req.ModelName)
success, e := Lock(ctx, lockKey, -1, int64(time.Minute.Seconds()*5), func(ctx context.Context) error {
const (
keyExpireSec = 600 // 计数Key兜底过期时间 10min
waitInterval = 10 * time.Second // 轮询等待间隔
)
// Redis 操作统一使用独立上下文,避免外部 ctx canceled
redisCtx := context.WithoutCancel(ctx)
// 模型并发计数Key
concurrencyKey := fmt.Sprintf("model:concurrency:%s", req.ModelName)
maxCon := gconv.Int64(model.MaxConcurrency)
// 循环尝试获取并发名额,超限则等待重试
var held bool // 标记当前是否持有未释放的计数
for {
// 检测全局上下文取消
if ctx.Err() != nil {
if held {
g.Redis().Decr(redisCtx, concurrencyKey)
}
return ctx.Err()
}
// 计数自增
currentCon, e := g.Redis().Incr(redisCtx, concurrencyKey)
if e != nil {
if held {
g.Redis().Decr(redisCtx, concurrencyKey)
}
glog.Errorf(ctx, "redis incr concurrency key err: %v", e)
return e
}
held = true
// 首次创建Key时设置过期时间(避免重复执行EXPIRE)
exists, errr := g.Redis().Exists(redisCtx, concurrencyKey)
if errr == nil && exists == 1 {
g.Redis().Expire(redisCtx, concurrencyKey, keyExpireSec)
}
// 未超限:跳出循环,执行业务
if currentCon <= maxCon {
glog.Infof(ctx, "并发数: %s %d/%d", concurrencyKey, currentCon, maxCon)
break
}
// 超限立刻回减,撤销本次计数
g.Redis().Decr(redisCtx, concurrencyKey)
held = false
glog.Infof(ctx, "并发超限等待: %s %d/%d", concurrencyKey, currentCon, maxCon)
time.Sleep(waitInterval)
}
// 3) 构建任务实体
task := &entity.ModelGatewayTask{
ModelName: model.ModelName,
TaskID: taskID,
State: public.TaskStatusRunning,
BizName: req.BizName,
CallbackURL: req.CallbackUrl,
RequestPayload: &entity.RequestPayload{
Body: req.RequestPayload,
Headers: util.ParseHeadMsgHeaders(model.HeadMsg),
},
EpicycleId: req.EpicycleId,
BuildModelName: req.BuildModelName,
}
// 4) 插入任务记录
id, errr := dao.ModelGatewayTask.Insert(ctx, task)
if errr != nil {
g.Redis().Decr(redisCtx, concurrencyKey)
// TODO: 恢复排队逻辑后,此处需要回滚排队占位
//queue.ReleaseQueueSlot(ctx, req.ModelName, taskID)
return errr
}
task.Id = id
// 5) 记录操作日志(非关键路径,失败不影响主流程)
ip, ua := "", ""
if r := g.RequestFromCtx(ctx); r != nil {
ip = utils.GetLocalIP()
ua = r.UserAgent()
}
_, _ = dao.ModelGatewayLogsOp.Insert(ctx, &entity.ModelGatewayLogsOp{
IP: ip,
UserAgent: ua,
APIPath: "/task/createTask",
HttpMethod: "POST",
BizName: req.BizName,
ModelName: req.ModelName,
TaskID: taskID,
OpType: "createTask",
Success: 1,
CostMs: time.Since(startAt).Milliseconds(),
RequestPayload: task.RequestPayload,
ResponsePayload: gdb.Map{"taskId": taskID},
})
// 6) 模型计费
if len(model.BillingConfig) > 0 {
requestData := util.ExtractRequestBilling(ctx, model.BillingConfig, req.RequestPayload)
// 请求数据作为计费记录的基础字段,先存入数组
task.BillingData = append(task.BillingData, requestData)
_, _ = dao.ModelGatewayTask.Update(ctx, &entity.ModelGatewayTask{
SQLBaseDO: beans.SQLBaseDO{Id: task.Id},
BillingData: task.BillingData,
})
}
// 7) 异步执行任务
go AsyncWorker.handleOne(util.AsyncCtx(ctx), task, model, req)
return nil
})
if e != nil {
err = e
return
}
if !success {
err = gerror.New("任务排队已满,请稍后再试")
return
}
return &dto.CreateTaskRes{TaskID: taskID}, nil
}
// Lock 分布式锁 纯原生命令、无Lua、隔离上下文防 context canceled
func Lock(ctx context.Context, key string, limit, expireSeconds int64, fn func(ctx context.Context) error) (success bool, err error) {
if limit <= 0 {
limit = -1
}
// 过期时间合法校验(单位:秒)
const maxExpireSec = 86400 * 7
if expireSeconds < 1 || expireSeconds > maxExpireSec {
glog.Warningf(ctx, "锁过期时间非法,原值:%d,兜底为60秒", expireSeconds)
expireSeconds = 60
}
lockVal := "1"
LOOP:
// 检测父级上下文取消,防止无限重试阻塞 goroutine
if ctx.Err() != nil {
return false, ctx.Err()
}
if limit != -1 {
if limit < 0 {
return false, errors.New("锁重试次数耗尽,获取锁失败")
}
limit--
}
// 核心:创建独立上下文,不受外部 ctx 取消影响
redisCtx := context.WithoutCancel(ctx)
// 加锁
val, err := g.Redis().Set(redisCtx, key, lockVal, gredis.SetOption{
TTLOption: gredis.TTLOption{
EX: &expireSeconds,
},
NX: true,
})
if err != nil {
glog.Errorf(ctx, "redis set lock failed: %v", err)
time.Sleep(time.Second)
goto LOOP
}
if val.Bool() {
// 执行业务逻辑(使用原上下文)
runErr := fn(ctx)
// 释放锁:同样使用独立上下文 + 先GET再DEL防误删
getRes, err := g.Redis().Get(redisCtx, key)
if err != nil {
glog.Errorf(ctx, "redis get lock value failed: %v", err)
} else if getRes.String() == lockVal {
_, delErr := g.Redis().Del(redisCtx, key)
if delErr != nil {
glog.Errorf(ctx, "redis del lock failed: %v", delErr)
}
}
return true, runErr
}
// 抢锁失败,休眠重试
time.Sleep(time.Second)
goto LOOP
}
// 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
}
// 每个任务使用独立的超时上下文,防止单个任务阻塞整个轮询
pullCtx, pullCancel := context.WithTimeout(ctx, 30*time.Second)
result, err := util.PullTaskResult(pullCtx, nil, model.QueryConfig, model.HeadMsg)
pullCancel()
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
}
+559
View File
@@ -0,0 +1,559 @@
package task
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"model-gateway/common/util"
"model-gateway/consts/public"
"model-gateway/dao"
"model-gateway/model/dto"
"model-gateway/model/entity"
"model-gateway/service/gateway"
"net/http"
"strings"
"sync"
"time"
"gitea.redpowerfuture.com/red-future/common/beans"
"gitea.redpowerfuture.com/red-future/common/utils"
"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()
rawData []byte
result map[string]any
err error
surplus float64
)
g.Log().Infof(ctx, "[handleOne] 开始 taskId=%s model=%s", task.TaskID, task.ModelName)
// ============================================
// 1) 查询余额
// ============================================
surplus, _ = gateway.GetTenantSurplus(ctx, model.TenantId)
if surplus <= 200 {
w.failTask(ctx, task, startTime, "租户余额不足")
return
}
g.Log().Infof(ctx, "[handleOne] 当前余额 tenantId=%d surplus=%.2f", model.TenantId, surplus)
// ============================================
// 2) 调用模型
// ============================================
for attempt := 0; ; attempt++ {
if attempt > 0 {
g.Log().Infof(ctx, "[handleOne] 调模型重试 第%d次 taskId=%s", attempt, task.TaskID)
time.Sleep(time.Duration(attempt) * 5 * time.Second)
}
rawData, err = InvokeModel(ctx, model, body)
switch {
case model.CallMode != nil && *model.CallMode == public.CallModeStream:
if err == nil {
result, err = util.ParseStreamResponse(rawData, model.StreamConfig)
}
case model.CallMode != nil && *model.CallMode == public.CallModeAsync:
if err == nil {
result = gjson.New(string(rawData)).Map()
result, err = util.PullTaskResult(ctx, result, model.QueryConfig, model.HeadMsg)
}
default:
if err == nil {
result = gjson.New(string(rawData)).Map()
}
}
if err == nil {
break
}
// 模型调用失败
if !strings.Contains(err.Error(), "Timeout") &&
!strings.Contains(err.Error(), "RequestCanceled") &&
!strings.Contains(err.Error(), "InternalServiceError") &&
!strings.Contains(err.Error(), "Invalid video_url") &&
!strings.Contains(err.Error(), "Invalid audio track") &&
!strings.Contains(err.Error(), "Error while downloading") &&
!strings.Contains(err.Error(), "Error while connecting") &&
!strings.Contains(err.Error(), "download failed") {
w.failTask(ctx, task, startTime, err.Error())
return
}
g.Log().Warningf(ctx, "[handleOne] 调模型失败 taskId=%s attempt=%d err=%v", task.TaskID, attempt, err)
}
// ============================================
// 3) 解析返回映射 + 存储 token 相关信息
// ============================================
mapped, err := util.MapResponsePayload(model.ResponseMapping, result)
if err != nil {
w.failTask(ctx, task, startTime, err.Error())
return
}
// 计费处理
if len(model.BillingConfig) > 0 && len(task.BillingData) > 0 {
// 取请求阶段数据作为基础
billingInput := make(map[string]any)
for k, v := range task.BillingData[0] {
billingInput[k] = v
}
// 补充返回数据
responseData := util.ExtractResponseBilling(model.BillingConfig, mapped)
for k, v := range responseData {
billingInput[k] = v
}
// 计算费用,替换数组第一个元素
billingResult := util.CalculateBilling(model.BillingConfig, billingInput)
if billingResult != nil {
task.BillingData[0] = billingResult
}
if billingResult != nil {
task.BillingData[0] = billingResult
totalFee := gconv.Float64(billingResult["total_fee"])
if totalFee > 0 {
for attempt := 0; attempt <= maxRetry; attempt++ {
err = gateway.DeductBalance(util.AsyncCtx(ctx), model.TenantId, -totalFee)
if err == nil {
break
}
g.Log().Warningf(ctx, "[handleOne] 扣除余额失败 taskId=%s attempt=%d/%d err=%v", task.TaskID, attempt, maxRetry, err)
}
}
}
}
task.ExpendTokens = gconv.Int64(mapped[entity.TotalTokens])
if _, err = dao.ModelGatewayTask.Update(ctx, task); err != nil {
g.Log().Errorf(ctx, "[handleOne] 更新DB失败 taskId=%s err=%v", task.TaskID, err)
return
}
// ============================================
// 4) 处理提示词相关数据解析涵盖重试
// ============================================
if req.BuildType == public.BuildTypePrompt {
mapped, err = w.parseAndRetry(ctx, mapped, model, task, maxRetry)
if err != nil {
task.TextResult = mapped
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, "[handleOne] OSS上传重试 第%d/%d次 taskId=%s", attempt, maxRetry, task.TaskID)
}
oss, err = gateway.UploadByTask(ctx, gjson.New(mapped).MustToJson(), "json")
if err == nil {
break
}
g.Log().Errorf(ctx, "[handleOne] OSS上传失败 taskId=%s attempt=%d/%d err=%v", task.TaskID, attempt, maxRetry, err)
if attempt == maxRetry {
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 = mapped
if _, err = dao.ModelGatewayTask.Update(ctx, task); err != nil {
g.Log().Errorf(ctx, "[handleOne] 更新DB失败 taskId=%s err=%v", task.TaskID, err)
return
}
concurrencyKey := fmt.Sprintf("model:concurrency:%s", req.ModelName)
g.Redis().Decr(ctx, concurrencyKey)
gateway.TriggerCallback(ctx, task)
if req.EpicycleId != 0 {
gateway.TriggerPromptsCallback(ctx, task, req.EpicycleId)
}
g.Log().Infof(ctx, "[handleOne] 成功 taskId=%s duration=%ds fileType=%s",
task.TaskID, task.DurationSeconds, oss.FileFormat)
}
// 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. 提交异步任务
rawData, err := InvokeModel(ctx, model, body)
if err != nil {
return nil, err
}
body = gjson.New(string(rawData)).Map()
// 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 调用模型 + 提取文本结果
//func (w *asyncWorker) callModel(ctx context.Context, task *entity.ModelGatewayTask, model *entity.ModelGatewayModel, body map[string]any) (map[string]any, error) {
// data, err := InvokeModel(ctx, model, body)
// if err != nil {
// return nil, err
// }
// contentType, _ := util.DetectFileType(data)
// var textResult string
// if utf8.Valid(data) && (strings.HasPrefix(contentType, "text/") || contentType == "application/json") {
// textResult = string(data)
// }
//
// if textResult == "" {
// return nil, fmt.Errorf("模型返回非文本内容,contentType=%s", contentType)
// }
//
// return gjson.New(textResult).Map(), nil
//}
// parseAndRetry 解析模型返回结果,并重试
func (w *asyncWorker) parseAndRetry(ctx context.Context, body map[string]any, model *entity.ModelGatewayModel, task *entity.ModelGatewayTask, maxRetry int) (map[string]any, error) {
// 获取构建模型的必填字段
user, err := utils.GetUserInfo(ctx)
if err != nil {
return nil, err
}
buildModel, err := dao.ModelGatewayModels.Get(ctx, &entity.ModelGatewayModel{
SQLBaseDO: beans.SQLBaseDO{TenantId: user.TenantId, Creator: user.UserName},
ModelName: task.BuildModelName,
})
if err != nil {
return nil, err
}
var lastErr error
for attempt := 0; attempt <= maxRetry; attempt++ {
if attempt > 0 {
g.Log().Infof(ctx, "[执行任务][重试] JSON解析 第%d/%d次 taskId=%s", attempt, maxRetry, task.TaskID)
}
// 解析 + 校验(用构建模型的 RequiredFields
parsed, err := util.ParseAndValidate(body, buildModel.RequiredFields)
if err == nil {
return parsed, nil
}
lastErr = err
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", lastErr)
}
// 重试:重新调模型
task.RetryCount++
_, _ = dao.ModelGatewayTask.Update(ctx, task)
reqBody := injectErrorMessage(task.RequestPayload.Body, lastErr)
rawData, callErr := InvokeModel(ctx, model, reqBody)
if callErr != nil {
g.Log().Warningf(ctx, "[执行任务][重调模型失败] taskId=%s attempt=%d/%d err=%v", task.TaskID, attempt, maxRetry, callErr)
continue
}
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
}
mapped, mapErr := util.MapResponsePayload(model.ResponseMapping, rawResp)
if mapErr != nil {
g.Log().Warningf(ctx, "[执行任务][映射失败] taskId=%s err=%v", task.TaskID, mapErr)
continue
}
// 计费
if len(model.BillingConfig) > 0 && len(task.BillingData) > 0 {
requestData := task.BillingData[0]
retryData := make(map[string]any)
for k, v := range requestData {
retryData[k] = v
}
responseData := util.ExtractResponseBilling(model.BillingConfig, mapped)
for k, v := range responseData {
retryData[k] = v
}
billingResult := util.CalculateBilling(model.BillingConfig, retryData)
if billingResult != nil {
task.BillingData = append(task.BillingData, billingResult)
totalFee := gconv.Float64(billingResult["total_fee"])
if totalFee > 0 {
for a := 0; a <= maxRetry; a++ {
errr := gateway.DeductBalance(util.AsyncCtx(ctx), task.TenantId, -totalFee)
if errr == nil {
break
}
g.Log().Warningf(ctx, "[handleOne] 扣除余额失败 taskId=%s attempt=%d/%d err=%v", task.TaskID, a, maxRetry, errr)
}
}
}
task.ExpendTokens += gconv.Int64(mapped[entity.TotalTokens])
_, _ = dao.ModelGatewayTask.Update(ctx, &entity.ModelGatewayTask{
SQLBaseDO: beans.SQLBaseDO{Id: task.Id},
BillingData: task.BillingData,
ExpendTokens: task.ExpendTokens,
})
}
body = mapped
}
return body, nil
}
// injectErrorMessage 将错误信息插入到最后一个 user 消息之前
func injectErrorMessage(payload map[string]any, err error) map[string]any {
if err == nil {
return payload
}
messages, _ := payload["messages"].([]any)
if len(messages) == 0 {
return payload
}
errMsg := fmt.Sprintf("【上一轮输出错误,请修正】%s", err.Error())
// 找到最后一个 user 的位置
lastUserIdx := -1
for i := len(messages) - 1; i >= 0; i-- {
msg, ok := messages[i].(map[string]any)
if !ok {
continue
}
if gconv.String(msg["role"]) == "user" {
lastUserIdx = i
break
}
}
if lastUserIdx == -1 {
return payload
}
// 在最后一个 user 之前插入错误消息
errMsgObj := map[string]any{
"role": "user",
"content": []map[string]any{{"type": "text", "text": errMsg}},
}
// 切片插入
messages = append(messages[:lastUserIdx], append([]any{errMsgObj}, messages[lastUserIdx:]...)...)
payload["messages"] = messages
return payload
}
// InvokeModel 调用模型服务,返回二进制结果
// modelKey 用于覆盖/补充模型配置 head_msg(例如每次请求携带不同的 X-API-Key)
func InvokeModel(ctx context.Context, model *entity.ModelGatewayModel, body map[string]any) ([]byte, error) {
//surplus, _ := gateway.GetTenantSurplus(ctx, model.TenantId)
//if surplus <= 0 {
// return nil, fmt.Errorf("租户余额不足")
//}
// 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()
}
}
// 改用独立超时ctx,隔绝外层截止
reqCtx, reqCancel := context.WithTimeout(context.Background(), timeout)
defer reqCancel()
req, err = http.NewRequestWithContext(reqCtx, http.MethodGet, baseURL, nil)
//req, err = http.NewRequestWithContext(ctx, http.MethodGet, baseURL, nil)
default:
bodyBytes, err := json.Marshal(body)
if err != nil {
return nil, err
}
reqCtx, reqCancel := context.WithTimeout(context.Background(), timeout)
defer reqCancel()
req, err = http.NewRequestWithContext(reqCtx, http.MethodPost, baseURL, bytes.NewReader(bodyBytes))
//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)
}
//
//
g.Log().Debugf(ctx, "[执行任务][模型调用成功] StatusCode=%v", resp.StatusCode)
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())
concurrencyKey := fmt.Sprintf("model:concurrency:%s", t.ModelName)
g.Redis().Decr(ctx, concurrencyKey)
_, _ = dao.ModelGatewayTask.Update(ctx, t) // 更新任务状态
go gateway.TriggerCallback(util.AsyncCtx(ctx), t) // 触发回调
}
-266
View File
@@ -1,266 +0,0 @@
package service
import (
"context"
"errors"
"fmt"
"time"
"model-gateway/dao"
"model-gateway/model/dto"
"model-gateway/model/entity"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
"github.com/google/uuid"
)
var Task = &taskService{}
type taskService struct{}
func (s *taskService) Create(ctx context.Context, req *dto.CreateTaskReq) (res *dto.CreateTaskRes, err error) {
fmt.Printf("打印请求:%+v", req)
startAt := time.Now()
// 固化 token/user 等信息
ctx = asyncCtx(ctx)
// 1) 检查模型配置
m, err := dao.Model.GetByModelName(ctx, req.ModelName)
if err != nil {
return nil, err
}
if m == nil || (m.Enabled != nil && *m.Enabled != 1) {
return nil, errors.New("模型不存在或未启用")
}
taskID := uuid.NewString()
// 2) 排队上限(严格控制:Redis 原子闸门)
limit := GetRuntimeQueueLimit(ctx, req.ModelName, m.QueueLimit)
if limit > 0 {
ok, err := AcquireQueueSlot(ctx, req.ModelName, taskID, limit, m.ExpectedSeconds)
if err != nil {
return nil, err
}
if !ok {
return nil, errors.New("任务排队已满,请稍后再试")
}
}
// 将调用模型的 payload 与透传头信息一起存入 request_payload,供后台 worker 使用
storedPayload := map[string]any{
"payload": req.RequestPayload,
"headers": forwardHeaders(ctx),
}
t := &entity.AsynchTask{
ModelName: req.ModelName,
TaskID: taskID,
State: 0,
BizName: req.BizName,
CallbackURL: req.CallbackUrl,
ModelKey: m.ApiKey,
InputRef: req.InputRef,
RequestPayload: storedPayload,
EpicycleId: req.EpicycleId,
}
_, err = dao.Task.Insert(ctx, t)
if err != nil {
// 入库失败:回滚闸门占位
ReleaseQueueSlot(ctx, req.ModelName, taskID)
return nil, err
}
// 3) 写操作日志(尽量不影响主流程,失败忽略)
ip := ""
ua := ""
apiPath := "/task/createTask"
httpMethod := "POST"
if r := g.RequestFromCtx(ctx); r != nil {
ip = r.GetClientIp()
ua = r.UserAgent()
apiPath = r.URL.Path
httpMethod = r.Method
}
_, _ = dao.OpLog.Insert(ctx, &entity.LogsModelOp{
IP: ip,
UserAgent: ua,
APIPath: apiPath,
HttpMethod: httpMethod,
BizName: req.BizName,
ModelName: req.ModelName,
TaskID: taskID,
OpType: "createTask",
Success: 1,
ErrorMsg: "",
CostMs: time.Since(startAt).Milliseconds(),
RequestPayload: storedPayload,
ResponsePayload: gdb.Map{
"taskId": taskID,
},
})
// 4) 创建成功后立即异步尝试执行当前任务,并仅在任务仍处于 pending(state=0) 时做定向轮询。
// 一旦任务进入 running/success/failed/downloaded,就停止轮询,避免一直空转。
go s.pollAndRunUntilPicked(context.WithoutCancel(ctx), taskID, req.EpicycleId)
return &dto.CreateTaskRes{TaskID: taskID}, nil
}
// pollAndRunUntilPicked 用于 createTask 创建后的“轻量级定向轮询”:
// - 目标:尽快把刚创建的任务拉起来执行
// - 只在任务仍为 pending(state=0) 时继续尝试抢占
// - 一旦任务进入 running(1) / success(2) / failed(3) / downloaded(4),立即停止
// - 这样不会无限轮询;runWork 仍负责处理积压队列和未处理到的任务
func (s *taskService) pollAndRunUntilPicked(ctx context.Context, taskID string, epicycleId int64) {
if taskID == "" {
return
}
interval := g.Cfg().MustGet(ctx, "asynch.worker.intervalSeconds").Int()
if interval <= 0 {
interval = 5
}
g.Log().Infof(ctx, "[task-auto-run][start] taskId=%s interval=%ds", taskID, interval)
ticker := time.NewTicker(time.Duration(interval) * time.Second)
defer ticker.Stop()
tryRun := func() bool {
t, err := dao.Task.GetByTaskID(ctx, taskID)
if err != nil {
g.Log().Warningf(ctx, "[task-auto-run][stop] taskId=%s reason=query_failed err=%v", taskID, err)
return true
}
if t == nil {
g.Log().Warningf(ctx, "[task-auto-run][stop] taskId=%s reason=task_not_found", taskID)
return true
}
switch t.State {
case 0:
if err := AsyncWorker.RunByTaskID(ctx, taskID, epicycleId); err != nil {
g.Log().Warningf(ctx, "[task-auto-run][retry] taskId=%s state=0 err=%v", taskID, err)
} else {
g.Log().Infof(ctx, "[task-auto-run][triggered] taskId=%s state=0", taskID)
}
return false
case 1:
g.Log().Infof(ctx, "[task-auto-run][stop] taskId=%s reason=running", taskID)
return true
case 2, 3, 4:
g.Log().Infof(ctx, "[task-auto-run][stop] taskId=%s reason=terminal state=%d", taskID, t.State)
return true
default:
g.Log().Infof(ctx, "[task-auto-run][stop] taskId=%s reason=unknown_state state=%d", taskID, t.State)
return true
}
}
// 先立即尝试一次
if stop := tryRun(); stop {
return
}
for {
select {
case <-ctx.Done():
g.Log().Infof(ctx, "[task-auto-run][stop] taskId=%s reason=context_done", taskID)
return
case <-ticker.C:
if stop := tryRun(); stop {
return
}
}
}
}
func (s *taskService) GetResult(ctx context.Context, taskID string) (res *dto.GetTaskResultRes, err error) {
t, err := dao.Task.GetByTaskID(ctx, taskID)
if err != nil {
return nil, err
}
if t == nil {
return nil, errors.New("任务不存在")
}
return &dto.GetTaskResultRes{
OssFile: t.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.Task.ListByTaskIDs(ctx, req.TaskIDs)
if err != nil {
return nil, err
}
// 2) 对成功(state=2)的任务:标记为已下载(state=4)并写入 expire_at
now := time.Now()
for _, t := range list {
if t == nil {
continue
}
if t.State != 2 {
continue
}
// 按模型配置决定保留时间
m, err := dao.Model.GetByModelName(ctx, t.ModelName)
if err != nil {
return nil, err
}
retainSeconds := 86400
if m != nil && m.AutoCleanSeconds > 0 {
retainSeconds = m.AutoCleanSeconds
}
expireAt := gtime.New(now.Add(time.Duration(retainSeconds) * time.Second))
_ = dao.Task.MarkDownloadedByID(ctx, t.Id, expireAt)
// 为了本次返回一致性,内存里也更新
t.State = 4
t.ExpireAt = expireAt
}
// 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.OssFile,
})
}
return &dto.GetTaskBatchRes{List: items}, nil
}
func (s *taskService) List(ctx context.Context, req *dto.ListTaskReq) (res *dto.ListTaskRes, err error) {
pageNum, pageSize := 1, 10
if req != nil {
if req.PageNum > 0 {
pageNum = req.PageNum
}
if req.PageSize > 0 {
pageSize = req.PageSize
}
}
modelName := ""
taskID := ""
var state *int
if req != nil {
modelName = req.ModelName
taskID = req.TaskID
state = req.State
}
list, total, err := dao.Task.List(ctx, pageNum, pageSize, modelName, taskID, state)
if err != nil {
return nil, err
}
return &dto.ListTaskRes{List: list, Total: total}, nil
}
-38
View File
@@ -1,38 +0,0 @@
package service
import (
"fmt"
"os"
"path/filepath"
)
// 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
}
func loadTmpResult(path string) ([]byte, error) {
return os.ReadFile(path)
}
func deleteTmpResult(path string) {
if path == "" {
return
}
_ = os.Remove(path)
}
-113
View File
@@ -1,113 +0,0 @@
package service
import (
"encoding/json"
"strings"
"github.com/gogf/gf/v2/container/gvar"
)
func normalizeFormValue(v any) any {
// 目标:对外永远返回 JSON 数组/对象,而不是字符串。
if v == nil {
return []any{}
}
switch t := v.(type) {
case string:
s := strings.TrimSpace(t)
if s == "" {
return []any{}
}
return normalizeFormValueFromJSONString(s)
case []byte:
if len(t) == 0 {
return []any{}
}
return normalizeFormValueFromJSONBytes(t)
case *gvar.Var:
// goframe 常见的 DB 返回类型
if t == nil {
return []any{}
}
b := t.Bytes()
if len(b) > 0 {
return normalizeFormValueFromJSONBytes(b)
}
s := strings.TrimSpace(t.String())
if s == "" {
return []any{}
}
return normalizeFormValueFromJSONString(s)
default:
// 尝试兼容其他“像 JSON 的值类型”(例如实现了 Bytes/String 的包装类型)
if vb, ok := v.(interface{ Bytes() []byte }); ok {
if b := vb.Bytes(); len(b) > 0 {
return normalizeFormValueFromJSONBytes(b)
}
}
if vs, ok := v.(interface{ String() string }); ok {
if s := strings.TrimSpace(vs.String()); s != "" {
return normalizeFormValueFromJSONString(s)
}
}
// 已经是 []any / map[string]any 等结构
return v
}
}
// 兼容“JSONB 里存了 JSON 字符串”的历史数据:
// 例如 form_json = '"[]"' 或 '"[{...}]"'(外层是字符串,内层才是数组/对象)
func normalizeFormValueFromJSONString(s string) any {
var out any
if err := json.Unmarshal([]byte(s), &out); err != nil || out == nil {
return []any{}
}
// 如果解出来还是 string,且看起来是 JSON,再解一层
if inner, ok := out.(string); ok {
inner = strings.TrimSpace(inner)
if inner == "" {
return []any{}
}
if strings.HasPrefix(inner, "[") || strings.HasPrefix(inner, "{") {
var out2 any
if err := json.Unmarshal([]byte(inner), &out2); err == nil && out2 != nil {
return out2
}
}
return []any{}
}
return out
}
func normalizeFormValueFromJSONBytes(b []byte) any {
var out any
if err := json.Unmarshal(b, &out); err != nil || out == nil {
return []any{}
}
// bytes 解出来也可能是 string(同上)
if inner, ok := out.(string); ok {
return normalizeFormValueFromJSONString(inner)
}
return out
}
func ParseJSONField(field any) any {
var v *gvar.Var
switch val := field.(type) {
case *gvar.Var:
v = val
default:
return field
}
if v == nil || v.IsNil() || v.IsEmpty() {
return nil
}
str := v.String()
var result any
if json.Unmarshal([]byte(str), &result) == nil {
return result
}
return str
}
-246
View File
@@ -1,246 +0,0 @@
package service
import (
"context"
"fmt"
"strings"
"time"
"unicode/utf8"
"model-gateway/dao"
"model-gateway/model/entity"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/grpool"
"github.com/tidwall/gjson"
)
var AsyncWorker = &asyncWorker{}
type asyncWorker struct {
}
// RunOnce 由上层定时任务触发:一次性抢占并处理一批任务
// - batchSize: 本次抢占数量
// - goroutines: 本次并发数(协程池大小)
func (w *asyncWorker) RunOnce(ctx context.Context, batchSize, goroutines int) (claimed int, err error) {
if batchSize <= 0 {
batchSize = 10
}
if goroutines <= 0 {
goroutines = 1
}
tasks, err := dao.Task.ClaimPendingGlobal(ctx, batchSize)
if err != nil {
return 0, err
}
if len(tasks) == 0 {
return 0, nil
}
pool := grpool.New(goroutines)
defer pool.Close()
claimed = len(tasks)
done := make(chan struct{}, claimed)
for _, t := range tasks {
task := t
_ = pool.AddWithRecover(ctx, func(ctx context.Context) {
w.handleOne(ctx, task, 0)
done <- struct{}{}
}, func(ctx context.Context, e error) {
if e != nil {
_ = dao.Task.UpdateFailedGlobal(ctx, task.Id, fmt.Sprintf("worker panic: %v", e))
ReleaseQueueSlot(ctx, task.ModelName, task.TaskID)
}
done <- struct{}{}
})
}
for i := 0; i < claimed; i++ {
<-done
}
return claimed, nil
}
// RunByTaskID 创建任务后立即异步尝试执行当前任务:
// - 只定向抢占当前 taskId 对应的 pending 任务
// - 若任务已被其它 worker 抢走/已不在 pending,则直接返回
func (w *asyncWorker) RunByTaskID(ctx context.Context, taskID string, epicycleId int64) error {
task, err := dao.Task.ClaimPendingByTaskIDGlobal(ctx, taskID)
if err != nil {
return err
}
if task == nil {
return nil
}
w.handleOne(ctx, task, epicycleId)
return nil
}
func (w *asyncWorker) handleOne(ctx context.Context, t *entity.AsynchTask, epicycleId int64) {
// 从任务入库的 request_payload 里恢复 payload + headers
payload, headers := parseStoredPayload(t.RequestPayload)
if len(headers) > 0 {
ctx = setTaskHeadersToCtx(ctx, headers)
}
// 1) 拉取模型配置
m, err := dao.Model.GetByModelNameForTenant(ctx, t.TenantId, t.ModelName)
if err != nil {
_ = dao.Task.UpdateFailedGlobal(ctx, t.Id, err.Error())
ReleaseQueueSlot(ctx, t.ModelName, t.TaskID)
// ============ 失败回调 ============
t.State = 3
t.ErrorMsg = err.Error()
go triggerCallback(context.WithoutCancel(ctx), t)
// ================================
return
}
if m == nil || (m.Enabled != nil && *m.Enabled != 1) {
errMsg := "模型不存在或未启用"
_ = dao.Task.UpdateFailedGlobal(ctx, t.Id, errMsg)
ReleaseQueueSlot(ctx, t.ModelName, t.TaskID)
// ============ 失败回调 ============
t.State = 3
t.ErrorMsg = errMsg
go triggerCallback(context.WithoutCancel(ctx), t)
// ================================
return
}
// 2) 分布式并发限制
semKey := fmt.Sprintf("asynch:sem:%s", t.ModelName)
leaseSeconds := int64(3600)
maxC := GetRuntimeMaxConcurrency(ctx, t.ModelName, m.MaxConcurrency)
acquired, err := acquireSemaphore(ctx, semKey, maxC, leaseSeconds)
if err != nil {
_ = dao.Task.UpdateFailedGlobal(ctx, t.Id, err.Error())
ReleaseQueueSlot(ctx, t.ModelName, t.TaskID)
// ============ 失败回调 ============
t.State = 3
t.ErrorMsg = err.Error()
go triggerCallback(context.WithoutCancel(ctx), t)
// ================================
return
}
if !acquired {
// 并发满了:放回排队,不回调(不是失败)
_ = w.rollbackToPending(ctx, t.Id)
return
}
defer func() {
_ = releaseSemaphore(ctx, semKey)
}()
// 3) 调用模型服务
if payload == nil {
payload = map[string]any{
"taskId": t.TaskID,
"inputRef": t.InputRef,
}
}
var (
data []byte
contentType string
ext string
textResult string
)
// phase=1 表示模型已成功但 OSS 上传失败:优先从临时文件加载
if t.Phase == 1 && strings.TrimSpace(t.TmpFile) != "" {
data, err = loadTmpResult(t.TmpFile)
if err == nil && len(data) > 0 {
contentType, ext = DetectFileType(data)
} else {
data = nil
}
}
if data == nil {
// 统计
_ = dao.Stat.IncRequestCount(ctx, time.Now(), int64(t.TenantId), t.Creator, t.ModelName)
// 核心调用
data, err = InvokeModel(ctx, m, payload, t.ModelKey)
if err != nil {
_ = dao.Task.UpdateFailedGlobal(ctx, t.Id, err.Error())
ReleaseQueueSlot(ctx, t.ModelName, t.TaskID)
// ============ 失败回调 ============
t.State = 3
t.ErrorMsg = err.Error()
go triggerCallback(context.WithoutCancel(ctx), t)
// ================================
return
}
contentType, ext = DetectFileType(data)
if utf8.Valid(data) && (strings.HasPrefix(contentType, "text/") || contentType == "application/json") {
textResult = string(data)
}
tmpPath, err := saveTmpResult(t.TaskID, data, ext)
if err == nil && tmpPath != "" {
t.TmpFile = tmpPath
t.Phase = 1
_ = dao.Task.UpdateTmpAfterModelGlobal(ctx, t.Id, tmpPath)
}
}
// 4) 存储 OSS
ossURL, err := Storage.UploadByTask(ctx, t, data, ext, contentType)
if err != nil {
// OSS 阶段失败:保留临时文件,下一轮仅重试 OSS
_ = dao.Task.UpdateFailedKeepTmpGlobal(ctx, t.Id, err.Error())
ReleaseQueueSlot(ctx, t.ModelName, t.TaskID)
// ============ OSS失败不回调(还会重试) ============
// 注意:OSS失败保留临时文件,下次重试,所以这里不触发最终回调
// 如果已经重试多次还没成功,需要在任务超时或超过最大重试次数时才回调失败
return
}
// 5) 更新任务状态成功
fileType := strings.TrimPrefix(ext, ".")
if fileType == "" {
fileType = contentType
}
if err := dao.Task.UpdateSuccessGlobal(
ctx,
t.Id,
ossURL,
fileType,
textResult,
int64(len(data)),
nil,
GetExpendTokens(m.TokenMapping, textResult),
); err != nil {
g.Log().Errorf(ctx, "[worker] update success failed: %v", err)
return
}
// 成功/失败均不再占用 queue_limit
ReleaseQueueSlot(ctx, t.ModelName, t.TaskID)
// 6) 成功回调
t.State = 2
t.OssFile = ossURL
t.FileType = fileType
t.TextResult = textResult
g.Log().Infof(ctx, "[CALLBACK][DISPATCH] taskId=%s bizName=%s callbackUrl=%s", t.TaskID, t.BizName, t.CallbackURL)
go triggerCallback(context.WithoutCancel(ctx), t)
// ============ 如果有 epicycleId,也触发业务回调 ============
if epicycleId != 0 {
go triggerPromptsCallback(context.WithoutCancel(ctx), t, epicycleId)
}
// 成功后清理临时文件
deleteTmpResult(t.TmpFile)
}
func (w *asyncWorker) rollbackToPending(ctx context.Context, id int64) error {
return dao.Task.RollbackToPendingGlobal(ctx, id)
}
// GetExpendTokens 根据映射路径从 textResult 中提取消耗 token 值
func GetExpendTokens(tokenMapping string, textResult string) int {
value := gjson.Get(textResult, tokenMapping)
if value.Exists() {
return int(value.Int())
} else {
return len(textResult)
}
}
BIN
View File
Binary file not shown.
Binary file not shown.
-1
View File
@@ -1 +0,0 @@
Asia/Shanghai
+326 -239
View File
@@ -1,264 +1,351 @@
-- model-asynch 核心表(pgsql)
-- 1) asynch_models:模型配置
-- 2) asynch_task:异步任务
-- 3) logs_model_op:操作日志(统计用)
-- 4) logs_model_stat:按天模型请求统计(限流/监控用)
-- =========================
-- 1) asynch_models
-- model_gateway_models
-- =========================
CREATE TABLE IF NOT EXISTS asynch_models (
-- 基础字段
id BIGINT PRIMARY KEY, -- 主键ID(非自增)
tenant_id BIGINT NOT NULL DEFAULT 0, -- 租户ID
creator VARCHAR(64) NOT NULL, -- 创建人
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间
updater VARCHAR(64) NOT NULL, -- 更新人
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 更新时间
deleted_at TIMESTAMP(6), -- 删除时间(软删)
-- 业务字段
model_name VARCHAR(128) NOT NULL, -- 模型名称
model_type SMALLINT NOT NULL DEFAULT 0, -- 模型类型
base_url VARCHAR(256) NOT NULL, -- 模型地址
http_method VARCHAR(8) NOT NULL DEFAULT 'POST', -- 请求方式 GET/POST
head_msg VARCHAR(1024) DEFAULT '', -- 请求头绑定(支持多个,逗号分隔)示例 X-API:xxx,operation:true
is_private SMALLINT NOT NULL DEFAULT 0, -- 是否私有化 0-私有 1-公共
enabled SMALLINT NOT NULL DEFAULT 1, -- 是否启用 0停用 1-启用
is_chat_model SMALLINT NOT NULL DEFAULT 0, -- 是否为对话模型 0-否 1-是
is_owner SMALLINT NOT NULL DEFAULT 99, -- 1=当前用户创建的,0=超级管理员的
api_key VARCHAR(256) NOT NULL DEFAULT '', -- 调用凭证,密钥
prompt TEXT NOT NULL DEFAULT '', -- 提示词内容(文本)
form_json JSONB NOT NULL DEFAULT '{}'::jsonb, -- 表单结构(用于前端渲染)
request_mapping JSONB NOT NULL DEFAULT '{}'::jsonb -- 请求映射
response_mapping JSONB NOT NULL DEFAULT '{}'::jsonb, -- 返回映射
response_body JSONB NOT NULL DEFAULT '{}'::jsonb, -- 返回主体
max_concurrency INT NOT NULL DEFAULT 10, -- 单模型最大并发
queue_limit INT NOT NULL DEFAULT 1000, -- 排队上限(近似控制)
timeout_seconds INT NOT NULL DEFAULT 600, -- 调用模型服务超时(秒)
expected_seconds INT NOT NULL DEFAULT 600, -- 模型预计执行时间(秒)
retry_times SMALLINT NOT NULL DEFAULT 3, -- 失败重试次数
retry_queue_max_seconds INT NOT NULL DEFAULT 600, -- 失败重试最大排队时间(秒 0=插队到队首;>0=排队超过该时间后插队,否则仍到队尾)
auto_clean_seconds INT NOT NULL DEFAULT 86400, -- 已下载(state=4 后的保留时间(秒),到期清理)
remark TEXT DEFAULT '' -- 备注
token_mapping VARCHAR(128) NOT NULL DEFAULT ''; -- token 映射
);
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,
auto_clean_seconds int4 NOT NULL DEFAULT 86400,
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_asynch_models_tenant_creator_chat ON asynch_models(tenant_id, creator) WHERE is_chat_model = 1 AND deleted_at IS NULL;
CREATE UNIQUE INDEX IF NOT EXISTS uk_asynch_models_tenant_model_name ON asynch_models(tenant_id, creator, model_name);
CREATE INDEX IF NOT EXISTS idx_asynch_models_tenant_id ON asynch_models(tenant_id);
CREATE INDEX IF NOT EXISTS idx_asynch_models_model_name ON asynch_models(model_name);
CREATE INDEX IF NOT EXISTS idx_asynch_models_model_type ON asynch_models(model_type);
CREATE INDEX IF NOT EXISTS idx_asynch_models_enabled ON asynch_models(enabled);
CREATE INDEX IF NOT EXISTS idx_asynch_models_deleted_at ON asynch_models(deleted_at);
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 asynch_models IS '模型配置表';
COMMENT ON COLUMN asynch_models.id IS '主键ID(非自增)';
COMMENT ON COLUMN asynch_models.tenant_id IS '租户ID';
COMMENT ON COLUMN asynch_models.creator IS '创建人';
COMMENT ON COLUMN asynch_models.created_at IS '创建时间';
COMMENT ON COLUMN asynch_models.updater IS '更新人';
COMMENT ON COLUMN asynch_models.updated_at IS '更新时间';
COMMENT ON COLUMN asynch_models.deleted_at IS '删除时间(软删)';
COMMENT ON 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 asynch_models.model_name IS '模型名称';
COMMENT ON COLUMN asynch_models.model_type IS '模型类型';
COMMENT ON COLUMN asynch_models.base_url IS '模型地址';
COMMENT ON COLUMN asynch_models.http_method IS '请求方式 GET/POST';
COMMENT ON COLUMN asynch_models.head_msg IS '请求头绑定(支持多个,逗号分隔)示例 X-API:xxx,operation:true';
COMMENT ON COLUMN asynch_models.is_private IS '是否私有化 0-私有 1-公共';
COMMENT ON COLUMN asynch_models.enabled IS '是否启用 0停用 1-启用';
COMMENT ON COLUMN asynch_models.is_chat_model IS '是否为对话模型 0-否 1-是';
COMMENT ON COLUMN asynch_models.is_owner IS '1=当前用户创建的,0=超级管理员的';
COMMENT ON COLUMN asynch_models.api_key IS '调用凭证,密钥';
COMMENT ON COLUMN asynch_models.prompt IS '提示词内容(文本)';
COMMENT ON COLUMN asynch_models.form_json IS '表单结构(用于前端渲染,也用于后端校验)';
COMMENT ON COLUMN asynch_models.request_mapping IS '请求映射';
COMMENT ON COLUMN asynch_models.response_mapping IS '返回映射';
COMMENT ON COLUMN asynch_models.response_body IS '返回主体';
COMMENT ON COLUMN asynch_models.max_concurrency IS '单模型最大并发';
COMMENT ON COLUMN asynch_models.queue_limit IS '排队上限(近似控制)';
COMMENT ON COLUMN asynch_models.timeout_seconds IS '调用模型服务超时(秒)';
COMMENT ON COLUMN asynch_models.expected_seconds IS '模型预计执行时间(秒)';
COMMENT ON COLUMN asynch_models.retry_times IS '失败重试次数';
COMMENT ON COLUMN asynch_models.retry_queue_max_seconds IS '失败重试最大排队时间(秒 0=插队到队首;>0=排队超过该时间后插队,否则仍到队尾)';
COMMENT ON COLUMN asynch_models.auto_clean_seconds IS '已下载(state=4 后的保留时间(秒),到期清理)';
COMMENT ON COLUMN asynch_models.remark IS '备注';
COMMENT ON COLUMN asynch_models.token_mapping IS 'token映射';
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.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.auto_clean_seconds IS '任务完成后自动清理时间(秒)';
COMMENT ON COLUMN model_gateway_models.response_token_field IS '响应中消耗token的字段映射';
COMMENT ON COLUMN model_gateway_models.call_mode IS '调用模式:0-同步 1-异步 2-流式';
COMMENT ON COLUMN model_gateway_models.required_fields IS '必选字段列表';
COMMENT ON COLUMN model_gateway_models.max_tokens IS '最大 token 数,0 表示不传';
-- =========================
-- 2) asynch_task
-- model_gateway_task
-- =========================
CREATE TABLE IF NOT EXISTS asynch_task (
-- 基础字段
id BIGINT PRIMARY KEY, -- 主键ID(非自增)
tenant_id BIGINT NOT NULL DEFAULT 0, -- 租户ID
creator VARCHAR(64) NOT NULL, -- 创建人
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间
updater VARCHAR(64) NOT NULL, -- 更新人
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 更新时间
deleted_at TIMESTAMP(6), -- 删除时间(软删)
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 ''
);
-- 业务字段
model_name VARCHAR(128) NOT NULL, -- 模型名称
task_id VARCHAR(64) NOT NULL, -- 任务ID(对外返回)
biz_name VARCHAR(128) NOT NULL DEFAULT '', -- 业务名称(调用方模块/系统)
callback_url VARCHAR(512) DEFAULT '', -- 回调地址(可选,用于后续业务通知)
model_key VARCHAR(1024) DEFAULT '', -- 动态请求头(用于覆盖/补充模型配置 head_msg),如 X-API-Key:xxx
state SMALLINT NOT NULL DEFAULT 0, -- 0排队中/1执行中/2成功/3失败/4已下载
oss_file VARCHAR(512) DEFAULT '', -- 结果文件OSS地址
file_type VARCHAR(32) DEFAULT '', -- 文件类型(mp3/mp4/png/...)
file_size BIGINT NOT NULL DEFAULT 0, -- 文件大小(字节)
error_msg TEXT DEFAULT '', -- 错误信息
started_at TIMESTAMP, -- 开始执行时间
finished_at TIMESTAMP, -- 执行结束时间
duration_seconds BIGINT NOT NULL DEFAULT 0, -- 耗时(秒):从创建到完成(成功/失败)整体耗时
expire_at TIMESTAMP, -- state=4 后写入,用于清理
retry_count INT NOT NULL DEFAULT 0, -- 已重试次数(不含首次)
enqueue_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 入队时间(用于排队顺序)
phase SMALLINT NOT NULL DEFAULT 0, -- 0模型阶段/1OSS阶段
tmp_file TEXT DEFAULT '', -- 临时结果文件路径(phase=1 时仅重试 OSS 上传)
input_ref TEXT DEFAULT '', -- 输入引用(如OSS/业务资源ID等)
request_payload JSONB, -- 请求参数(可选)
text_result TEXT DEFAULT '', -- 文本类结果(可选,支持直接回调)
epicycle_id VARCHAR(64) DEFAULT '', -- 轮次ID
expend_tokens BIGINT NOT NULL DEFAULT 0 -- 消耗 token 数
);
CREATE UNIQUE INDEX IF NOT EXISTS uk_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);
CREATE UNIQUE INDEX IF NOT EXISTS uk_asynch_task_tenant_task_id ON asynch_task(tenant_id, task_id);
CREATE INDEX IF NOT EXISTS idx_asynch_task_tenant_id ON asynch_task(tenant_id);
CREATE INDEX IF NOT EXISTS idx_asynch_task_model_name ON asynch_task(model_name);
CREATE INDEX IF NOT EXISTS idx_asynch_task_biz_name ON asynch_task(biz_name);
CREATE INDEX IF NOT EXISTS idx_asynch_task_model_key ON asynch_task(model_key);
CREATE INDEX IF NOT EXISTS idx_asynch_task_state ON asynch_task(state);
CREATE INDEX IF NOT EXISTS idx_asynch_task_enqueue_at ON asynch_task(enqueue_at);
CREATE INDEX IF NOT EXISTS idx_asynch_task_updated_at ON asynch_task(updated_at);
CREATE INDEX IF NOT EXISTS idx_asynch_task_expire_at ON asynch_task(expire_at);
CREATE INDEX IF NOT EXISTS idx_asynch_task_deleted_at ON asynch_task(deleted_at);
CREATE INDEX IF NOT EXISTS idx_asynch_task_epicycle_id ON asynch_task(epicycle_id);
CREATE INDEX IF NOT EXISTS idx_asynch_task_expend_tokens ON asynch_task(expend_tokens);
COMMENT ON TABLE asynch_task IS '异步任务表';
COMMENT ON COLUMN asynch_task.id IS '主键ID(非自增)';
COMMENT ON COLUMN asynch_task.tenant_id IS '租户ID';
COMMENT ON COLUMN asynch_task.creator IS '创建人';
COMMENT ON COLUMN asynch_task.created_at IS '创建时间';
COMMENT ON COLUMN asynch_task.updater IS '更新人';
COMMENT ON COLUMN asynch_task.updated_at IS '更新时间';
COMMENT ON COLUMN asynch_task.deleted_at IS '删除时间(软删)';
COMMENT ON COLUMN asynch_task.model_name IS '模型名称';
COMMENT ON COLUMN asynch_task.task_id IS '任务ID(对外返回)';
COMMENT ON COLUMN asynch_task.biz_name IS '业务名称(调用方模块/系统)';
COMMENT ON COLUMN asynch_task.callback_url IS '回调地址(可选,用于后续业务通知)';
COMMENT ON COLUMN asynch_task.model_key IS '动态请求头(用于覆盖/补充模型配置 head_msg),如 X-API-Key:xxx';
COMMENT ON COLUMN asynch_task.state IS '0排队中/1执行中/2成功/3失败/4已下载';
COMMENT ON COLUMN asynch_task.oss_file IS '结果文件OSS地址';
COMMENT ON COLUMN asynch_task.file_type IS '文件类型(mp3/mp4/png/...)';
COMMENT ON COLUMN asynch_task.file_size IS '文件大小(字节)';
COMMENT ON COLUMN asynch_task.error_msg IS '错误信息';
COMMENT ON COLUMN asynch_task.started_at IS '开始执行时间';
COMMENT ON COLUMN asynch_task.finished_at IS '执行结束时间';
COMMENT ON COLUMN asynch_task.duration_seconds IS '耗时(秒):从创建到完成(成功/失败)整体耗时';
COMMENT ON COLUMN asynch_task.expire_at IS 'state=4 后写入,用于清理';
COMMENT ON COLUMN asynch_task.retry_count IS '已重试次数(不含首次)';
COMMENT ON COLUMN asynch_task.enqueue_at IS '入队时间(用于排队顺序)';
COMMENT ON COLUMN asynch_task.phase IS '执行阶段 模型阶段/1OSS阶段(模型已成功,等待上传OSS)';
COMMENT ON COLUMN asynch_task.tmp_file IS '临时结果文件路径(phase=1 时仅重试 OSS 上传)';
COMMENT ON COLUMN asynch_task.input_ref IS '输入引用(如OSS/业务资源ID等)';
COMMENT ON COLUMN asynch_task.request_payload IS '请求参数(可选,JSON)';
COMMENT ON COLUMN asynch_task.text_result IS '文本类结果(可选,支持直接回调)';
COMMENT ON COLUMN asynch_task.epicycle_id IS '轮次ID(用于标识同一轮次的任务)';
COMMENT ON COLUMN asynch_task.expend_tokens IS '消耗 token 数';
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';
-- =========================
-- 3) logs_model_op
-- model_gateway_log_stat
-- =========================
CREATE TABLE IF NOT EXISTS logs_model_op (
-- 基础字段
id BIGINT PRIMARY KEY,
tenant_id BIGINT NOT NULL DEFAULT 0,
creator VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updater VARCHAR(64) NOT NULL,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP(6),
-- 基础审计信息
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 SMALLINT NOT NULL DEFAULT 1, -- 1成功/0失败
error_msg TEXT DEFAULT '',
cost_ms BIGINT NOT NULL DEFAULT 0, -- 耗时(毫秒)
-- 请求/响应 JSON(用于后期统计分析)
request_payload JSONB,
response_payload JSONB
);
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_logs_model_op_tenant_time ON logs_model_op(tenant_id, created_at);
CREATE INDEX IF NOT EXISTS idx_logs_model_op_model_name ON logs_model_op(model_name);
CREATE INDEX IF NOT EXISTS idx_logs_model_op_biz_name ON logs_model_op(biz_name);
CREATE INDEX IF NOT EXISTS idx_logs_model_op_task_id ON logs_model_op(task_id);
CREATE INDEX IF NOT EXISTS idx_logs_model_op_op_type ON logs_model_op(op_type);
CREATE INDEX IF NOT EXISTS idx_logs_model_op_deleted_at ON logs_model_op(deleted_at);
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 logs_model_op IS '操作记录日志表(创建任务等,用于统计)';
COMMENT ON COLUMN logs_model_op.id IS '主键ID(非自增)';
COMMENT ON COLUMN logs_model_op.tenant_id IS '租户ID';
COMMENT ON COLUMN logs_model_op.creator IS '创建人';
COMMENT ON COLUMN logs_model_op.created_at IS '创建时间';
COMMENT ON COLUMN logs_model_op.updater IS '更新人';
COMMENT ON COLUMN logs_model_op.updated_at IS '更新时间';
COMMENT ON COLUMN logs_model_op.deleted_at IS '删除时间(软删)';
COMMENT ON COLUMN logs_model_op.ip IS '客户端IP';
COMMENT ON COLUMN logs_model_op.user_agent IS 'User-Agent';
COMMENT ON COLUMN logs_model_op.api_path IS '接口路径';
COMMENT ON COLUMN logs_model_op.http_method IS 'HTTP方法';
COMMENT ON COLUMN logs_model_op.biz_name IS '业务名称(调用方模块/系统)';
COMMENT ON COLUMN logs_model_op.model_name IS '模型名称';
COMMENT ON COLUMN logs_model_op.task_id IS '任务ID';
COMMENT ON COLUMN logs_model_op.op_type IS '操作类型(如 createTask/getTaskResult/getTaskBatch 等)';
COMMENT ON COLUMN logs_model_op.success IS '是否成功:1成功/0失败';
COMMENT ON COLUMN logs_model_op.error_msg IS '错误信息(失败时)';
COMMENT ON COLUMN logs_model_op.cost_ms IS '耗时(毫秒)';
COMMENT ON COLUMN logs_model_op.request_payload IS '请求 JSON';
COMMENT ON COLUMN logs_model_op.response_payload IS '响应 JSON';
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 '更新时间';
-- =========================
-- 4) logs_model_stat
-- model_gateway_logs_op
-- =========================
CREATE TABLE IF NOT EXISTS logs_model_stat (
day DATE NOT NULL, -- 天(YYYY-MM-DD)
tenant_id BIGINT NOT NULL DEFAULT 0, -- 租户ID
creator VARCHAR(64) NOT NULL DEFAULT '', -- 创建人
model_name VARCHAR(128) NOT NULL DEFAULT '', -- 模型名称
request_count BIGINT NOT NULL DEFAULT 0, -- 请求次数
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY(day, tenant_id, creator, model_name)
);
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_logs_model_stat_tenant_day ON logs_model_stat(tenant_id, day);
CREATE INDEX IF NOT EXISTS idx_logs_model_stat_day ON logs_model_stat(day);
CREATE INDEX IF NOT EXISTS idx_logs_model_stat_model_name ON logs_model_stat(model_name);
CREATE INDEX IF NOT EXISTS idx_logs_model_stat_creator ON logs_model_stat(creator);
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 logs_model_stat IS '按天模型请求统计(用于限流/监控)';
COMMENT ON COLUMN logs_model_stat.day IS '天(YYYY-MM-DD)';
COMMENT ON COLUMN logs_model_stat.tenant_id IS '租户ID';
COMMENT ON COLUMN logs_model_stat.creator IS '创建人';
COMMENT ON COLUMN logs_model_stat.model_name IS '模型名称';
COMMENT ON COLUMN logs_model_stat.request_count IS '请求次数';
COMMENT ON COLUMN logs_model_stat.created_at IS '创建时间';
COMMENT ON COLUMN logs_model_stat.updated_at IS '更新时间';
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';
-- =========================================================================================================================
CREATE TABLE "public"."model_gateway_model_manage" (
"id" int8 NOT NULL,
"tenant_id" int8 NOT NULL DEFAULT 0,
"creator" varchar(64) COLLATE "pg_catalog"."default" NOT NULL,
"created_at" timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" varchar(64) COLLATE "pg_catalog"."default" NOT NULL,
"updated_at" timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" timestamp(6),
"model_supplier" varchar(32) COLLATE "pg_catalog"."default" NOT NULL DEFAULT ''::character varying,
"model_name" varchar(128) COLLATE "pg_catalog"."default" NOT NULL DEFAULT ''::character varying,
"model_type" varchar(32) COLLATE "pg_catalog"."default" NOT NULL DEFAULT ''::character varying,
"base_url" varchar(512) COLLATE "pg_catalog"."default" NOT NULL DEFAULT ''::character varying,
"system_model" bool,
"http_method" varchar(32) COLLATE "pg_catalog"."default" NOT NULL DEFAULT ''::character varying,
"chat_model" bool,
"response_type" int2 NOT NULL DEFAULT 0,
"api_key" varchar(255) COLLATE "pg_catalog"."default" NOT NULL DEFAULT ''::character varying,
"enabled" bool,
"request_head_mapping" jsonb DEFAULT '{}'::jsonb,
"request_body_mapping" jsonb DEFAULT '{}'::jsonb,
"response_mapping" jsonb DEFAULT '{}'::jsonb,
"max_concurrency" int4 NOT NULL DEFAULT 0,
"token_mapping" jsonb,
"async_task_mapping" jsonb,
"token_predict_price" numeric(12,6) NOT NULL DEFAULT 0.000000,
"max_tokens" int4 NOT NULL DEFAULT 0,
"last_frame" varchar(512) COLLATE "pg_catalog"."default" NOT NULL DEFAULT ''::character varying,
"response_body_mapping" jsonb DEFAULT '{}'::jsonb,
"token_predict_price_unit" varchar(32) COLLATE "pg_catalog"."default",
"max_duration" int4,
CONSTRAINT "model_gateway_model_manage_pkey" PRIMARY KEY ("id")
)
;
ALTER TABLE "public"."model_gateway_model_manage"
OWNER TO "postgres";
CREATE INDEX "idx_model_manage_deleted_at" ON "public"."model_gateway_model_manage" USING btree (
"deleted_at" "pg_catalog"."timestamp_ops" ASC NULLS LAST
);
CREATE INDEX "idx_model_manage_model_type" ON "public"."model_gateway_model_manage" USING btree (
"model_type" COLLATE "pg_catalog"."default" "pg_catalog"."text_ops" ASC NULLS LAST
);
CREATE INDEX "idx_model_manage_response_type" ON "public"."model_gateway_model_manage" USING btree (
"response_type" "pg_catalog"."int2_ops" ASC NULLS LAST
);
CREATE INDEX "idx_model_manage_supplier" ON "public"."model_gateway_model_manage" USING btree (
"model_supplier" COLLATE "pg_catalog"."default" "pg_catalog"."text_ops" ASC NULLS LAST
);
CREATE INDEX "idx_model_manage_tenant_id" ON "public"."model_gateway_model_manage" USING btree (
"tenant_id" "pg_catalog"."int8_ops" ASC NULLS LAST
);
COMMENT ON COLUMN "public"."model_gateway_model_manage"."id" IS '主键ID';
COMMENT ON COLUMN "public"."model_gateway_model_manage"."tenant_id" IS '租户ID';
COMMENT ON COLUMN "public"."model_gateway_model_manage"."creator" IS '创建人';
COMMENT ON COLUMN "public"."model_gateway_model_manage"."created_at" IS '创建时间';
COMMENT ON COLUMN "public"."model_gateway_model_manage"."updater" IS '更新人';
COMMENT ON COLUMN "public"."model_gateway_model_manage"."updated_at" IS '更新时间';
COMMENT ON COLUMN "public"."model_gateway_model_manage"."deleted_at" IS '删除时间(软删)';
COMMENT ON COLUMN "public"."model_gateway_model_manage"."model_supplier" IS '模型供应商';
COMMENT ON COLUMN "public"."model_gateway_model_manage"."model_name" IS '模型名称';
COMMENT ON COLUMN "public"."model_gateway_model_manage"."model_type" IS '模型类型';
COMMENT ON COLUMN "public"."model_gateway_model_manage"."base_url" IS '模型地址';
COMMENT ON COLUMN "public"."model_gateway_model_manage"."system_model" IS '是否系统模型';
COMMENT ON COLUMN "public"."model_gateway_model_manage"."http_method" IS 'http请求方法';
COMMENT ON COLUMN "public"."model_gateway_model_manage"."chat_model" IS '是否聊天模型';
COMMENT ON COLUMN "public"."model_gateway_model_manage"."response_type" IS '返回类型:1同步,2异步,3流';
COMMENT ON COLUMN "public"."model_gateway_model_manage"."api_key" IS '接口密钥';
COMMENT ON COLUMN "public"."model_gateway_model_manage"."enabled" IS '是否启用';
COMMENT ON COLUMN "public"."model_gateway_model_manage"."request_head_mapping" IS '请求头映射';
COMMENT ON COLUMN "public"."model_gateway_model_manage"."request_body_mapping" IS '请求体映射';
COMMENT ON COLUMN "public"."model_gateway_model_manage"."response_mapping" IS '响应映射';
COMMENT ON COLUMN "public"."model_gateway_model_manage"."max_concurrency" IS '最大并发数';
COMMENT ON COLUMN "public"."model_gateway_model_manage"."token_mapping" IS 'token映射';
COMMENT ON COLUMN "public"."model_gateway_model_manage"."async_task_mapping" IS '异步任务映射';
COMMENT ON COLUMN "public"."model_gateway_model_manage"."token_predict_price" IS '模型Token预估价格';
COMMENT ON COLUMN "public"."model_gateway_model_manage"."max_tokens" IS '最大token数';
COMMENT ON COLUMN "public"."model_gateway_model_manage"."last_frame" IS '视频尾帧图像地址';
COMMENT ON COLUMN "public"."model_gateway_model_manage"."response_body_mapping" IS '响应主体映射';
COMMENT ON COLUMN "public"."model_gateway_model_manage"."token_predict_price_unit" IS '模型token预估价格单位(秒,百万Token,千Token,字数)';
COMMENT ON COLUMN "public"."model_gateway_model_manage"."max_duration" IS '最大时长(秒)';
COMMENT ON TABLE "public"."model_gateway_model_manage" IS '模型管理表';