Compare commits
88
Commits
master
...
continue-exec
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73f296731c | ||
|
|
8c6305f267 | ||
|
|
f7957761f0 | ||
|
|
fc629ee493 | ||
|
|
d699f7ce14 | ||
|
|
67d049e586 | ||
|
|
740003a192 | ||
|
|
5c5f97af17 | ||
|
|
e0f7ce61a1 | ||
|
|
36b10ae8e7 | ||
|
|
ce2407bb2d | ||
|
|
95dc0d6052 | ||
|
|
788c835580 | ||
|
|
e3c299412f | ||
|
|
b039a70547 | ||
|
|
3620e1ea61 | ||
|
|
5672c8272e | ||
|
|
d5b2d90a27 | ||
|
|
b3f4b94b21 | ||
|
|
855d0cca72 | ||
|
|
01a73e5153 | ||
|
|
d984cba714 | ||
|
|
bae916e019 | ||
|
|
2d7f6888a6 | ||
|
|
0de29988b4 | ||
|
|
035c467a62 | ||
|
|
ac7936829f | ||
|
|
e0529c4ced | ||
|
|
a357ca860f | ||
|
|
8936a4c921 | ||
|
|
f0f8724bd9 | ||
|
|
75fb40274d | ||
|
|
f34566d9ad | ||
|
|
76b899133e | ||
|
|
294b8b229e | ||
|
|
c7091e3702 | ||
|
|
670fe61033 | ||
|
|
aae4ebe199 | ||
|
|
5b93a7a507 | ||
|
|
a06766db6d | ||
|
|
9bc388018b | ||
|
|
cef837a35c | ||
|
|
a939c45508 | ||
|
|
155a5cad0c | ||
|
|
1c0f1a7707 | ||
|
|
1dce62921f | ||
|
|
ab7b8889ed | ||
|
|
114563b4f0 | ||
|
|
358c5ca3a7 | ||
|
|
7ea3abbefe | ||
|
|
49674576ac | ||
|
|
9b23f69d9c | ||
|
|
cb9510c13b | ||
|
|
2f6f994730 | ||
|
|
bbd62c082d | ||
|
|
cfad80b359 | ||
|
|
f28ca0a50a | ||
|
|
db694c298c | ||
|
|
695c00aed5 | ||
|
|
6a249af7fa | ||
|
|
a6e98914e8 | ||
|
|
fba7d032ae | ||
|
|
ddc4e0be63 | ||
|
|
73dd18baf4 | ||
|
|
03c95c3601 | ||
|
|
ab3a2d967e | ||
|
|
34c5eeaf63 | ||
|
|
de55c16734 | ||
|
|
1fbed2febd | ||
|
|
d5206df131 | ||
|
|
ffba1f30ec | ||
|
|
b1ee117f6c | ||
|
|
d4614e3cc9 | ||
|
|
2c3cbab11d | ||
|
|
68576b2132 | ||
|
|
7c26914353 | ||
|
|
2aec7fe30f | ||
|
|
74ede5bc0f | ||
|
|
ed333dd15c | ||
|
|
1e693e32ca | ||
|
|
2600ed20ac | ||
|
|
11bf15e72b | ||
|
|
6ba2262a17 | ||
|
|
151e0bcd7d | ||
|
|
8679832019 | ||
|
|
f66925eb73 | ||
|
|
995e038541 | ||
|
|
ba360bc89b |
@@ -1 +1,3 @@
|
|||||||
/.idea/*
|
/.idea/*
|
||||||
|
/.superpowers/
|
||||||
|
/docs/superpowers/
|
||||||
|
|||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
# 阶段1: 构建
|
||||||
|
FROM golang:alpine AS builder
|
||||||
|
|
||||||
|
RUN apk add --no-cache git ca-certificates tzdata
|
||||||
|
|
||||||
|
ENV TZ=Asia/Shanghai
|
||||||
|
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||||
|
|
||||||
|
ENV GO111MODULE=on
|
||||||
|
ENV GOPROXY=https://goproxy.cn,direct
|
||||||
|
ENV CGO_ENABLED=0
|
||||||
|
ENV GOTOOLCHAIN=auto
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN go mod download && go mod tidy
|
||||||
|
|
||||||
|
RUN go build -ldflags="-s -w" -o main ./main.go
|
||||||
|
|
||||||
|
|
||||||
|
EXPOSE 3005
|
||||||
|
|
||||||
|
CMD ["./main"]
|
||||||
+75
@@ -0,0 +1,75 @@
|
|||||||
|
server:
|
||||||
|
address: ":3005"
|
||||||
|
name: "ai-agent"
|
||||||
|
workerId: 1 # 雪花算法worker ID
|
||||||
|
database:
|
||||||
|
default:
|
||||||
|
- type: "pgsql"
|
||||||
|
host: "192.168.0.83"
|
||||||
|
port: "15432"
|
||||||
|
user: "sql9f15b63fd203b36e"
|
||||||
|
pass: "1ec94b1acdaf57b66030242d418fad5a"
|
||||||
|
name: "digital-human"
|
||||||
|
prefix: "digital_human_" # (可选)表名前缀
|
||||||
|
role: "master" # (可选)数据库主从角色(master/slave),默认为master。如果不使用应用主从机制请不配置或留空即可。
|
||||||
|
debug: true # (可选)开启调试模式
|
||||||
|
dryRun: false # (可选)ORM空跑(只读不写)
|
||||||
|
charset: "utf8" # (可选)数据库编码(如: utf8mb4/utf8/gbk/gb2312),一般设置为utf8mb4。默认为utf8。
|
||||||
|
timezone: "Asia/Shanghai" # (可选)时区配置,例如:Local
|
||||||
|
maxIdle: 5 # (可选)连接池最大闲置的连接数(默认10)
|
||||||
|
maxOpen: 20 # (可选)连接池最大打开的连接数(默认无限制)
|
||||||
|
maxLifetime: "30s" # (可选)连接对象可重复使用的时间长度(默认30秒)
|
||||||
|
maxIdleConnTime: "30s" # (可选,v2.10新增)连接池中空闲连接的最大生存时间(默认30秒)。可以通过配置文件或SetConnMaxIdleTime方法设置,避免长时间空闲连接占用资源。
|
||||||
|
createdAt: "created_at" # (可选)自动创建时间字段名称
|
||||||
|
updatedAt: "updated_at" # (可选)自动更新时间字段名称
|
||||||
|
deletedAt: "deleted_at" # (可选)软删除时间字段名称
|
||||||
|
timeMaintainDisabled: false # (可选)是否完全关闭时间更新特性,为true时CreatedAt/UpdatedAt/DeletedAt都将失效
|
||||||
|
black_deacon:
|
||||||
|
- type: "pgsql"
|
||||||
|
host: "192.168.0.83"
|
||||||
|
port: "15432"
|
||||||
|
user: "sql9f15b63fd203b36e"
|
||||||
|
pass: "1ec94b1acdaf57b66030242d418fad5a"
|
||||||
|
name: "black-deacon"
|
||||||
|
prefix: "black_deacon_" # (可选)表名前缀
|
||||||
|
role: "master"
|
||||||
|
debug: true # (可选)开启调试模式
|
||||||
|
dryRun: false # (可选)ORM空跑(只读不写)
|
||||||
|
charset: "utf8" # (可选)数据库编码(如: utf8mb4/utf8/gbk/gb2312),一般设置为utf8mb4。默认为utf8。
|
||||||
|
timezone: "Asia/Shanghai" # (可选)时区配置,例如:Local
|
||||||
|
maxIdle: 5 # (可选)连接池最大闲置的连接数(默认10)
|
||||||
|
maxOpen: 20 # (可选)连接池最大打开的连接数(默认无限制)
|
||||||
|
maxLifetime: "30s" # (可选)连接对象可重复使用的时间长度(默认30秒)
|
||||||
|
maxIdleConnTime: "30s" # (可选,v2.10新增)连接池中空闲连接的最大生存时间(默认30秒)。可以通过配置文件或SetConnMaxIdleTime方法设置,避免长时间空闲连接占用资源。
|
||||||
|
createdAt: "created_at" # (可选)自动创建时间字段名称
|
||||||
|
updatedAt: "updated_at" # (可选)自动更新时间字段名称
|
||||||
|
deletedAt: "deleted_at" # (可选)软删除时间字段名称
|
||||||
|
timeMaintainDisabled: false # (可选)是否完全关闭时间更新特性,为true时CreatedAt/UpdatedAt/DeletedAt都将失效
|
||||||
|
|
||||||
|
redis:
|
||||||
|
default:
|
||||||
|
address: 192.168.0.83:6379
|
||||||
|
db: 0
|
||||||
|
idleTimeout: "60s" #连接最大空闲时间,使用时间字符串例如30s/1m/1d
|
||||||
|
maxConnLifetime: "90s" #连接最长存活时间,使用时间字符串例如30s/1m/1d
|
||||||
|
waitTimeout: "60s" #等待连接池连接的超时时间,使用时间字符串例如30s/1m/1d
|
||||||
|
dialTimeout: "30s" #TCP连接的超时时间,使用时间字符串例如30s/1m/1d
|
||||||
|
readTimeout: "30s" #TCP的Read操作超时时间,使用时间字符串例如30s/1m/1d
|
||||||
|
writeTimeout: "30s" #TCP的Write操作超时时间,使用时间字符串例如30s/1m/1d
|
||||||
|
maxActive: 100
|
||||||
|
|
||||||
|
consul:
|
||||||
|
address: 192.168.0.83:8500
|
||||||
|
|
||||||
|
jaeger:
|
||||||
|
addr: 192.168.0.83:4318
|
||||||
|
|
||||||
|
nats:
|
||||||
|
addr: 192.168.0.83
|
||||||
|
port: 4222
|
||||||
|
|
||||||
|
# 文件上传服务地址,cdn访问地址
|
||||||
|
filePrefix: "http://cdn.redpowerfuture.com"
|
||||||
|
|
||||||
|
# 文件上传服务地址,minio内网访问地址
|
||||||
|
minioPrefix: "http://192.168.0.83:9000"
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package account
|
||||||
|
|
||||||
|
import "github.com/gogf/gf/v2/util/gconv"
|
||||||
|
|
||||||
|
var (
|
||||||
|
PlatformXHS = newPlatform(gconv.PtrString("xiaohongshu"), "小红书")
|
||||||
|
PlatformDY = newPlatform(gconv.PtrString("douyin"), "抖音")
|
||||||
|
PlatformKS = newPlatform(gconv.PtrString("kuaishou"), "快手")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Platform *string
|
||||||
|
|
||||||
|
type platform struct {
|
||||||
|
code Platform
|
||||||
|
desc string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s platform) Code() Platform {
|
||||||
|
return s.code
|
||||||
|
}
|
||||||
|
func (s platform) Desc() string {
|
||||||
|
return s.desc
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPlatform(code Platform, desc string) platform {
|
||||||
|
return platform{code: code, desc: desc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDescByCode(code Platform) string {
|
||||||
|
switch *code {
|
||||||
|
case *PlatformXHS.Code():
|
||||||
|
return PlatformXHS.Desc()
|
||||||
|
case *PlatformDY.Code():
|
||||||
|
return PlatformDY.Desc()
|
||||||
|
case *PlatformKS.Code():
|
||||||
|
return PlatformKS.Desc()
|
||||||
|
}
|
||||||
|
return "未知平台"
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package account
|
||||||
|
|
||||||
|
import "github.com/gogf/gf/v2/util/gconv"
|
||||||
|
|
||||||
|
var (
|
||||||
|
StatusDisable = newStatus(gconv.PtrInt8(0), "disable")
|
||||||
|
StatusEnable = newStatus(gconv.PtrInt8(1), "enable")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Status *int8
|
||||||
|
|
||||||
|
type status struct {
|
||||||
|
code Status
|
||||||
|
desc string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s status) Code() Status {
|
||||||
|
return s.code
|
||||||
|
}
|
||||||
|
func (s status) Desc() string {
|
||||||
|
return s.desc
|
||||||
|
}
|
||||||
|
|
||||||
|
func newStatus(code Status, desc string) status {
|
||||||
|
return status{code: code, desc: desc}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package consts
|
||||||
|
|
||||||
|
const ReClick = "操作过于频繁,请稍后再试。"
|
||||||
|
const NoRow = "未找到可用数据。"
|
||||||
|
const GenerateQrCodeFail = "生成二维码失败。"
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package public
|
||||||
|
|
||||||
|
const GmqMsgPluginsName = "gmq_msg"
|
||||||
|
|
||||||
|
const (
|
||||||
|
AccountMsgKey = "account:%s:%s:%s"
|
||||||
|
AccountDialogHistoryKey = "account:dialog:history:%s"
|
||||||
|
AccountGreetingOptionsKey = "account:greeting:options:%s"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
AccountFollowupTopic = "account:followup:stream" // 请求 Stream 键名(与发消息的key一致)
|
||||||
|
AccountFollowupConsumer = "account-followup-consumer" // 消费者名称(唯一标识)
|
||||||
|
AccountFollowupCount = 1 // 批处理大小(每次读取1条)
|
||||||
|
AccountFollowupAck = false // ACK是否自动确认(true自动确认,false不确认)
|
||||||
|
)
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package public
|
||||||
|
|
||||||
|
// 欢迎语
|
||||||
|
const (
|
||||||
|
GreetingBegin = "您好,很高兴为您服务!请问有什么可以帮您?"
|
||||||
|
GreetingBetween = "💗回复数字就好~"
|
||||||
|
GreetingEnd = "🌟也可直接点击下方咨询专业老师~"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 追问
|
||||||
|
const (
|
||||||
|
SceneOpeningRemark = "宝子,刚才给您发的信息您有看到吗?有任何问题都能直接问我,加微信也能更方便沟通~"
|
||||||
|
SceneDialog = "看您暂时没回复,是不是还有什么疑问?加微信我详细给您说明~"
|
||||||
|
SceneCardSend = "宝子,加上没~要及时加哦,不然卡片容易失效哒✨"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 对话超时时间
|
||||||
|
const DialogTimeout = 10
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package public
|
||||||
|
|
||||||
|
// sql 数据库表名
|
||||||
|
const (
|
||||||
|
TableNameAccount = "account"
|
||||||
|
TableNameAccountUserDialog = "account_user_dialog"
|
||||||
|
TableNameScriptedSpeech = "scripted_speech"
|
||||||
|
)
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package consts
|
||||||
|
|
||||||
|
const QrCodeCount = "qrCodeCount:order:%s"
|
||||||
|
const QrCode = "qrCode:order:%s"
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package scriptedSpeech
|
||||||
|
|
||||||
|
import "github.com/gogf/gf/v2/util/gconv"
|
||||||
|
|
||||||
|
var (
|
||||||
|
SceneTypeOpeningRemark = newSceneType(gconv.PtrInt8(1), "开场白无回应")
|
||||||
|
SceneTypeDialog = newSceneType(gconv.PtrInt8(2), "对话中途无回应")
|
||||||
|
SceneTypeCardSend = newSceneType(gconv.PtrInt8(3), "卡片发送后无回应")
|
||||||
|
)
|
||||||
|
|
||||||
|
type SceneType *int8
|
||||||
|
|
||||||
|
type sceneType struct {
|
||||||
|
code SceneType
|
||||||
|
desc string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s sceneType) Code() SceneType {
|
||||||
|
return s.code
|
||||||
|
}
|
||||||
|
func (s sceneType) Desc() string {
|
||||||
|
return s.desc
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSceneType(code SceneType, desc string) sceneType {
|
||||||
|
return sceneType{code: code, desc: desc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDescByCode(code SceneType) string {
|
||||||
|
switch *code {
|
||||||
|
case *SceneTypeOpeningRemark.Code():
|
||||||
|
return SceneTypeOpeningRemark.Desc()
|
||||||
|
case *SceneTypeDialog.Code():
|
||||||
|
return SceneTypeDialog.Desc()
|
||||||
|
case *SceneTypeCardSend.Code():
|
||||||
|
return SceneTypeCardSend.Desc()
|
||||||
|
}
|
||||||
|
return "未知场景类型"
|
||||||
|
}
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var text = "欢迎使用红动未来数字人服务平台,我们将为您提供最优质的AI数字人解决方案。"
|
||||||
|
|
||||||
|
type TTSCommonResponse struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
Audio string `json:"audio"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// 获取当前工作目录
|
||||||
|
outputDir, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("获取当前目录失败: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找项目根目录(向上查找包含 go.mod 的目录)
|
||||||
|
outputDir = findProjectRoot(outputDir)
|
||||||
|
|
||||||
|
// 验证根目录是否正确(检查是否有 go.mod)
|
||||||
|
if _, err := os.Stat(outputDir + "/go.mod"); err != nil {
|
||||||
|
fmt.Printf("未找到项目根目录,当前目录: %s\n", outputDir)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("=================== TTS测试开始 ===================")
|
||||||
|
fmt.Printf("输出目录: %s\n", outputDir)
|
||||||
|
fmt.Printf("随机文本: %s\n", text)
|
||||||
|
fmt.Printf("请求URL: http://127.0.0.1:8000/tts\n")
|
||||||
|
|
||||||
|
// 创建带超时的 HTTP 客户端(120秒超时)
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: 120 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Post("http://127.0.0.1:8000/tts", "application/json", bytes.NewBufferString(fmt.Sprintf(`"%s"`, text)))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("请求失败: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// 打印响应头
|
||||||
|
fmt.Printf("Content-Type: %s\n", resp.Header.Get("Content-Type"))
|
||||||
|
fmt.Printf("Content-Length: %s\n", resp.Header.Get("Content-Length"))
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("读取响应失败: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("状态码: %d, 响应大小: %d字节\n", resp.StatusCode, len(body))
|
||||||
|
|
||||||
|
// 打印响应内容的前200字节(用于调试)
|
||||||
|
if len(body) > 0 {
|
||||||
|
previewLen := minInt(200, len(body))
|
||||||
|
fmt.Printf("响应内容预览(前%d字节): ", previewLen)
|
||||||
|
if len(body) >= 4 && string(body[:4]) == "RIFF" {
|
||||||
|
// WAV文件头
|
||||||
|
fmt.Printf("WAV文件格式 (RIFF...)\n")
|
||||||
|
} else if len(body) >= 3 && string(body[:3]) == "ID3" {
|
||||||
|
// MP3 ID3标签
|
||||||
|
fmt.Printf("MP3 ID3格式\n")
|
||||||
|
} else if len(body) >= 2 && body[0] == 0xFF && (body[1]&0xE0) == 0xE0 {
|
||||||
|
// MP3帧同步
|
||||||
|
fmt.Printf("MP3帧格式\n")
|
||||||
|
} else {
|
||||||
|
// 可能是JSON或其他格式
|
||||||
|
fmt.Printf("%s\n", string(body[:previewLen]))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Printf("响应内容为空!\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试解析JSON响应(包含base64音频)
|
||||||
|
var commonResp TTSCommonResponse
|
||||||
|
var audioData []byte
|
||||||
|
var ext string
|
||||||
|
|
||||||
|
if json.Unmarshal(body, &commonResp) == nil && commonResp.Audio != "" && commonResp.Audio != "base64_placeholder" {
|
||||||
|
fmt.Printf("检测到JSON响应,code=%d, msg=%s\n", commonResp.Code, commonResp.Msg)
|
||||||
|
fmt.Printf("Audio字段长度: %d 字符\n", len(commonResp.Audio))
|
||||||
|
|
||||||
|
// 检查是否成功
|
||||||
|
if commonResp.Code != 0 {
|
||||||
|
fmt.Printf("TTS服务返回错误: %s\n", commonResp.Msg)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解码base64音频数据
|
||||||
|
decoded, err := base64.StdEncoding.DecodeString(commonResp.Audio)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("base64解码失败: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if len(decoded) == 0 {
|
||||||
|
fmt.Printf("解码后数据为空!\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
audioData = decoded
|
||||||
|
fmt.Printf("解码后音频数据大小: %d 字节\n", len(audioData))
|
||||||
|
|
||||||
|
// 根据解码后的音频数据格式决定扩展名
|
||||||
|
if len(audioData) >= 4 && string(audioData[:4]) == "RIFF" {
|
||||||
|
ext = ".wav"
|
||||||
|
fmt.Printf("检测到WAV格式\n")
|
||||||
|
} else if len(audioData) >= 3 && string(audioData[:3]) == "ID3" || (len(audioData) >= 2 && audioData[0] == 0xFF && (audioData[1]&0xE0) == 0xE0) {
|
||||||
|
ext = ".mp3"
|
||||||
|
fmt.Printf("检测到MP3格式\n")
|
||||||
|
} else {
|
||||||
|
ext = ".wav" // 默认wav
|
||||||
|
fmt.Printf("未知格式,默认保存为 .wav\n")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 直接是二进制音频数据
|
||||||
|
audioData = body
|
||||||
|
|
||||||
|
// 根据音频数据格式决定扩展名
|
||||||
|
if len(audioData) >= 4 && string(audioData[:4]) == "RIFF" {
|
||||||
|
ext = ".wav"
|
||||||
|
} else if len(audioData) >= 3 && string(audioData[:3]) == "ID3" || (len(audioData) >= 2 && audioData[0] == 0xFF && (audioData[1]&0xE0) == 0xE0) {
|
||||||
|
ext = ".mp3"
|
||||||
|
} else {
|
||||||
|
ext = ".wav" // 默认wav
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存音频文件
|
||||||
|
filename := fmt.Sprintf("%s/tts_output_%d%s", outputDir, time.Now().Unix(), ext)
|
||||||
|
if err = os.WriteFile(filename, audioData, 0644); err != nil {
|
||||||
|
fmt.Printf("写文件失败: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Printf("音频已保存: %s (%d字节)\n", filename, len(audioData))
|
||||||
|
fmt.Println("=================== TTS测试成功 ===================")
|
||||||
|
}
|
||||||
|
|
||||||
|
func maxInt(a, b int) int {
|
||||||
|
if a > b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func minInt(a, b int) int {
|
||||||
|
if a < b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// findProjectRoot 查找项目根目录(包含 go.mod 的目录)
|
||||||
|
func findProjectRoot(startDir string) string {
|
||||||
|
dir := startDir
|
||||||
|
for {
|
||||||
|
// 检查当前目录是否有 go.mod
|
||||||
|
if _, err := os.Stat(dir + "/go.mod"); err == nil {
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果已经是根目录或无法继续向上查找,返回当前目录
|
||||||
|
parentDir := dir[:maxInt(0, len(dir)-len("/"+getLastPathSegment(dir)))]
|
||||||
|
if parentDir == dir || parentDir == "" {
|
||||||
|
return startDir
|
||||||
|
}
|
||||||
|
|
||||||
|
dir = parentDir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getLastPathSegment 获取路径的最后一部分
|
||||||
|
func getLastPathSegment(path string) string {
|
||||||
|
if idx := strings.LastIndex(path, "/"); idx != -1 {
|
||||||
|
return path[idx+1:]
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package consts
|
||||||
|
|
||||||
|
// Age 年龄段类型
|
||||||
|
type Age string
|
||||||
|
|
||||||
|
// 年龄段常量
|
||||||
|
const (
|
||||||
|
AgeChild Age = "child" // 儿童
|
||||||
|
AgeTeenager Age = "teenager" // 青少年
|
||||||
|
AgeYoung Age = "young" // 青年
|
||||||
|
AgeMiddle Age = "middle" // 中年
|
||||||
|
AgeSenior Age = "senior" // 老年
|
||||||
|
AgeUnlimited Age = "unlimited" // 不限
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetAgeText 获取年龄段文本
|
||||||
|
func GetAgeText(age string) string {
|
||||||
|
switch age {
|
||||||
|
case string(AgeChild):
|
||||||
|
return "儿童"
|
||||||
|
case string(AgeTeenager):
|
||||||
|
return "青少年"
|
||||||
|
case string(AgeYoung):
|
||||||
|
return "青年"
|
||||||
|
case string(AgeMiddle):
|
||||||
|
return "中年"
|
||||||
|
case string(AgeSenior):
|
||||||
|
return "老年"
|
||||||
|
case string(AgeUnlimited):
|
||||||
|
return "不限"
|
||||||
|
default:
|
||||||
|
return "未知"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllAgeKeyValue 获取所有年龄段选项
|
||||||
|
func GetAllAgeKeyValue() []AgeKeyValue {
|
||||||
|
return []AgeKeyValue{
|
||||||
|
{Value: string(AgeChild), Label: "儿童"},
|
||||||
|
{Value: string(AgeTeenager), Label: "青少年"},
|
||||||
|
{Value: string(AgeYoung), Label: "青年"},
|
||||||
|
{Value: string(AgeMiddle), Label: "中年"},
|
||||||
|
{Value: string(AgeSenior), Label: "老年"},
|
||||||
|
{Value: string(AgeUnlimited), Label: "不限"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AgeKeyValue 年龄段键值对
|
||||||
|
type AgeKeyValue struct {
|
||||||
|
Value string `json:"value"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package consts
|
||||||
|
|
||||||
|
// AudioStatus 音频状态类型
|
||||||
|
type AudioStatus int
|
||||||
|
|
||||||
|
// 音频状态常量
|
||||||
|
const (
|
||||||
|
AudioStatusGenerating AudioStatus = 0 // 生成中
|
||||||
|
AudioStatusSuccess AudioStatus = 1 // 成功
|
||||||
|
AudioStatusFailed AudioStatus = 2 // 失败
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetAudioStatusText 获取音频状态文本
|
||||||
|
func GetAudioStatusText(status int) string {
|
||||||
|
switch status {
|
||||||
|
case int(AudioStatusGenerating):
|
||||||
|
return "生成中"
|
||||||
|
case int(AudioStatusSuccess):
|
||||||
|
return "成功"
|
||||||
|
case int(AudioStatusFailed):
|
||||||
|
return "失败"
|
||||||
|
default:
|
||||||
|
return "未知"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllAudioStatusKeyValue 获取所有音频状态选项
|
||||||
|
func GetAllAudioStatusKeyValue() []AudioStatusKeyValue {
|
||||||
|
return []AudioStatusKeyValue{
|
||||||
|
{Value: int(AudioStatusGenerating), Label: "生成中"},
|
||||||
|
{Value: int(AudioStatusSuccess), Label: "成功"},
|
||||||
|
{Value: int(AudioStatusFailed), Label: "失败"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AudioStatusKeyValue 音频状态键值对
|
||||||
|
type AudioStatusKeyValue struct {
|
||||||
|
Value int `json:"value"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package consts
|
||||||
|
|
||||||
|
// MongoDB集合名称常量
|
||||||
|
const (
|
||||||
|
DigitalHumanCollection = "digital_human" // 数字人形象集合
|
||||||
|
AudioCollection = "audio" // 音频集合
|
||||||
|
VideoCollection = "video" // 视频集合
|
||||||
|
)
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package consts
|
||||||
|
|
||||||
|
// CustomVoiceStatus 自定义音色状态类型
|
||||||
|
type CustomVoiceStatus int
|
||||||
|
|
||||||
|
const (
|
||||||
|
CustomVoiceStatusGenerating CustomVoiceStatus = 0 // 生成中
|
||||||
|
CustomVoiceStatusSuccess CustomVoiceStatus = 1 // 成功
|
||||||
|
CustomVoiceStatusFailed CustomVoiceStatus = 2 // 失败
|
||||||
|
)
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package consts
|
||||||
|
|
||||||
|
// DigitalHumanStatus 数字人状态类型
|
||||||
|
type DigitalHumanStatus int
|
||||||
|
|
||||||
|
// 数字人状态常量
|
||||||
|
const (
|
||||||
|
DigitalHumanStatusInactive DigitalHumanStatus = 0 // 停用
|
||||||
|
DigitalHumanStatusActive DigitalHumanStatus = 1 // 启用
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetDigitalHumanStatusText 获取数字人状态文本
|
||||||
|
func GetDigitalHumanStatusText(status int) string {
|
||||||
|
switch status {
|
||||||
|
case int(DigitalHumanStatusInactive):
|
||||||
|
return "停用"
|
||||||
|
case int(DigitalHumanStatusActive):
|
||||||
|
return "启用"
|
||||||
|
default:
|
||||||
|
return "未知"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllDigitalHumanStatusKeyValue 获取所有数字人状态选项
|
||||||
|
func GetAllDigitalHumanStatusKeyValue() []DigitalHumanStatusKeyValue {
|
||||||
|
return []DigitalHumanStatusKeyValue{
|
||||||
|
{Value: int(DigitalHumanStatusInactive), Label: "停用"},
|
||||||
|
{Value: int(DigitalHumanStatusActive), Label: "启用"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DigitalHumanStatusKeyValue 数字人状态键值对
|
||||||
|
type DigitalHumanStatusKeyValue struct {
|
||||||
|
Value int `json:"value"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStatusText 获取状态文本(向后兼容)
|
||||||
|
func GetStatusText(status int) string {
|
||||||
|
return GetDigitalHumanStatusText(status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllStatusKeyValue 获取所有状态选项(向后兼容)
|
||||||
|
func GetAllStatusKeyValue() []StatusKeyValue {
|
||||||
|
return []StatusKeyValue{
|
||||||
|
{Value: int(DigitalHumanStatusInactive), Label: "停用"},
|
||||||
|
{Value: int(DigitalHumanStatusActive), Label: "启用"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StatusKeyValue 状态键值对(向后兼容)
|
||||||
|
type StatusKeyValue struct {
|
||||||
|
Value int `json:"value"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package consts
|
||||||
|
|
||||||
|
// Gender 性别类型
|
||||||
|
type Gender string
|
||||||
|
|
||||||
|
// 性别常量
|
||||||
|
const (
|
||||||
|
GenderMale Gender = "male" // 男
|
||||||
|
GenderFemale Gender = "female" // 女
|
||||||
|
GenderOther Gender = "other" // 其他
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetGenderText 获取性别文本
|
||||||
|
func GetGenderText(gender string) string {
|
||||||
|
switch gender {
|
||||||
|
case string(GenderMale):
|
||||||
|
return "男"
|
||||||
|
case string(GenderFemale):
|
||||||
|
return "女"
|
||||||
|
case string(GenderOther):
|
||||||
|
return "其他"
|
||||||
|
default:
|
||||||
|
return "未知"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllGenderKeyValue 获取所有性别选项
|
||||||
|
func GetAllGenderKeyValue() []GenderKeyValue {
|
||||||
|
return []GenderKeyValue{
|
||||||
|
{Value: string(GenderMale), Label: "男"},
|
||||||
|
{Value: string(GenderFemale), Label: "女"},
|
||||||
|
{Value: string(GenderOther), Label: "其他"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenderKeyValue 性别键值对
|
||||||
|
type GenderKeyValue struct {
|
||||||
|
Value string `json:"value"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package public
|
||||||
|
|
||||||
|
const (
|
||||||
|
ModelNameCustomVoice = "qwen3-tts-customvoice" // 预设音频
|
||||||
|
ModelNameVoiceDesign = "qwen3-tts-voicedesign" // 设计音频
|
||||||
|
ModelNameBase = "qwen3-tts-base" // 克隆音频
|
||||||
|
)
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package public
|
||||||
|
|
||||||
|
const (
|
||||||
|
TableNameAudio = "digital_human_audio"
|
||||||
|
TableNameCustomVoice = "digital_human_custom_voice"
|
||||||
|
TableNameAsyncTaskRef = "digital_human_async_task_ref"
|
||||||
|
TableNameVideo = "digital_human_video"
|
||||||
|
TableNameDigitalHuman = "digital_human"
|
||||||
|
)
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package consts
|
||||||
|
|
||||||
|
// Resolution 视频分辨率
|
||||||
|
type Resolution string
|
||||||
|
|
||||||
|
const (
|
||||||
|
Resolution480P Resolution = "480p" // 标清
|
||||||
|
Resolution720P Resolution = "720p" // 高清
|
||||||
|
Resolution1080P Resolution = "1080p" // 全高清
|
||||||
|
Resolution2K Resolution = "2k" // 2K超清
|
||||||
|
Resolution4K Resolution = "4k" // 4K超高清
|
||||||
|
Resolution8K Resolution = "8k" // 8K超高清
|
||||||
|
)
|
||||||
|
|
||||||
|
// Text 获取分辨率文本描述
|
||||||
|
func (r Resolution) Text() string {
|
||||||
|
switch r {
|
||||||
|
case Resolution480P:
|
||||||
|
return "标清 (480p)"
|
||||||
|
case Resolution720P:
|
||||||
|
return "高清 (720p)"
|
||||||
|
case Resolution1080P:
|
||||||
|
return "全高清 (1080p)"
|
||||||
|
case Resolution2K:
|
||||||
|
return "2K超清 (1440p)"
|
||||||
|
case Resolution4K:
|
||||||
|
return "4K超高清 (2160p)"
|
||||||
|
case Resolution8K:
|
||||||
|
return "8K超高清 (4320p)"
|
||||||
|
default:
|
||||||
|
return string(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolutionKeyValue 分辨率键值对(用于前端选项)
|
||||||
|
type ResolutionKeyValue struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetResolutionOptions 获取所有分辨率选项
|
||||||
|
func GetResolutionOptions() []ResolutionKeyValue {
|
||||||
|
return []ResolutionKeyValue{
|
||||||
|
{Key: string(Resolution480P), Value: Resolution480P.Text()},
|
||||||
|
{Key: string(Resolution720P), Value: Resolution720P.Text()},
|
||||||
|
{Key: string(Resolution1080P), Value: Resolution1080P.Text()},
|
||||||
|
{Key: string(Resolution2K), Value: Resolution2K.Text()},
|
||||||
|
{Key: string(Resolution4K), Value: Resolution4K.Text()},
|
||||||
|
{Key: string(Resolution8K), Value: Resolution8K.Text()},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package consts
|
||||||
|
|
||||||
|
// Style 风格类型
|
||||||
|
type Style string
|
||||||
|
|
||||||
|
// 风格常量
|
||||||
|
const (
|
||||||
|
StyleBusiness Style = "business" // 商务
|
||||||
|
StyleCasual Style = "casual" // 休闲
|
||||||
|
StyleFormal Style = "formal" // 正式
|
||||||
|
StyleCreative Style = "creative" // 创意
|
||||||
|
StyleElegant Style = "elegant" // 优雅
|
||||||
|
StyleFriendly Style = "friendly" // 友好
|
||||||
|
StyleProfessional Style = "professional" // 专业
|
||||||
|
StyleUnlimited Style = "unlimited" // 不限
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetStyleText 获取风格文本
|
||||||
|
func GetStyleText(style string) string {
|
||||||
|
switch style {
|
||||||
|
case string(StyleBusiness):
|
||||||
|
return "商务"
|
||||||
|
case string(StyleCasual):
|
||||||
|
return "休闲"
|
||||||
|
case string(StyleFormal):
|
||||||
|
return "正式"
|
||||||
|
case string(StyleCreative):
|
||||||
|
return "创意"
|
||||||
|
case string(StyleElegant):
|
||||||
|
return "优雅"
|
||||||
|
case string(StyleFriendly):
|
||||||
|
return "友好"
|
||||||
|
case string(StyleProfessional):
|
||||||
|
return "专业"
|
||||||
|
case string(StyleUnlimited):
|
||||||
|
return "不限"
|
||||||
|
default:
|
||||||
|
return "未知"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllStyleKeyValue 获取所有风格选项
|
||||||
|
func GetAllStyleKeyValue() []StyleKeyValue {
|
||||||
|
return []StyleKeyValue{
|
||||||
|
{Value: string(StyleBusiness), Label: "商务"},
|
||||||
|
{Value: string(StyleCasual), Label: "休闲"},
|
||||||
|
{Value: string(StyleFormal), Label: "正式"},
|
||||||
|
{Value: string(StyleCreative), Label: "创意"},
|
||||||
|
{Value: string(StyleElegant), Label: "优雅"},
|
||||||
|
{Value: string(StyleFriendly), Label: "友好"},
|
||||||
|
{Value: string(StyleProfessional), Label: "专业"},
|
||||||
|
{Value: string(StyleUnlimited), Label: "不限"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StyleKeyValue 风格键值对
|
||||||
|
type StyleKeyValue struct {
|
||||||
|
Value string `json:"value"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package consts
|
||||||
|
|
||||||
|
// VideoStatus 视频状态类型
|
||||||
|
type VideoStatus int
|
||||||
|
|
||||||
|
// 视频生成状态常量
|
||||||
|
const (
|
||||||
|
VideoStatusGenerating VideoStatus = 0 // 生成中
|
||||||
|
VideoStatusSuccess VideoStatus = 1 // 成功
|
||||||
|
VideoStatusFailed VideoStatus = 2 // 失败
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetVideoStatusText 获取视频状态文本
|
||||||
|
func GetVideoStatusText(status int) string {
|
||||||
|
switch status {
|
||||||
|
case int(VideoStatusGenerating):
|
||||||
|
return "生成中"
|
||||||
|
case int(VideoStatusSuccess):
|
||||||
|
return "成功"
|
||||||
|
case int(VideoStatusFailed):
|
||||||
|
return "失败"
|
||||||
|
default:
|
||||||
|
return "未知"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllVideoStatusKeyValue 获取所有视频状态选项
|
||||||
|
func GetAllVideoStatusKeyValue() []VideoStatusKeyValue {
|
||||||
|
return []VideoStatusKeyValue{
|
||||||
|
{Value: int(VideoStatusGenerating), Label: "生成中"},
|
||||||
|
{Value: int(VideoStatusSuccess), Label: "成功"},
|
||||||
|
{Value: int(VideoStatusFailed), Label: "失败"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// VideoStatusKeyValue 视频状态键值对
|
||||||
|
type VideoStatusKeyValue struct {
|
||||||
|
Value int `json:"value"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"ai-agent/digital-human/model/dto"
|
||||||
|
"ai-agent/digital-human/service"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/util/gconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type asyncTask struct{}
|
||||||
|
|
||||||
|
// AsyncTask 异步任务同步控制器(供定时任务服务调用)
|
||||||
|
var AsyncTask = new(asyncTask)
|
||||||
|
|
||||||
|
// SyncAsyncTasks 扫描待处理任务并同步状态/转移结果
|
||||||
|
func (c *asyncTask) SyncAsyncTasks(ctx context.Context, req *dto.SyncAsyncTasksReq) (res *dto.SyncAsyncTasksRes, err error) {
|
||||||
|
// 从上下文获取用户信息(gfdb Hook 会自动填充)
|
||||||
|
if ctx.Value("userId") == nil {
|
||||||
|
ctx = context.WithValue(ctx, "userId", gconv.String(1))
|
||||||
|
}
|
||||||
|
if ctx.Value("userName") == nil {
|
||||||
|
ctx = context.WithValue(ctx, "userName", "admin")
|
||||||
|
}
|
||||||
|
if ctx.Value("tenantId") == nil {
|
||||||
|
ctx = context.WithValue(ctx, "tenantId", uint64(1))
|
||||||
|
}
|
||||||
|
return service.AsyncTask.Sync(ctx, req)
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ai-agent/digital-human/model/dto"
|
||||||
|
"ai-agent/digital-human/service"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/beans"
|
||||||
|
"github.com/gogf/gf/v2/util/gconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type audio struct{}
|
||||||
|
|
||||||
|
// Audio 音频控制器
|
||||||
|
var Audio = new(audio)
|
||||||
|
|
||||||
|
// CreateAudio 创建音频
|
||||||
|
func (c *audio) CreateAudio(ctx context.Context, req *dto.CreateAudioReq) (res *dto.CreateAudioRes, err error) {
|
||||||
|
// 从上下文获取用户信息(gfdb Hook 会自动填充)
|
||||||
|
if ctx.Value("userId") == nil {
|
||||||
|
ctx = context.WithValue(ctx, "userId", gconv.String(1))
|
||||||
|
}
|
||||||
|
if ctx.Value("userName") == nil {
|
||||||
|
ctx = context.WithValue(ctx, "userName", "admin")
|
||||||
|
}
|
||||||
|
if ctx.Value("tenantId") == nil {
|
||||||
|
ctx = context.WithValue(ctx, "tenantId", uint64(1))
|
||||||
|
}
|
||||||
|
return service.Audio.Create(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAudio 获取音频列表
|
||||||
|
func (c *audio) ListAudio(ctx context.Context, req *dto.ListAudioReq) (res *dto.ListAudioRes, err error) {
|
||||||
|
// 从上下文获取用户信息
|
||||||
|
if ctx.Value("userId") == nil {
|
||||||
|
ctx = context.WithValue(ctx, "userId", gconv.String(1))
|
||||||
|
}
|
||||||
|
if ctx.Value("userName") == nil {
|
||||||
|
ctx = context.WithValue(ctx, "userName", "admin")
|
||||||
|
}
|
||||||
|
if ctx.Value("tenantId") == nil {
|
||||||
|
ctx = context.WithValue(ctx, "tenantId", uint64(1))
|
||||||
|
}
|
||||||
|
return service.Audio.List(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAudio 获取音频详情
|
||||||
|
func (c *audio) GetAudio(ctx context.Context, req *dto.GetAudioReq) (res *dto.GetAudioRes, err error) {
|
||||||
|
if ctx.Value("userId") == nil {
|
||||||
|
ctx = context.WithValue(ctx, "userId", gconv.String(1))
|
||||||
|
}
|
||||||
|
if ctx.Value("userName") == nil {
|
||||||
|
ctx = context.WithValue(ctx, "userName", "admin")
|
||||||
|
}
|
||||||
|
if ctx.Value("tenantId") == nil {
|
||||||
|
ctx = context.WithValue(ctx, "tenantId", uint64(1))
|
||||||
|
}
|
||||||
|
return service.Audio.GetOne(ctx, req.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateAudio 更新音频
|
||||||
|
func (c *audio) UpdateAudio(ctx context.Context, req *dto.UpdateAudioReq) (res *beans.ResponseEmpty, err error) {
|
||||||
|
if ctx.Value("userId") == nil {
|
||||||
|
ctx = context.WithValue(ctx, "userId", gconv.String(1))
|
||||||
|
}
|
||||||
|
if ctx.Value("userName") == nil {
|
||||||
|
ctx = context.WithValue(ctx, "userName", "admin")
|
||||||
|
}
|
||||||
|
if ctx.Value("tenantId") == nil {
|
||||||
|
ctx = context.WithValue(ctx, "tenantId", uint64(1))
|
||||||
|
}
|
||||||
|
err = service.Audio.Update(ctx, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteAudio 删除音频
|
||||||
|
func (c *audio) DeleteAudio(ctx context.Context, req *dto.DeleteAudioReq) (res *beans.ResponseEmpty, err error) {
|
||||||
|
if ctx.Value("userId") == nil {
|
||||||
|
ctx = context.WithValue(ctx, "userId", gconv.String(1))
|
||||||
|
}
|
||||||
|
if ctx.Value("userName") == nil {
|
||||||
|
ctx = context.WithValue(ctx, "userName", "admin")
|
||||||
|
}
|
||||||
|
if ctx.Value("tenantId") == nil {
|
||||||
|
ctx = context.WithValue(ctx, "tenantId", uint64(1))
|
||||||
|
}
|
||||||
|
err = service.Audio.Delete(ctx, req.ID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateAudio 重新生成音频
|
||||||
|
func (c *audio) GenerateAudio(ctx context.Context, req *dto.GenerateAudioReq) (res *dto.GenerateAudioRes, err error) {
|
||||||
|
if ctx.Value("userId") == nil {
|
||||||
|
ctx = context.WithValue(ctx, "userId", gconv.String(1))
|
||||||
|
}
|
||||||
|
if ctx.Value("userName") == nil {
|
||||||
|
ctx = context.WithValue(ctx, "userName", "admin")
|
||||||
|
}
|
||||||
|
if ctx.Value("tenantId") == nil {
|
||||||
|
ctx = context.WithValue(ctx, "tenantId", uint64(1))
|
||||||
|
}
|
||||||
|
return service.Audio.Generate(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TTS 文本转语音
|
||||||
|
func (c *audio) TTS(ctx context.Context, req *dto.TTSReq) (res *dto.TTSRes, err error) {
|
||||||
|
return service.Audio.TTS(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStatusOptions 获取状态选项
|
||||||
|
func (c *audio) GetStatusOptions(ctx context.Context, req *dto.GetAudioStatusOptionsReq) (res *dto.GetAudioStatusOptionsRes, err error) {
|
||||||
|
return service.Audio.GetStatusOptions(ctx, req)
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ai-agent/digital-human/model/dto"
|
||||||
|
"ai-agent/digital-human/service"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/beans"
|
||||||
|
"github.com/gogf/gf/v2/util/gconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type customVoice struct{}
|
||||||
|
|
||||||
|
// CustomVoice 自定义音色控制器
|
||||||
|
var CustomVoice = new(customVoice)
|
||||||
|
|
||||||
|
// CreateCustomVoice 创建自定义音色
|
||||||
|
func (c *customVoice) CreateCustomVoice(ctx context.Context, req *dto.CreateCustomVoiceReq) (res *dto.CreateCustomVoiceRes, err error) {
|
||||||
|
// 从上下文获取用户信息
|
||||||
|
if ctx.Value("userId") == nil {
|
||||||
|
ctx = context.WithValue(ctx, "userId", gconv.String(1))
|
||||||
|
}
|
||||||
|
if ctx.Value("userName") == nil {
|
||||||
|
ctx = context.WithValue(ctx, "userName", "admin")
|
||||||
|
}
|
||||||
|
if ctx.Value("tenantId") == nil {
|
||||||
|
ctx = context.WithValue(ctx, "tenantId", uint64(1))
|
||||||
|
}
|
||||||
|
return service.CustomVoice.CreateCustomVoice(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListCustomVoices 获取自定义音色列表
|
||||||
|
func (c *customVoice) ListCustomVoices(ctx context.Context, req *dto.ListCustomVoiceReq) (res *dto.ListCustomVoiceRes, err error) {
|
||||||
|
// 从上下文获取用户信息
|
||||||
|
if ctx.Value("userId") == nil {
|
||||||
|
ctx = context.WithValue(ctx, "userId", gconv.String(1))
|
||||||
|
}
|
||||||
|
if ctx.Value("userName") == nil {
|
||||||
|
ctx = context.WithValue(ctx, "userName", "admin")
|
||||||
|
}
|
||||||
|
if ctx.Value("tenantId") == nil {
|
||||||
|
ctx = context.WithValue(ctx, "tenantId", uint64(1))
|
||||||
|
}
|
||||||
|
return service.CustomVoice.ListCustomVoices(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteCustomVoice 删除自定义音色
|
||||||
|
func (c *customVoice) DeleteCustomVoice(ctx context.Context, req *dto.DeleteCustomVoiceReq) (res *beans.ResponseEmpty, err error) {
|
||||||
|
// 从上下文获取用户信息
|
||||||
|
if ctx.Value("userId") == nil {
|
||||||
|
ctx = context.WithValue(ctx, "userId", gconv.String(1))
|
||||||
|
}
|
||||||
|
if ctx.Value("userName") == nil {
|
||||||
|
ctx = context.WithValue(ctx, "userName", "admin")
|
||||||
|
}
|
||||||
|
if ctx.Value("tenantId") == nil {
|
||||||
|
ctx = context.WithValue(ctx, "tenantId", uint64(1))
|
||||||
|
}
|
||||||
|
err = service.CustomVoice.DeleteCustomVoice(ctx, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ai-agent/digital-human/model/dto"
|
||||||
|
"ai-agent/digital-human/service"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/beans"
|
||||||
|
)
|
||||||
|
|
||||||
|
type digitalhuman struct{}
|
||||||
|
|
||||||
|
// DigitalHuman 数字人形象控制器
|
||||||
|
var DigitalHuman = new(digitalhuman)
|
||||||
|
|
||||||
|
// CreateDigitalHuman 创建数字人形象
|
||||||
|
func (c *digitalhuman) CreateDigitalHuman(ctx context.Context, req *dto.CreateDigitalHumanReq) (res *dto.CreateDigitalHumanRes, err error) {
|
||||||
|
return service.DigitalHuman.Create(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListDigitalHuman 获取数字人形象列表
|
||||||
|
func (c *digitalhuman) ListDigitalHuman(ctx context.Context, req *dto.ListDigitalHumanReq) (res *dto.ListDigitalHumanRes, err error) {
|
||||||
|
return service.DigitalHuman.List(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDigitalHuman 获取数字人形象详情
|
||||||
|
func (c *digitalhuman) GetDigitalHuman(ctx context.Context, req *dto.GetDigitalHumanReq) (res *dto.GetDigitalHumanRes, err error) {
|
||||||
|
return service.DigitalHuman.GetOne(ctx, req.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateDigitalHuman 更新数字人形象
|
||||||
|
func (c *digitalhuman) UpdateDigitalHuman(ctx context.Context, req *dto.UpdateDigitalHumanReq) (res *beans.ResponseEmpty, err error) {
|
||||||
|
err = service.DigitalHuman.Update(ctx, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateDigitalHumanStatus 更新数字人形象状态
|
||||||
|
func (c *digitalhuman) UpdateDigitalHumanStatus(ctx context.Context, req *dto.UpdateDigitalHumanStatusReq) (res *beans.ResponseEmpty, err error) {
|
||||||
|
err = service.DigitalHuman.UpdateStatus(ctx, req.ID, req.Status)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteDigitalHuman 删除数字人形象
|
||||||
|
func (c *digitalhuman) DeleteDigitalHuman(ctx context.Context, req *dto.DeleteDigitalHumanReq) (res *beans.ResponseEmpty, err error) {
|
||||||
|
err = service.DigitalHuman.Delete(ctx, req.ID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDigitalHumanStatusOptions 获取数字人状态选项
|
||||||
|
func (c *digitalhuman) GetDigitalHumanStatusOptions(ctx context.Context, req *dto.GetDigitalHumanStatusOptionsReq) (res *dto.GetDigitalHumanStatusOptionsRes, err error) {
|
||||||
|
return service.DigitalHuman.GetStatusOptions(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetGenderOptions 获取性别选项
|
||||||
|
func (c *digitalhuman) GetGenderOptions(ctx context.Context, req *dto.GetGenderOptionsReq) (res *dto.GetGenderOptionsRes, err error) {
|
||||||
|
return service.DigitalHuman.GetGenderOptions(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAgeOptions 获取年龄段选项
|
||||||
|
func (c *digitalhuman) GetAgeOptions(ctx context.Context, req *dto.GetAgeOptionsReq) (res *dto.GetAgeOptionsRes, err error) {
|
||||||
|
return service.DigitalHuman.GetAgeOptions(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStyleOptions 获取风格选项
|
||||||
|
func (c *digitalhuman) GetStyleOptions(ctx context.Context, req *dto.GetStyleOptionsReq) (res *dto.GetStyleOptionsRes, err error) {
|
||||||
|
return service.DigitalHuman.GetStyleOptions(ctx, req)
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ai-agent/digital-human/model/dto"
|
||||||
|
"ai-agent/digital-human/service"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/beans"
|
||||||
|
)
|
||||||
|
|
||||||
|
type video struct{}
|
||||||
|
|
||||||
|
// Video 视频控制器
|
||||||
|
var Video = new(video)
|
||||||
|
|
||||||
|
// CreateVideo 创建视频
|
||||||
|
func (c *video) CreateVideo(ctx context.Context, req *dto.CreateVideoReq) (res *dto.CreateVideoRes, err error) {
|
||||||
|
return service.Video.Create(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListVideo 获取视频列表
|
||||||
|
func (c *video) ListVideo(ctx context.Context, req *dto.ListVideoReq) (res *dto.ListVideoRes, err error) {
|
||||||
|
return service.Video.List(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetVideo 获取视频详情
|
||||||
|
func (c *video) GetVideo(ctx context.Context, req *dto.GetVideoReq) (res *dto.GetVideoRes, err error) {
|
||||||
|
return service.Video.GetOne(ctx, req.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateVideo 更新视频
|
||||||
|
func (c *video) UpdateVideo(ctx context.Context, req *dto.UpdateVideoReq) (res *beans.ResponseEmpty, err error) {
|
||||||
|
err = service.Video.Update(ctx, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteVideo 删除视频
|
||||||
|
func (c *video) DeleteVideo(ctx context.Context, req *dto.DeleteVideoReq) (res *beans.ResponseEmpty, err error) {
|
||||||
|
err = service.Video.Delete(ctx, req.ID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateVideo 生成视频
|
||||||
|
func (c *video) GenerateVideo(ctx context.Context, req *dto.GenerateVideoReq) (res *dto.GenerateVideoRes, err error) {
|
||||||
|
return service.Video.Generate(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetVideoStatusOptions 获取视频状态选项
|
||||||
|
func (c *video) GetVideoStatusOptions(ctx context.Context, req *dto.GetVideoStatusOptionsReq) (res *dto.GetVideoStatusOptionsRes, err error) {
|
||||||
|
return service.Video.GetStatusOptions(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetResolutionOptions 获取分辨率选项
|
||||||
|
func (c *video) GetResolutionOptions(ctx context.Context, req *dto.GetResolutionOptionsReq) (res *dto.GetResolutionOptionsRes, err error) {
|
||||||
|
return service.Video.GetResolutionOptions(ctx, req)
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package dao
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"ai-agent/digital-human/consts/public"
|
||||||
|
"ai-agent/digital-human/model/entity"
|
||||||
|
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/db/gfdb"
|
||||||
|
"github.com/gogf/gf/v2/database/gdb"
|
||||||
|
)
|
||||||
|
|
||||||
|
var AsyncTaskRef = &asyncTaskRefDao{}
|
||||||
|
|
||||||
|
type asyncTaskRefDao struct{}
|
||||||
|
|
||||||
|
func (d *asyncTaskRefDao) Insert(ctx context.Context, ref *entity.AsyncTaskRef) (id int64, err error) {
|
||||||
|
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameAsyncTaskRef).Data(ref).Insert()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return r.LastInsertId()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListPending 列出待处理任务绑定(state=0/1)
|
||||||
|
func (d *asyncTaskRefDao) ListPending(ctx context.Context, limit int) (list []*entity.AsyncTaskRef, err error) {
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 200
|
||||||
|
}
|
||||||
|
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameAsyncTaskRef).
|
||||||
|
Where("deleted_at IS NULL").
|
||||||
|
// 业务侧只维护三态:生成中/成功/失败;绑定表仅用于“待同步列表”
|
||||||
|
WhereIn(entity.AsyncTaskRefCol.State, []int{0, 1}).
|
||||||
|
OrderAsc(entity.AsyncTaskRefCol.UpdatedAt).
|
||||||
|
Limit(limit).
|
||||||
|
All()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = r.Structs(&list)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *asyncTaskRefDao) UpdateByTaskID(ctx context.Context, taskID string, data gdb.Map) (rows int64, err error) {
|
||||||
|
// 触发 gfdb 的 updateHook 自动填充 updater,需要显式带 updater 字段
|
||||||
|
data[entity.AsyncTaskRefCol.Updater] = ""
|
||||||
|
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameAsyncTaskRef).
|
||||||
|
Where(entity.AsyncTaskRefCol.TaskID, taskID).
|
||||||
|
Data(data).
|
||||||
|
Update()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return r.RowsAffected()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *asyncTaskRefDao) GetByTaskID(ctx context.Context, taskID string) (ref *entity.AsyncTaskRef, err error) {
|
||||||
|
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameAsyncTaskRef).
|
||||||
|
Where(entity.AsyncTaskRefCol.TaskID, taskID).
|
||||||
|
One()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if r.IsEmpty() {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
err = r.Struct(&ref)
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
package dao
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ai-agent/digital-human/consts"
|
||||||
|
"ai-agent/digital-human/consts/public"
|
||||||
|
"ai-agent/digital-human/model/dto"
|
||||||
|
"ai-agent/digital-human/model/entity"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/db/gfdb"
|
||||||
|
"github.com/gogf/gf/v2/database/gdb"
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
"github.com/gogf/gf/v2/util/gconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Audio = &audio{}
|
||||||
|
|
||||||
|
type audio struct{}
|
||||||
|
|
||||||
|
// Insert 插入音频
|
||||||
|
func (d *audio) Insert(ctx context.Context, req *dto.CreateAudioReq) (id int64, err error) {
|
||||||
|
var res *entity.Audio
|
||||||
|
if err = gconv.Struct(req, &res); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameAudio).Data(&res).Insert()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return r.LastInsertId()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update 更新音频
|
||||||
|
func (d *audio) Update(ctx context.Context, id int64, updateData *entity.Audio) (rows int64, err error) {
|
||||||
|
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameAudio).Where(entity.AudioCol.Id, id).Data(&updateData).Update()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return r.RowsAffected()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateStatus 更新音频状态
|
||||||
|
func (d *audio) UpdateStatus(ctx context.Context, id int64, status consts.AudioStatus, errorMsg string, audioURL string, duration int, externalID string) (rows int64, err error) {
|
||||||
|
model := gfdb.DB(ctx).Model(ctx, public.TableNameAudio).Where(entity.AudioCol.Id, id)
|
||||||
|
|
||||||
|
updateData := gdb.Map{
|
||||||
|
entity.AudioCol.Status: int(status),
|
||||||
|
}
|
||||||
|
if errorMsg != "" {
|
||||||
|
updateData[entity.AudioCol.ErrorMsg] = errorMsg
|
||||||
|
}
|
||||||
|
if audioURL != "" {
|
||||||
|
updateData[entity.AudioCol.AudioURL] = audioURL
|
||||||
|
}
|
||||||
|
if duration > 0 {
|
||||||
|
updateData[entity.AudioCol.Duration] = duration
|
||||||
|
}
|
||||||
|
if externalID != "" {
|
||||||
|
updateData[entity.AudioCol.ExternalID] = externalID
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := model.Data(&updateData).Update()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return r.RowsAffected()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete 删除音频
|
||||||
|
func (d *audio) Delete(ctx context.Context, id int64) (rows int64, err error) {
|
||||||
|
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameAudio).Where(entity.AudioCol.Id, id).Delete()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return r.RowsAffected()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOne 获取单个音频
|
||||||
|
func (d *audio) GetOne(ctx context.Context, id int64) (audio *entity.Audio, err error) {
|
||||||
|
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameAudio).Where(entity.AudioCol.Id, id).One()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = r.Struct(&audio)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// List 获取音频列表
|
||||||
|
func (d *audio) List(ctx context.Context, req *dto.ListAudioReq) (res []*entity.Audio, total int, err error) {
|
||||||
|
model := gfdb.DB(ctx).Model(ctx, public.TableNameAudio).OmitEmpty()
|
||||||
|
|
||||||
|
// 构建查询过滤条件
|
||||||
|
if req.Status != consts.AudioStatusGenerating && req.Status != consts.AudioStatusSuccess && req.Status != consts.AudioStatusFailed {
|
||||||
|
// 不添加状态过滤
|
||||||
|
} else {
|
||||||
|
model = model.Where(entity.AudioCol.Status+" = ?", req.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !g.IsEmpty(req.Keyword) {
|
||||||
|
like := "%" + req.Keyword + "%"
|
||||||
|
model = model.Where(
|
||||||
|
"("+entity.AudioCol.Name+
|
||||||
|
" LIKE ? OR "+entity.AudioCol.Description+
|
||||||
|
" LIKE ? OR "+entity.AudioCol.ScriptText+
|
||||||
|
" LIKE ?)",
|
||||||
|
like, like, like,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
model = model.OrderDesc(entity.AudioCol.CreatedAt)
|
||||||
|
|
||||||
|
if req.Page != nil {
|
||||||
|
if req.Page.PageNum <= 0 {
|
||||||
|
req.Page.PageNum = 1
|
||||||
|
}
|
||||||
|
if req.Page.PageSize <= 0 {
|
||||||
|
req.Page.PageSize = 10
|
||||||
|
}
|
||||||
|
model = model.Page(int(req.Page.PageNum), int(req.Page.PageSize))
|
||||||
|
}
|
||||||
|
|
||||||
|
r, total, err := model.AllAndCount(false)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = r.Structs(&res)
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
package dao
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ai-agent/digital-human/consts/public"
|
||||||
|
"ai-agent/digital-human/model/dto"
|
||||||
|
"ai-agent/digital-human/model/entity"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/beans"
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/db/gfdb"
|
||||||
|
"github.com/gogf/gf/v2/database/gdb"
|
||||||
|
"github.com/gogf/gf/v2/util/gconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CustomVoice 自定义音色数据访问层
|
||||||
|
var CustomVoice = &customVoice{}
|
||||||
|
|
||||||
|
type customVoice struct{}
|
||||||
|
|
||||||
|
// Insert 插入自定义音色
|
||||||
|
func (d *customVoice) Insert(ctx context.Context, req *dto.CreateCustomVoiceReq) (id int64, err error) {
|
||||||
|
var result *entity.CustomVoice
|
||||||
|
if err = gconv.Struct(req, &result); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 初始状态:生成中
|
||||||
|
result.Status = 0
|
||||||
|
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameCustomVoice).Data(&result).Insert()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return r.LastInsertId()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *customVoice) UpdateReferenceAudio(ctx context.Context, id int64, referenceAudio []byte) (rows int64, err error) {
|
||||||
|
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameCustomVoice).
|
||||||
|
Where(entity.CustomVoiceCol.Id, id).
|
||||||
|
Data(gdb.Map{
|
||||||
|
entity.CustomVoiceCol.ReferenceAudio: referenceAudio,
|
||||||
|
}).
|
||||||
|
Update()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return r.RowsAffected()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *customVoice) UpdateDescription(ctx context.Context, id int64, description string) (rows int64, err error) {
|
||||||
|
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameCustomVoice).
|
||||||
|
Where(entity.CustomVoiceCol.Id, id).
|
||||||
|
Data(gdb.Map{
|
||||||
|
entity.CustomVoiceCol.Description: description,
|
||||||
|
}).
|
||||||
|
Update()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return r.RowsAffected()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateStatus 更新自定义音色状态/结果
|
||||||
|
func (d *customVoice) UpdateStatus(ctx context.Context, id int64, status int, errorMsg string, ossFile string) (rows int64, err error) {
|
||||||
|
data := gdb.Map{
|
||||||
|
entity.CustomVoiceCol.Status: status,
|
||||||
|
}
|
||||||
|
if errorMsg != "" {
|
||||||
|
data[entity.CustomVoiceCol.ErrorMsg] = errorMsg
|
||||||
|
}
|
||||||
|
if ossFile != "" {
|
||||||
|
data[entity.CustomVoiceCol.OssFile] = ossFile
|
||||||
|
}
|
||||||
|
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameCustomVoice).
|
||||||
|
Where(entity.CustomVoiceCol.Id, id).
|
||||||
|
Data(data).
|
||||||
|
Update()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return r.RowsAffected()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete 删除自定义音色
|
||||||
|
func (d *customVoice) Delete(ctx context.Context, id int64) (rows int64, err error) {
|
||||||
|
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameCustomVoice).Where(entity.CustomVoiceCol.Id, id).Delete()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return r.RowsAffected()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOne 获取单个自定义音色
|
||||||
|
func (d *customVoice) GetOne(ctx context.Context, id int64) (customVoice *entity.CustomVoice, err error) {
|
||||||
|
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameCustomVoice).Where(entity.CustomVoiceCol.Id, id).One()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = r.Struct(&customVoice)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// List 获取自定义音色列表
|
||||||
|
func (d *customVoice) List(ctx context.Context, req *dto.ListCustomVoiceReq) (res []*entity.CustomVoice, total int, err error) {
|
||||||
|
model := gfdb.DB(ctx).Model(ctx, public.TableNameCustomVoice)
|
||||||
|
|
||||||
|
// 处理分页
|
||||||
|
if req.Page == nil {
|
||||||
|
req.Page = &beans.Page{PageNum: 1, PageSize: 20}
|
||||||
|
}
|
||||||
|
|
||||||
|
r, total, err := model.OrderDesc(entity.CustomVoiceCol.CreatedAt).Page(int(req.Page.PageNum), int(req.Page.PageSize)).AllAndCount(false)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = r.Structs(&res)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCustomVoiceItem 转换为 DTO 列表项
|
||||||
|
func (d *customVoice) GetCustomVoiceItem(entity *entity.CustomVoice) *dto.CustomVoiceItem {
|
||||||
|
item := &dto.CustomVoiceItem{
|
||||||
|
ID: gconv.String(entity.Id),
|
||||||
|
Name: entity.Name,
|
||||||
|
Description: entity.Description,
|
||||||
|
Status: entity.Status,
|
||||||
|
ErrorMsg: entity.ErrorMsg,
|
||||||
|
OssFile: entity.OssFile,
|
||||||
|
}
|
||||||
|
if entity.CreatedAt != nil {
|
||||||
|
item.CreatedAt = entity.CreatedAt
|
||||||
|
}
|
||||||
|
if entity.UpdatedAt != nil {
|
||||||
|
item.UpdatedAt = entity.UpdatedAt
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
package dao
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ai-agent/digital-human/consts"
|
||||||
|
"ai-agent/digital-human/consts/public"
|
||||||
|
"ai-agent/digital-human/model/dto"
|
||||||
|
"ai-agent/digital-human/model/entity"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/db/gfdb"
|
||||||
|
"github.com/gogf/gf/v2/database/gdb"
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
"github.com/gogf/gf/v2/util/gconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DigitalHuman 数字人形象数据
|
||||||
|
var DigitalHuman = &digitalHuman{}
|
||||||
|
|
||||||
|
type digitalHuman struct{}
|
||||||
|
|
||||||
|
// Insert 插入数字人形象
|
||||||
|
func (d *digitalHuman) Insert(ctx context.Context, req *dto.CreateDigitalHumanReq) (ids []any, err error) {
|
||||||
|
var result entity.DigitalHuman
|
||||||
|
if err = gconv.Struct(req, &result); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameDigitalHuman).Data(&result).Insert()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lastInsertId, err := r.LastInsertId()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ids = []any{lastInsertId}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update 更新数字人形象
|
||||||
|
func (d *digitalHuman) Update(ctx context.Context, id int64, updateData *entity.DigitalHuman) (err error) {
|
||||||
|
_, err = gfdb.DB(ctx).Model(ctx, public.TableNameDigitalHuman).Data(updateData).Where("id = ?", id).Update()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateStatus 更新数字人形象状态
|
||||||
|
func (d *digitalHuman) UpdateStatus(ctx context.Context, id int64, status consts.DigitalHumanStatus) (rows int64, err error) {
|
||||||
|
model := gfdb.DB(ctx).Model(ctx, public.TableNameDigitalHuman).Where("id = ?", id)
|
||||||
|
r, err := model.Data(g.Map{"status": status}).Update()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return r.RowsAffected()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete 删除数字人形象
|
||||||
|
func (d *digitalHuman) Delete(ctx context.Context, id int64) (err error) {
|
||||||
|
_, err = gfdb.DB(ctx).Model(ctx, public.TableNameDigitalHuman).Where("id = ?", id).Delete()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOne 获取单个数字人形象
|
||||||
|
func (d *digitalHuman) GetOne(ctx context.Context, id int64) (digitalHuman *entity.DigitalHuman, err error) {
|
||||||
|
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameDigitalHuman).Where("id = ?", id).One()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = r.Struct(&digitalHuman)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// List 获取数字人形象列表
|
||||||
|
func (d *digitalHuman) List(ctx context.Context, req *dto.ListDigitalHumanReq) (res []entity.DigitalHuman, total int64, err error) {
|
||||||
|
model := d.buildListFilter(ctx, req)
|
||||||
|
|
||||||
|
var totalCount int
|
||||||
|
totalCount, err = model.Count()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
total = gconv.Int64(totalCount)
|
||||||
|
|
||||||
|
if req.Page != nil {
|
||||||
|
model.Page(int(req.Page.PageNum), int(req.Page.PageSize))
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := model.OrderDesc("id").All()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = r.Structs(&res)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildListFilter 构建列表查询的过滤条件
|
||||||
|
func (d *digitalHuman) buildListFilter(ctx context.Context, req *dto.ListDigitalHumanReq) *gdb.Model {
|
||||||
|
model := gfdb.DB(ctx).Model(ctx, public.TableNameDigitalHuman).OmitEmpty()
|
||||||
|
// 状态字段允许查询所有状态值,包括0(停用),所以需要特别处理
|
||||||
|
if req.Status != consts.DigitalHumanStatusInactive && req.Status != consts.DigitalHumanStatusActive {
|
||||||
|
// 如果状态不是有效值之一,则不添加状态过滤条件
|
||||||
|
} else {
|
||||||
|
model = model.Where("status = ?", req.Status)
|
||||||
|
}
|
||||||
|
if !g.IsEmpty(req.Gender) {
|
||||||
|
model = model.Where("gender = ?", req.Gender)
|
||||||
|
}
|
||||||
|
if !g.IsEmpty(req.Style) {
|
||||||
|
model = model.Where("style = ?", req.Style)
|
||||||
|
}
|
||||||
|
if !g.IsEmpty(req.Keyword) {
|
||||||
|
model = model.Where("name LIKE ? OR description LIKE ?", "%"+req.Keyword+"%", "%"+req.Keyword+"%")
|
||||||
|
}
|
||||||
|
|
||||||
|
return model
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count 计数
|
||||||
|
func (d *digitalHuman) Count(ctx context.Context, req *dto.CreateDigitalHumanReq) (count int64, err error) {
|
||||||
|
var totalCount int
|
||||||
|
totalCount, err = gfdb.DB(ctx).Model(ctx, public.TableNameDigitalHuman).Where("name = ?", req.Name).Count()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
count = gconv.Int64(totalCount)
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
package dao
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ai-agent/digital-human/consts"
|
||||||
|
"ai-agent/digital-human/consts/public"
|
||||||
|
"ai-agent/digital-human/model/dto"
|
||||||
|
"ai-agent/digital-human/model/entity"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/db/gfdb"
|
||||||
|
"github.com/gogf/gf/v2/database/gdb"
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
"github.com/gogf/gf/v2/util/gconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Video 视频数据
|
||||||
|
var Video = &video{}
|
||||||
|
|
||||||
|
type video struct{}
|
||||||
|
|
||||||
|
// Insert 插入视频
|
||||||
|
func (d *video) Insert(ctx context.Context, req *dto.CreateVideoReq) (ids []any, err error) {
|
||||||
|
var result entity.Video
|
||||||
|
if err = gconv.Struct(req, &result); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameVideo).Data(&result).Insert()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lastInsertId, err := r.LastInsertId()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ids = []any{lastInsertId}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update 更新视频
|
||||||
|
func (d *video) Update(ctx context.Context, id int64, updateData *entity.Video) (err error) {
|
||||||
|
_, err = gfdb.DB(ctx).Model(ctx, public.TableNameVideo).Data(updateData).Where("id = ?", id).Update()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete 删除视频
|
||||||
|
func (d *video) Delete(ctx context.Context, id int64) (err error) {
|
||||||
|
_, err = gfdb.DB(ctx).Model(ctx, public.TableNameVideo).Where("id = ?", id).Delete()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOne 获取单个视频
|
||||||
|
func (d *video) GetOne(ctx context.Context, id int64) (video *entity.Video, err error) {
|
||||||
|
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameVideo).Where("id = ?", id).One()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = r.Struct(&video)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// List 获取视频列表
|
||||||
|
func (d *video) List(ctx context.Context, req *dto.ListVideoReq) (res []entity.Video, total int64, err error) {
|
||||||
|
model := d.buildListFilter(ctx, req)
|
||||||
|
|
||||||
|
var totalCount int
|
||||||
|
totalCount, err = model.Count()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
total = gconv.Int64(totalCount)
|
||||||
|
|
||||||
|
if req.Page != nil {
|
||||||
|
model.Page(int(req.Page.PageNum), int(req.Page.PageSize))
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := model.OrderDesc("id").All()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = r.Structs(&res)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildListFilter 构建列表查询的过滤条件
|
||||||
|
func (d *video) buildListFilter(ctx context.Context, req *dto.ListVideoReq) *gdb.Model {
|
||||||
|
model := gfdb.DB(ctx).Model(ctx, public.TableNameVideo).OmitEmpty()
|
||||||
|
// 状态字段允许查询所有状态值,包括0(生成中),所以需要特别处理
|
||||||
|
if req.Status != consts.VideoStatusGenerating && req.Status != consts.VideoStatusSuccess && req.Status != consts.VideoStatusFailed {
|
||||||
|
// 如果状态不是有效值之一,则不添加状态过滤条件
|
||||||
|
} else {
|
||||||
|
model = model.Where("status = ?", req.Status)
|
||||||
|
}
|
||||||
|
if !g.IsEmpty(req.DigitalHumanID) {
|
||||||
|
model = model.Where("digitalHumanId = ?", req.DigitalHumanID)
|
||||||
|
}
|
||||||
|
if !g.IsEmpty(req.Keyword) {
|
||||||
|
model = model.Where("name LIKE ? OR description LIKE ?", "%"+req.Keyword+"%", "%"+req.Keyword+"%")
|
||||||
|
}
|
||||||
|
|
||||||
|
return model
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateStatus 更新视频状态
|
||||||
|
func (d *video) UpdateStatus(ctx context.Context, id int64, status consts.VideoStatus, errorMsg string, videoURL string, duration int, thumbnailURL string, externalTaskID string) (rows int64, err error) {
|
||||||
|
model := gfdb.DB(ctx).Model(ctx, public.TableNameVideo).Where("id = ?", id)
|
||||||
|
|
||||||
|
updateData := gdb.Map{
|
||||||
|
"status": status,
|
||||||
|
}
|
||||||
|
if errorMsg != "" {
|
||||||
|
updateData["errorMsg"] = errorMsg
|
||||||
|
}
|
||||||
|
if videoURL != "" {
|
||||||
|
updateData["videoUrl"] = videoURL
|
||||||
|
}
|
||||||
|
if duration > 0 {
|
||||||
|
updateData["duration"] = duration
|
||||||
|
}
|
||||||
|
if thumbnailURL != "" {
|
||||||
|
updateData["thumbnailUrl"] = thumbnailURL
|
||||||
|
}
|
||||||
|
if externalTaskID != "" {
|
||||||
|
updateData["externalTaskId"] = externalTaskID
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := model.Data(updateData).Update()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return r.RowsAffected()
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import "github.com/gogf/gf/v2/frame/g"
|
||||||
|
|
||||||
|
// SyncAsyncTasksReq 定时任务/业务侧轮询使用:同步处理中间件任务状态并转移结果
|
||||||
|
type SyncAsyncTasksReq struct {
|
||||||
|
g.Meta `path:"/syncAsyncTasks" method:"post" tags:"异步任务" summary:"同步异步任务" dc:"扫描本服务待处理任务(task_id),批量查询 model-asynch 状态,成功则转移OSS并更新业务表"`
|
||||||
|
Limit int `p:"limit" json:"limit" dc:"单次处理上限(默认200)"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SyncAsyncTasksItem struct {
|
||||||
|
TaskID string `json:"taskId"`
|
||||||
|
State int `json:"state"`
|
||||||
|
TableName string `json:"tableName"`
|
||||||
|
BizID string `json:"bizId"`
|
||||||
|
OssFile string `json:"ossFile"`
|
||||||
|
ErrorMsg string `json:"errorMsg"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SyncAsyncTasksRes struct {
|
||||||
|
Total int `json:"total" dc:"本次扫描到的任务数"`
|
||||||
|
Handled int `json:"handled" dc:"本次成功处理数(含更新状态/转移)"`
|
||||||
|
List []SyncAsyncTasksItem `json:"list" dc:"任务明细"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ai-agent/digital-human/consts"
|
||||||
|
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/beans"
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
"github.com/gogf/gf/v2/os/gtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CreateAudioReq 创建音频请求
|
||||||
|
type CreateAudioReq struct {
|
||||||
|
g.Meta `path:"/createAudio" method:"post" tags:"音频管理" summary:"创建音频" dc:"创建新的音频"`
|
||||||
|
// 基础信息
|
||||||
|
Name string `json:"name" v:"required" dc:"音频名称"`
|
||||||
|
Description string `json:"description" dc:"音频描述"`
|
||||||
|
ScriptText string `json:"scriptText" v:"required" dc:"话术文本"`
|
||||||
|
// 音色配置
|
||||||
|
Voice string `json:"voice" dc:"音色:serena/vivian/uncle_fu/ryan/aiden/ono_anna/sohee/eric/dylan,默认serena"`
|
||||||
|
VoiceType string `json:"voiceType" dc:"音色类型:preset/custom(预设/自定义),默认preset"`
|
||||||
|
CustomVoice string `json:"customVoice" dc:"自定义音色ID(用于声音克隆),voiceType=custom时必填"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateAudioRes 创建音频响应
|
||||||
|
type CreateAudioRes struct {
|
||||||
|
Id int64 `json:"id" dc:"音频ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAudioReq 获取音频列表请求
|
||||||
|
type ListAudioReq struct {
|
||||||
|
g.Meta `path:"/listAudios" method:"get" tags:"音频管理" summary:"获取音频列表" dc:"分页查询音频列表,支持多条件筛选"`
|
||||||
|
*beans.Page
|
||||||
|
Status consts.AudioStatus `json:"status" dc:"状态:0生成中/1成功/2失败"`
|
||||||
|
Keyword string `json:"keyword" dc:"关键词搜索"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAudioRes 获取音频列表响应
|
||||||
|
type ListAudioRes struct {
|
||||||
|
List []*AudioListItem `json:"list" dc:"音频列表"`
|
||||||
|
Total int64 `json:"total" dc:"总数"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AudioListItem 音频列表项
|
||||||
|
type AudioListItem struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
ScriptText string `json:"scriptText"`
|
||||||
|
AudioURL string `json:"audioUrl"`
|
||||||
|
Status consts.AudioStatus `json:"status"`
|
||||||
|
ErrorMsg string `json:"errorMsg"`
|
||||||
|
Duration int `json:"duration"`
|
||||||
|
ExternalID string `json:"externalId"`
|
||||||
|
Voice string `json:"voice"`
|
||||||
|
VoiceType string `json:"voiceType"`
|
||||||
|
CustomVoice string `json:"customVoice"`
|
||||||
|
CreatedAt *gtime.Time `json:"createdAt"`
|
||||||
|
UpdatedAt *gtime.Time `json:"updatedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAudioReq 获取音频详情请求
|
||||||
|
type GetAudioReq struct {
|
||||||
|
g.Meta `path:"/getAudio" method:"get" tags:"音频管理" summary:"获取音频详情" dc:"获取音频详情"`
|
||||||
|
ID int64 `json:"id" v:"required" dc:"音频ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAudioRes 获取音频详情响应
|
||||||
|
type GetAudioRes struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
ScriptText string `json:"scriptText"`
|
||||||
|
AudioURL string `json:"audioUrl"`
|
||||||
|
Status consts.AudioStatus `json:"status"`
|
||||||
|
ErrorMsg string `json:"errorMsg"`
|
||||||
|
Duration int `json:"duration"`
|
||||||
|
ExternalID string `json:"externalId"`
|
||||||
|
Voice string `json:"voice"`
|
||||||
|
VoiceType string `json:"voiceType"`
|
||||||
|
CustomVoice string `json:"customVoice"`
|
||||||
|
CreatedAt *gtime.Time `json:"createdAt"`
|
||||||
|
UpdatedAt *gtime.Time `json:"updatedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateAudioReq 更新音频请求
|
||||||
|
type UpdateAudioReq struct {
|
||||||
|
g.Meta `path:"/updateAudio" method:"put" tags:"音频管理" summary:"更新音频" dc:"更新音频信息"`
|
||||||
|
ID int64 `json:"id" v:"required" dc:"音频ID"`
|
||||||
|
// 基础信息
|
||||||
|
Name string `json:"name" dc:"音频名称"`
|
||||||
|
Description string `json:"description" dc:"音频描述"`
|
||||||
|
// 音色配置
|
||||||
|
Voice string `json:"voice" dc:"音色"`
|
||||||
|
VoiceType string `json:"voiceType" dc:"音色类型"`
|
||||||
|
CustomVoice string `json:"customVoice" dc:"自定义音色ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteAudioReq 删除音频请求
|
||||||
|
type DeleteAudioReq struct {
|
||||||
|
g.Meta `path:"/deleteAudio" method:"delete" tags:"音频管理" summary:"删除音频" dc:"删除音频"`
|
||||||
|
ID int64 `json:"id" v:"required" dc:"音频ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateAudioReq 生成音频请求
|
||||||
|
type GenerateAudioReq struct {
|
||||||
|
g.Meta `path:"/generateAudio" method:"post" tags:"音频管理" summary:"生成音频" dc:"根据话术文本生成音频"`
|
||||||
|
ID int64 `json:"id" v:"required" dc:"音频ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateAudioRes 生成音频响应
|
||||||
|
type GenerateAudioRes struct {
|
||||||
|
TaskID string `json:"taskId" dc:"任务ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TTSReq 文本转语音请求
|
||||||
|
type TTSReq struct {
|
||||||
|
g.Meta `path:"/tts" method:"post" tags:"音频管理" summary:"文本转语音" dc:"将文本转换为语音,直接返回MP3二进制数据"`
|
||||||
|
Text string `json:"text" v:"required" dc:"要转换的文本内容"`
|
||||||
|
Voice string `json:"voice" dc:"音色:默认default"`
|
||||||
|
Speed int `json:"speed" dc:"语速:0.5-2.0,默认1.0"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TTSRes 文本转语音响应(返回二进制MP3数据)
|
||||||
|
type TTSRes struct {
|
||||||
|
g.Meta `mime:"audio/mpeg"`
|
||||||
|
Data []byte `json:"-" dc:"MP3音频二进制数据"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAudioStatusOptionsReq 获取音频状态选项请求
|
||||||
|
type GetAudioStatusOptionsReq struct {
|
||||||
|
g.Meta `path:"/getAudioStatusOptions" method:"get" tags:"音频管理" summary:"获取音频状态选项" dc:"获取所有音频状态的选项列表"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAudioStatusOptionsRes 获取音频状态选项响应
|
||||||
|
type GetAudioStatusOptionsRes struct {
|
||||||
|
Options []consts.AudioStatusKeyValue `json:"options" dc:"音频状态选项列表"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Qwen3TTSRequest Qwen3-TTS 请求结构
|
||||||
|
type Qwen3TTSRequest struct {
|
||||||
|
Text string `json:"text"`
|
||||||
|
Speaker string `json:"speaker"` // 预设音色名或克隆音色ID
|
||||||
|
VoiceID string `json:"voice_id,omitempty"` // 克隆音色ID(可选)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Qwen3TTSResponse Qwen3-TTS 响应结构
|
||||||
|
type Qwen3TTSResponse struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
Audio string `json:"audio"` // base64编码的音频数据
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/beans"
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
"github.com/gogf/gf/v2/os/gtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CreateCustomVoiceReq 创建自定义音色请求
|
||||||
|
type CreateCustomVoiceReq struct {
|
||||||
|
g.Meta `path:"/createCustomVoice" method:"post" tags:"自定义音色" summary:"创建自定义音色" dc:"上传参考音频创建自定义音色"`
|
||||||
|
VoiceType string `json:"voiceType" v:"required" dc:"音色类型 design/clone(设计/克隆)"`
|
||||||
|
Name string `json:"name" v:"required" dc:"音色名称"`
|
||||||
|
Description string `json:"description" dc:"音色描述"`
|
||||||
|
Text string `json:"text" dc:"参考文本"`
|
||||||
|
// 参考音频
|
||||||
|
ReferenceAudio []byte `json:"referenceAudio" dc:"参考音频数据(base64编码的WAV文件,voiceType=clone 时必填)"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateCustomVoiceRes 创建自定义音色响应
|
||||||
|
type CreateCustomVoiceRes struct {
|
||||||
|
VoiceID string `json:"voiceId" dc:"音色ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListCustomVoiceReq 获取自定义音色列表请求
|
||||||
|
type ListCustomVoiceReq struct {
|
||||||
|
g.Meta `path:"/listCustomVoices" method:"get" tags:"自定义音色" summary:"获取自定义音色列表" dc:"分页查询自定义音色列表"`
|
||||||
|
*beans.Page
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListCustomVoiceRes 获取自定义音色列表响应
|
||||||
|
type ListCustomVoiceRes struct {
|
||||||
|
List []*CustomVoiceItem `json:"list" dc:"自定义音色列表"`
|
||||||
|
Total int64 `json:"total" dc:"总数"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CustomVoiceItem 自定义音色列表项
|
||||||
|
type CustomVoiceItem struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Status int `json:"status" dc:"状态:0生成中/1成功/2失败"`
|
||||||
|
ErrorMsg string `json:"errorMsg" dc:"错误信息"`
|
||||||
|
OssFile string `json:"ossFile" dc:"结果文件OSS地址"`
|
||||||
|
CreatedAt *gtime.Time `json:"createdAt"`
|
||||||
|
UpdatedAt *gtime.Time `json:"updatedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteCustomVoiceReq 删除自定义音色请求
|
||||||
|
type DeleteCustomVoiceReq struct {
|
||||||
|
g.Meta `path:"/deleteCustomVoice" method:"delete" tags:"自定义音色" summary:"删除自定义音色" dc:"删除自定义音色"`
|
||||||
|
VoiceID string `json:"voiceId" v:"required" dc:"音色ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Qwen3VoiceCloneRequest Qwen3-TTS 音色克隆请求
|
||||||
|
type Qwen3VoiceCloneRequest struct {
|
||||||
|
Name string `json:"name"` // 音色名称
|
||||||
|
Audio string `json:"audio"` // base64编码的参考音频
|
||||||
|
Text string `json:"text"` // 参考文本(可选)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Qwen3VoiceCloneResponse Qwen3-TTS 音色克隆响应
|
||||||
|
type Qwen3VoiceCloneResponse struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
VoiceID string `json:"voice_id"` // 克隆后的音色ID
|
||||||
|
}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ai-agent/digital-human/consts"
|
||||||
|
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/beans"
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
"github.com/gogf/gf/v2/os/gtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CreateDigitalHumanReq 创建数字人形象请求
|
||||||
|
type CreateDigitalHumanReq struct {
|
||||||
|
g.Meta `path:"/createDigitalHuman" method:"post" tags:"数字人形象管理" summary:"创建数字人形象" dc:"创建新的数字人形象"`
|
||||||
|
// 基础信息
|
||||||
|
Name string `json:"name" v:"required" dc:"数字人名称"`
|
||||||
|
Description string `json:"description" dc:"数字人描述"`
|
||||||
|
ImageURL string `json:"imageUrl" dc:"形象图片URL"`
|
||||||
|
VideoURL string `json:"videoUrl" dc:"形象视频URL"`
|
||||||
|
Status consts.DigitalHumanStatus `json:"status" dc:"状态:1启用/0停用" d:"1"`
|
||||||
|
Tags []string `json:"tags" dc:"标签"`
|
||||||
|
Gender consts.Gender `json:"gender" dc:"性别"`
|
||||||
|
Age consts.Age `json:"age" dc:"年龄段"`
|
||||||
|
Style consts.Style `json:"style" dc:"风格:商务/休闲/正式等"`
|
||||||
|
ExternalID string `json:"externalId" dc:"外部系统ID"`
|
||||||
|
Metadata []map[string]interface{} `json:"metadata" dc:"动态元数据"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateDigitalHumanRes 创建数字人形象响应
|
||||||
|
type CreateDigitalHumanRes struct {
|
||||||
|
Id int64 `json:"id" dc:"数字人形象ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListDigitalHumanReq 获取数字人形象列表请求
|
||||||
|
type ListDigitalHumanReq struct {
|
||||||
|
g.Meta `path:"/listDigitalHumans" method:"get" tags:"数字人形象管理" summary:"获取数字人形象列表" dc:"分页查询数字人形象列表,支持多条件筛选"`
|
||||||
|
*beans.Page
|
||||||
|
Status consts.DigitalHumanStatus `json:"status" dc:"状态"`
|
||||||
|
Gender consts.Gender `json:"gender" dc:"性别"`
|
||||||
|
Style consts.Style `json:"style" dc:"风格"`
|
||||||
|
Keyword string `json:"keyword" dc:"关键词搜索"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListDigitalHumanRes 获取数字人形象列表响应
|
||||||
|
type ListDigitalHumanRes struct {
|
||||||
|
List []*DigitalHumanListItem `json:"list" dc:"数字人形象列表"`
|
||||||
|
Total int64 `json:"total" dc:"总数"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DigitalHumanListItem 数字人形象列表项
|
||||||
|
type DigitalHumanListItem struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
ImageURL string `json:"imageUrl"`
|
||||||
|
VideoURL string `json:"videoUrl"`
|
||||||
|
Status consts.DigitalHumanStatus `json:"status"`
|
||||||
|
Tags []string `json:"tags"`
|
||||||
|
Gender consts.Gender `json:"gender"`
|
||||||
|
Age consts.Age `json:"age"`
|
||||||
|
Style consts.Style `json:"style"`
|
||||||
|
CreatedAt *gtime.Time `json:"createdAt"`
|
||||||
|
UpdatedAt *gtime.Time `json:"updatedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDigitalHumanReq 获取数字人形象详情请求
|
||||||
|
type GetDigitalHumanReq struct {
|
||||||
|
g.Meta `path:"/getDigitalHuman" method:"get" tags:"数字人形象管理" summary:"获取数字人形象详情" dc:"获取数字人形象详情"`
|
||||||
|
ID int64 `json:"id" v:"required" dc:"数字人形象ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDigitalHumanRes 获取数字人形象详情响应
|
||||||
|
type GetDigitalHumanRes struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
ImageURL string `json:"imageUrl"`
|
||||||
|
VideoURL string `json:"videoUrl"`
|
||||||
|
Status consts.DigitalHumanStatus `json:"status"`
|
||||||
|
Tags []string `json:"tags"`
|
||||||
|
Gender consts.Gender `json:"gender"`
|
||||||
|
Age consts.Age `json:"age"`
|
||||||
|
Style consts.Style `json:"style"`
|
||||||
|
ExternalID string `json:"externalId"`
|
||||||
|
Metadata []map[string]interface{} `json:"metadata"`
|
||||||
|
CreatedAt *gtime.Time `json:"createdAt"`
|
||||||
|
UpdatedAt *gtime.Time `json:"updatedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateDigitalHumanReq 更新数字人形象请求
|
||||||
|
type UpdateDigitalHumanReq struct {
|
||||||
|
g.Meta `path:"/updateDigitalHuman" method:"put" tags:"数字人形象管理" summary:"更新数字人形象" dc:"更新数字人形象信息"`
|
||||||
|
ID int64 `json:"id" v:"required" dc:"数字人形象ID"`
|
||||||
|
// 基础信息
|
||||||
|
Name string `json:"name" dc:"数字人名称"`
|
||||||
|
Description string `json:"description" dc:"数字人描述"`
|
||||||
|
ImageURL string `json:"imageUrl" dc:"形象图片URL"`
|
||||||
|
VideoURL string `json:"videoUrl" dc:"形象视频URL"`
|
||||||
|
Status consts.DigitalHumanStatus `json:"status" dc:"状态:1启用/0停用"`
|
||||||
|
Tags []string `json:"tags" dc:"标签"`
|
||||||
|
Gender consts.Gender `json:"gender" dc:"性别"`
|
||||||
|
Age consts.Age `json:"age" dc:"年龄段"`
|
||||||
|
Style consts.Style `json:"style" dc:"风格:商务/休闲/正式等"`
|
||||||
|
ExternalID string `json:"externalId" dc:"外部系统ID"`
|
||||||
|
Metadata []map[string]interface{} `json:"metadata" dc:"动态元数据"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateDigitalHumanStatusReq 更新数字人形象状态请求
|
||||||
|
type UpdateDigitalHumanStatusReq struct {
|
||||||
|
g.Meta `path:"/updateDigitalHumanStatus" method:"put" tags:"数字人形象管理" summary:"更新数字人形象状态" dc:"更新数字人形象状态"`
|
||||||
|
ID int64 `json:"id" v:"required" dc:"数字人形象ID"`
|
||||||
|
Status consts.DigitalHumanStatus `json:"status" v:"required|in:1,0" dc:"状态:1启用/0停用"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteDigitalHumanReq 删除数字人形象请求
|
||||||
|
type DeleteDigitalHumanReq struct {
|
||||||
|
g.Meta `path:"/deleteDigitalHuman" method:"delete" tags:"数字人形象管理" summary:"删除数字人形象" dc:"删除数字人形象"`
|
||||||
|
ID int64 `json:"id" v:"required" dc:"数字人形象ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDigitalHumanStatusOptionsReq 获取数字人状态选项请求
|
||||||
|
type GetDigitalHumanStatusOptionsReq struct {
|
||||||
|
g.Meta `path:"/getDigitalHumanStatusOptions" method:"get" tags:"数字人形象管理" summary:"获取数字人状态选项" dc:"获取所有数字人状态的选项列表"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDigitalHumanStatusOptionsRes 获取数字人状态选项响应
|
||||||
|
type GetDigitalHumanStatusOptionsRes struct {
|
||||||
|
Options []consts.StatusKeyValue `json:"options" dc:"状态选项列表"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetGenderOptionsReq 获取性别选项请求
|
||||||
|
type GetGenderOptionsReq struct {
|
||||||
|
g.Meta `path:"/getGenderOptions" method:"get" tags:"数字人形象管理" summary:"获取性别选项" dc:"获取所有性别选项列表"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetGenderOptionsRes 获取性别选项响应
|
||||||
|
type GetGenderOptionsRes struct {
|
||||||
|
Options []consts.GenderKeyValue `json:"options" dc:"性别选项列表"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAgeOptionsReq 获取年龄段选项请求
|
||||||
|
type GetAgeOptionsReq struct {
|
||||||
|
g.Meta `path:"/getAgeOptions" method:"get" tags:"数字人形象管理" summary:"获取年龄段选项" dc:"获取所有年龄段选项列表"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAgeOptionsRes 获取年龄段选项响应
|
||||||
|
type GetAgeOptionsRes struct {
|
||||||
|
Options []consts.AgeKeyValue `json:"options" dc:"年龄段选项列表"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStyleOptionsReq 获取风格选项请求
|
||||||
|
type GetStyleOptionsReq struct {
|
||||||
|
g.Meta `path:"/getStyleOptions" method:"get" tags:"数字人形象管理" summary:"获取风格选项" dc:"获取所有风格选项列表"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStyleOptionsRes 获取风格选项响应
|
||||||
|
type GetStyleOptionsRes struct {
|
||||||
|
Options []consts.StyleKeyValue `json:"options" dc:"风格选项列表"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/beans"
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestModelStreamReq 测试模型流式调用请求(流式调用上游 → 全量一次返回)
|
||||||
|
type TestModelStreamReq struct {
|
||||||
|
g.Meta `path:"/testModelStream" method:"post" tags:"模型流测试" summary:"测试模型流式调用" dc:"透传调用model-gateway的流式接口(上游流式调用,缓冲后全量一次返回)"`
|
||||||
|
ModelName string `json:"modelName" v:"required#modelName不能为空" dc:"模型名称"`
|
||||||
|
BizName string `json:"bizName" dc:"业务名称"`
|
||||||
|
SessionId string `json:"sessionId" v:"required#sessionId不能为空" dc:"会话ID"`
|
||||||
|
RequestParams map[string]any `json:"requestParams" v:"required#请求参数不能为空" dc:"模型请求参数"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestModelStreamAAReq 测试模型流式调用请求(流式调用上游 → SSE 分块返回)
|
||||||
|
type TestModelStreamAAReq struct {
|
||||||
|
g.Meta `path:"/testModelStreamAA" method:"post" tags:"模型流测试" summary:"测试模型流式调用AA" dc:"透传调用model-gateway的SSE流式接口(上游流式调用,逐分片SSE返回)"`
|
||||||
|
ModelName string `json:"modelName" v:"required#modelName不能为空" dc:"模型名称"`
|
||||||
|
BizName string `json:"bizName" dc:"业务名称"`
|
||||||
|
SessionId string `json:"sessionId" v:"required#sessionId不能为空" dc:"会话ID"`
|
||||||
|
RequestParams map[string]any `json:"requestParams" v:"required#请求参数不能为空" dc:"模型请求参数"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestModelStreamRes 测试模型流式调用响应(实际通过RawWriter直接写出,该结构体仅占位)
|
||||||
|
type TestModelStreamRes struct {
|
||||||
|
*beans.ResponseEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestModelStreamAARes 测试模型流式调用AA响应(实际通过RawWriter直接写出,该结构体仅占位)
|
||||||
|
type TestModelStreamAARes struct {
|
||||||
|
*beans.ResponseEmpty
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ai-agent/digital-human/consts"
|
||||||
|
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/beans"
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
"github.com/gogf/gf/v2/os/gtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CreateVideoReq 创建视频请求
|
||||||
|
type CreateVideoReq struct {
|
||||||
|
g.Meta `path:"/createVideo" method:"post" tags:"视频管理" summary:"创建视频" dc:"创建新的视频任务"`
|
||||||
|
// 基础信息
|
||||||
|
Name string `json:"name" v:"required" dc:"视频名称"`
|
||||||
|
Description string `json:"description" dc:"视频描述"`
|
||||||
|
DigitalHumanID int64 `json:"digitalHumanId" v:"required" dc:"数字人形象ID"`
|
||||||
|
AudioID int64 `json:"audioId" v:"required" dc:"音频ID"`
|
||||||
|
Resolution consts.Resolution `json:"resolution" dc:"分辨率:480p/720p/1080p/2k/4k/8k"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateVideoRes 创建视频响应
|
||||||
|
type CreateVideoRes struct {
|
||||||
|
Id int64 `json:"id" dc:"视频ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListVideoReq 获取视频列表请求
|
||||||
|
type ListVideoReq struct {
|
||||||
|
g.Meta `path:"/listVideos" method:"get" tags:"视频管理" summary:"获取视频列表" dc:"分页查询视频列表,支持多条件筛选"`
|
||||||
|
*beans.Page
|
||||||
|
Status consts.VideoStatus `json:"status" dc:"状态:0生成中/1成功/2失败"`
|
||||||
|
DigitalHumanID int64 `json:"digitalHumanId" dc:"数字人形象ID"`
|
||||||
|
Keyword string `json:"keyword" dc:"关键词搜索"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListVideoRes 获取视频列表响应
|
||||||
|
type ListVideoRes struct {
|
||||||
|
List []*VideoListItem `json:"list" dc:"视频列表"`
|
||||||
|
Total int64 `json:"total" dc:"总数"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// VideoListItem 视频列表项
|
||||||
|
type VideoListItem struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
DigitalHumanID int64 `json:"digitalHumanId"`
|
||||||
|
DigitalHumanName string `json:"digitalHumanName"`
|
||||||
|
AudioID int64 `json:"audioId"`
|
||||||
|
AudioURL string `json:"audioUrl"`
|
||||||
|
VideoURL string `json:"videoUrl"`
|
||||||
|
Status consts.VideoStatus `json:"status"`
|
||||||
|
ErrorMsg string `json:"errorMsg"`
|
||||||
|
Duration int `json:"duration"`
|
||||||
|
Resolution consts.Resolution `json:"resolution"`
|
||||||
|
ThumbnailURL string `json:"thumbnailUrl"`
|
||||||
|
CreatedAt *gtime.Time `json:"createdAt"`
|
||||||
|
UpdatedAt *gtime.Time `json:"updatedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetVideoReq 获取视频详情请求
|
||||||
|
type GetVideoReq struct {
|
||||||
|
g.Meta `path:"/getVideo" method:"get" tags:"视频管理" summary:"获取视频详情" dc:"获取视频详情"`
|
||||||
|
ID int64 `json:"id" v:"required" dc:"视频ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetVideoRes 获取视频详情响应
|
||||||
|
type GetVideoRes struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
DigitalHumanID int64 `json:"digitalHumanId"`
|
||||||
|
DigitalHumanName string `json:"digitalHumanName"`
|
||||||
|
AudioID int64 `json:"audioId"`
|
||||||
|
AudioURL string `json:"audioUrl"`
|
||||||
|
VideoURL string `json:"videoUrl"`
|
||||||
|
Status consts.VideoStatus `json:"status"`
|
||||||
|
ErrorMsg string `json:"errorMsg"`
|
||||||
|
Duration int `json:"duration"`
|
||||||
|
Resolution consts.Resolution `json:"resolution"`
|
||||||
|
ThumbnailURL string `json:"thumbnailUrl"`
|
||||||
|
ExternalTaskID string `json:"externalTaskId"`
|
||||||
|
CreatedAt *gtime.Time `json:"createdAt"`
|
||||||
|
UpdatedAt *gtime.Time `json:"updatedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateVideoReq 更新视频请求
|
||||||
|
type UpdateVideoReq struct {
|
||||||
|
g.Meta `path:"/updateVideo" method:"put" tags:"视频管理" summary:"更新视频" dc:"更新视频信息"`
|
||||||
|
ID int64 `json:"id" v:"required" dc:"视频ID"`
|
||||||
|
// 基础信息
|
||||||
|
Name string `json:"name" dc:"视频名称"`
|
||||||
|
Description string `json:"description" dc:"视频描述"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteVideoReq 删除视频请求
|
||||||
|
type DeleteVideoReq struct {
|
||||||
|
g.Meta `path:"/deleteVideo" method:"delete" tags:"视频管理" summary:"删除视频" dc:"删除视频"`
|
||||||
|
ID int64 `json:"id" v:"required" dc:"视频ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateVideoReq 生成视频请求
|
||||||
|
type GenerateVideoReq struct {
|
||||||
|
g.Meta `path:"/generateVideo" method:"post" tags:"视频管理" summary:"生成视频" dc:"选择数字人,选择已生成的音频,调用数字人形象与音频合成形成视频"`
|
||||||
|
ID int64 `json:"id" v:"required" dc:"视频ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateVideoRes 生成视频响应
|
||||||
|
type GenerateVideoRes struct {
|
||||||
|
TaskID string `json:"taskId" dc:"任务ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetVideoStatusOptionsReq 获取视频状态选项请求
|
||||||
|
type GetVideoStatusOptionsReq struct {
|
||||||
|
g.Meta `path:"/getVideoStatusOptions" method:"get" tags:"视频管理" summary:"获取视频状态选项" dc:"获取所有视频状态的选项列表"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetVideoStatusOptionsRes 获取视频状态选项响应
|
||||||
|
type GetVideoStatusOptionsRes struct {
|
||||||
|
Options []consts.VideoStatusKeyValue `json:"options" dc:"视频状态选项列表"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetResolutionOptionsReq 获取分辨率选项请求
|
||||||
|
type GetResolutionOptionsReq struct {
|
||||||
|
g.Meta `path:"/getResolutionOptions" method:"get" tags:"视频管理" summary:"获取分辨率选项" dc:"获取所有分辨率的选项列表"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetResolutionOptionsRes 获取分辨率选项响应
|
||||||
|
type GetResolutionOptionsRes struct {
|
||||||
|
Options []consts.ResolutionKeyValue `json:"options" dc:"分辨率选项列表"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package entity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/beans"
|
||||||
|
)
|
||||||
|
|
||||||
|
type asyncTaskRefCol struct {
|
||||||
|
beans.SQLBaseCol
|
||||||
|
TaskID string
|
||||||
|
State string
|
||||||
|
TableName string
|
||||||
|
BizID string
|
||||||
|
OssFile string
|
||||||
|
ErrorMsg string
|
||||||
|
}
|
||||||
|
|
||||||
|
var AsyncTaskRefCol = asyncTaskRefCol{
|
||||||
|
SQLBaseCol: beans.DefSQLBaseCol,
|
||||||
|
TaskID: "task_id",
|
||||||
|
State: "state",
|
||||||
|
TableName: "table_name",
|
||||||
|
BizID: "biz_id",
|
||||||
|
OssFile: "oss_file",
|
||||||
|
ErrorMsg: "error_msg",
|
||||||
|
}
|
||||||
|
|
||||||
|
// AsyncTaskRef 异步任务绑定记录(中间件 task_id 绑定业务表)
|
||||||
|
// - state: 保存中间件返回的任务状态(0排队中/1执行中/2成功/3失败/4已下载)
|
||||||
|
type AsyncTaskRef struct {
|
||||||
|
beans.SQLBaseDO `orm:",inline"`
|
||||||
|
TaskID string `orm:"task_id" json:"taskId"`
|
||||||
|
State int `orm:"state" json:"state"`
|
||||||
|
TableName string `orm:"table_name" json:"tableName"`
|
||||||
|
BizID int64 `orm:"biz_id" json:"bizId,string"`
|
||||||
|
OssFile string `orm:"oss_file" json:"ossFile"`
|
||||||
|
ErrorMsg string `orm:"error_msg" json:"errorMsg"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package entity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ai-agent/digital-human/consts"
|
||||||
|
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/beans"
|
||||||
|
)
|
||||||
|
|
||||||
|
type audioCol struct {
|
||||||
|
beans.SQLBaseCol
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
ScriptText string
|
||||||
|
AudioURL string
|
||||||
|
Status string
|
||||||
|
ErrorMsg string
|
||||||
|
Duration string
|
||||||
|
ExternalID string
|
||||||
|
Voice string
|
||||||
|
VoiceType string
|
||||||
|
CustomVoice string
|
||||||
|
}
|
||||||
|
|
||||||
|
var AudioCol = audioCol{
|
||||||
|
SQLBaseCol: beans.DefSQLBaseCol,
|
||||||
|
Name: "name",
|
||||||
|
Description: "description",
|
||||||
|
ScriptText: "script_text",
|
||||||
|
AudioURL: "audio_url",
|
||||||
|
Status: "status",
|
||||||
|
ErrorMsg: "error_msg",
|
||||||
|
Duration: "duration",
|
||||||
|
ExternalID: "external_id",
|
||||||
|
Voice: "voice",
|
||||||
|
VoiceType: "voice_type",
|
||||||
|
CustomVoice: "custom_voice",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Audio 音频实体
|
||||||
|
type Audio struct {
|
||||||
|
beans.SQLBaseDO `orm:",inline"`
|
||||||
|
// 基础信息
|
||||||
|
Name string `orm:"name" json:"name"` // 音频名称
|
||||||
|
Description string `orm:"description" json:"description"` // 音频描述
|
||||||
|
ScriptText string `orm:"script_text" json:"scriptText"` // 话术文本
|
||||||
|
AudioURL string `orm:"audio_url" json:"audioUrl"` // 音频文件URL
|
||||||
|
Status consts.AudioStatus `orm:"status" json:"status"` // 状态:0生成中/1成功/2失败
|
||||||
|
ErrorMsg string `orm:"error_msg" json:"errorMsg"` // 错误信息
|
||||||
|
Duration int `orm:"duration" json:"duration"` // 音频时长(秒)
|
||||||
|
ExternalID string `orm:"external_id" json:"externalId"` // 外部音频ID
|
||||||
|
// 音色相关
|
||||||
|
Voice string `orm:"voice" json:"voice"` // 音色:serena/vivian/uncle_fu/ryan/aiden/ono_anna/sohee/eric/dylan
|
||||||
|
VoiceType string `orm:"voice_type" json:"voiceType"` // 音色类型:preset/custom(预设/克隆)
|
||||||
|
CustomVoice string `orm:"custom_voice" json:"customVoice"` // 自定义音色ID(用于声音克隆)
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package entity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/beans"
|
||||||
|
)
|
||||||
|
|
||||||
|
type customVoiceCol struct {
|
||||||
|
beans.SQLBaseCol
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
Status string
|
||||||
|
ErrorMsg string
|
||||||
|
OssFile string
|
||||||
|
ReferenceAudio string
|
||||||
|
}
|
||||||
|
|
||||||
|
var CustomVoiceCol = customVoiceCol{
|
||||||
|
SQLBaseCol: beans.DefSQLBaseCol,
|
||||||
|
Name: "name",
|
||||||
|
Description: "description",
|
||||||
|
Status: "status",
|
||||||
|
ErrorMsg: "error_msg",
|
||||||
|
OssFile: "oss_file",
|
||||||
|
ReferenceAudio: "reference_audio",
|
||||||
|
}
|
||||||
|
|
||||||
|
// CustomVoice 自定义音色实体
|
||||||
|
type CustomVoice struct {
|
||||||
|
beans.SQLBaseDO `orm:",inline"`
|
||||||
|
// 基础信息
|
||||||
|
Name string `orm:"name" json:"name"` // 音色名称
|
||||||
|
Description string `orm:"description" json:"description"` // 音色描述
|
||||||
|
Text string `orm:"text" json:"text"` // 参考文本
|
||||||
|
Status int `orm:"status" json:"status"` // 状态:0生成中/1成功/2失败
|
||||||
|
ErrorMsg string `orm:"error_msg" json:"errorMsg"` // 错误信息
|
||||||
|
OssFile string `orm:"oss_file" json:"ossFile"` // 结果文件URL(如参考音频/特征文件等)
|
||||||
|
ReferenceAudio []byte `orm:"reference_audio" json:"referenceAudio"` // 参考音频数据(二进制)
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package entity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ai-agent/digital-human/consts"
|
||||||
|
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/beans"
|
||||||
|
)
|
||||||
|
|
||||||
|
type digitalHumanCol struct {
|
||||||
|
beans.SQLBaseCol
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
AvatarURL string
|
||||||
|
VideoURL string
|
||||||
|
Voice string
|
||||||
|
Status string
|
||||||
|
Tags string
|
||||||
|
Gender string
|
||||||
|
Age string
|
||||||
|
Style string
|
||||||
|
ExternalID string
|
||||||
|
Metadata string
|
||||||
|
}
|
||||||
|
|
||||||
|
var DigitalHumanCol = digitalHumanCol{
|
||||||
|
SQLBaseCol: beans.DefSQLBaseCol,
|
||||||
|
Name: "name",
|
||||||
|
Description: "description",
|
||||||
|
AvatarURL: "avatar_url",
|
||||||
|
VideoURL: "video_url",
|
||||||
|
Voice: "voice",
|
||||||
|
Status: "status",
|
||||||
|
Tags: "tags",
|
||||||
|
Gender: "gender",
|
||||||
|
Age: "age",
|
||||||
|
Style: "style",
|
||||||
|
ExternalID: "external_id",
|
||||||
|
Metadata: "metadata",
|
||||||
|
}
|
||||||
|
|
||||||
|
// DigitalHuman 数字人形象实体
|
||||||
|
type DigitalHuman struct {
|
||||||
|
beans.SQLBaseDO `orm:",inline"`
|
||||||
|
// 基础信息
|
||||||
|
Name string `orm:"name" json:"name"` // 数字人名称
|
||||||
|
Description string `orm:"description" json:"description"` // 数字人描述
|
||||||
|
AvatarURL string `orm:"avatar_url" json:"imageUrl"` // 形象图片URL
|
||||||
|
VideoURL string `orm:"video_url" json:"videoUrl"` // 形象视频URL
|
||||||
|
Voice string `orm:"voice" json:"voice"` // 默认音色
|
||||||
|
Status consts.DigitalHumanStatus `orm:"status" json:"status"` // 状态:1启用/0停用
|
||||||
|
Tags []string `orm:"tags" json:"tags"` // 标签
|
||||||
|
Gender consts.Gender `orm:"gender" json:"gender"` // 性别
|
||||||
|
Age consts.Age `orm:"age" json:"age"` // 年龄段
|
||||||
|
Style consts.Style `orm:"style" json:"style"` // 风格:商务/休闲/正式等
|
||||||
|
ExternalID string `orm:"external_id" json:"externalId"` // 外部系统ID
|
||||||
|
Metadata []map[string]interface{} `orm:"metadata" json:"metadata"` // 动态元数据
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package entity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ai-agent/digital-human/consts"
|
||||||
|
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/beans"
|
||||||
|
)
|
||||||
|
|
||||||
|
type videoCol struct {
|
||||||
|
beans.SQLBaseCol
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
AudioID string
|
||||||
|
ScriptText string
|
||||||
|
VideoURL string
|
||||||
|
Status string
|
||||||
|
ErrorMsg string
|
||||||
|
Duration string
|
||||||
|
ThumbnailURL string
|
||||||
|
ExternalID string
|
||||||
|
DigitalHumanID string
|
||||||
|
DigitalHumanName string
|
||||||
|
Resolution string
|
||||||
|
}
|
||||||
|
|
||||||
|
var VideoCol = videoCol{
|
||||||
|
SQLBaseCol: beans.DefSQLBaseCol,
|
||||||
|
Name: "name",
|
||||||
|
Description: "description",
|
||||||
|
AudioID: "audio_id",
|
||||||
|
ScriptText: "script_text",
|
||||||
|
VideoURL: "video_url",
|
||||||
|
Status: "status",
|
||||||
|
ErrorMsg: "error_msg",
|
||||||
|
Duration: "duration",
|
||||||
|
ThumbnailURL: "thumbnail_url",
|
||||||
|
ExternalID: "external_id",
|
||||||
|
DigitalHumanID: "digital_human_id",
|
||||||
|
DigitalHumanName: "digital_human_name",
|
||||||
|
Resolution: "resolution",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Video 视频实体
|
||||||
|
type Video struct {
|
||||||
|
beans.SQLBaseDO `orm:",inline"`
|
||||||
|
// 基础信息
|
||||||
|
Name string `orm:"name" json:"name"` // 视频名称
|
||||||
|
Description string `orm:"description" json:"description"` // 视频描述
|
||||||
|
DigitalHumanID int64 `orm:"digital_human_id" json:"digitalHumanId"` // 数字人形象ID
|
||||||
|
AudioID int64 `orm:"audio_id" json:"audioId"` // 音频ID
|
||||||
|
ScriptText string `orm:"script_text" json:"scriptText"` // 话术文本
|
||||||
|
VideoURL string `orm:"video_url" json:"videoUrl"` // 合成视频URL
|
||||||
|
Status consts.VideoStatus `orm:"status" json:"status"` // 状态:0生成中/1成功/2失败
|
||||||
|
ErrorMsg string `orm:"error_msg" json:"errorMsg"` // 错误信息
|
||||||
|
Duration int `orm:"duration" json:"duration"` // 视频时长(秒)
|
||||||
|
ThumbnailURL string `orm:"thumbnail_url" json:"thumbnailUrl"` // 缩略图URL
|
||||||
|
ExternalID string `orm:"external_id" json:"externalId"` // 外部任务ID
|
||||||
|
DigitalHumanName string `orm:"digital_human_name" json:"digitalHumanName"` // 数字人名称(冗余字段)
|
||||||
|
Resolution consts.Resolution `orm:"resolution" json:"resolution"` // 分辨率
|
||||||
|
}
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"ai-agent/digital-human/consts"
|
||||||
|
"ai-agent/digital-human/consts/public"
|
||||||
|
"ai-agent/digital-human/dao"
|
||||||
|
"ai-agent/digital-human/model/dto"
|
||||||
|
"ai-agent/digital-human/model/entity"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/database/gdb"
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
"github.com/gogf/gf/v2/os/gtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
type asyncTaskService struct{}
|
||||||
|
|
||||||
|
// AsyncTask 异步任务同步服务(供定时任务/业务轮询调用)
|
||||||
|
var AsyncTask = new(asyncTaskService)
|
||||||
|
|
||||||
|
// Sync
|
||||||
|
// 1) 扫描 digital_human_async_task_ref 中 state=0/1 的记录(业务“生成中”)
|
||||||
|
// 2) 组装 task_id 批量请求 model-asynch /task/get-task-batch
|
||||||
|
// 3) 中间件状态映射到业务状态(业务只维护三态:0生成中/1成功/2失败):
|
||||||
|
// - 中间件 0/1/3(能查到 task_id) -> 业务 0(生成中)
|
||||||
|
// - 中间件 2/4(成功/已下载) -> 业务 1(成功)
|
||||||
|
// - 中间件 查不到 task_id(返回列表缺失) -> 业务 2(失败)
|
||||||
|
//
|
||||||
|
// 4) 绑定表仅用于“待同步列表”,因此:
|
||||||
|
// - 对中间件 0/1/3 不额外写库(减少查询/更新开销)
|
||||||
|
// - 对成功(2/4)与缺失(task_id 查不到)才更新绑定表
|
||||||
|
func (s *asyncTaskService) Sync(ctx context.Context, req *dto.SyncAsyncTasksReq) (res *dto.SyncAsyncTasksRes, err error) {
|
||||||
|
limit := 200
|
||||||
|
if req != nil && req.Limit > 0 {
|
||||||
|
limit = req.Limit
|
||||||
|
}
|
||||||
|
refs, err := dao.AsyncTaskRef.ListPending(ctx, limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
taskIDs := make([]string, 0, len(refs))
|
||||||
|
refMap := make(map[string]*entity.AsyncTaskRef, len(refs))
|
||||||
|
for _, r := range refs {
|
||||||
|
if r == nil || r.TaskID == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
taskIDs = append(taskIDs, r.TaskID)
|
||||||
|
refMap[r.TaskID] = r
|
||||||
|
}
|
||||||
|
|
||||||
|
out := &dto.SyncAsyncTasksRes{
|
||||||
|
Total: len(taskIDs),
|
||||||
|
List: make([]dto.SyncAsyncTasksItem, 0, len(taskIDs)),
|
||||||
|
}
|
||||||
|
if len(taskIDs) == 0 {
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
items, err := getModelAsynchTaskBatch(ctx, taskIDs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
seen := make(map[string]struct{}, len(items))
|
||||||
|
handled := 0
|
||||||
|
|
||||||
|
for _, it := range items {
|
||||||
|
r := refMap[it.TaskID]
|
||||||
|
if r == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[it.TaskID] = struct{}{}
|
||||||
|
|
||||||
|
switch it.State {
|
||||||
|
case 0, 1, 3:
|
||||||
|
// 排队中/执行中/失败(可能重试):业务侧仍视为生成中,不更新绑定表,减少更新开销
|
||||||
|
case 2, 4:
|
||||||
|
// 成功/已下载:业务侧写入 oss_file 并标记成功
|
||||||
|
if it.OssFile == "" {
|
||||||
|
errMsg := "中间件返回空oss地址"
|
||||||
|
_ = s.updateBizFailed(ctx, r, errMsg)
|
||||||
|
_, _ = dao.AsyncTaskRef.UpdateByTaskID(ctx, it.TaskID, gdb.Map{
|
||||||
|
entity.AsyncTaskRefCol.State: it.State,
|
||||||
|
entity.AsyncTaskRefCol.OssFile: "",
|
||||||
|
entity.AsyncTaskRefCol.ErrorMsg: errMsg,
|
||||||
|
})
|
||||||
|
out.List = append(out.List, dto.SyncAsyncTasksItem{
|
||||||
|
TaskID: it.TaskID,
|
||||||
|
State: it.State,
|
||||||
|
TableName: r.TableName,
|
||||||
|
BizID: fmt.Sprintf("%d", r.BizID),
|
||||||
|
OssFile: "",
|
||||||
|
ErrorMsg: errMsg,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := s.updateBizSuccess(ctx, r, it.OssFile); err != nil {
|
||||||
|
errMsg := fmt.Sprintf("生成音频失败: %v", err)
|
||||||
|
_ = s.updateBizFailed(ctx, r, errMsg)
|
||||||
|
_, _ = dao.AsyncTaskRef.UpdateByTaskID(ctx, it.TaskID, gdb.Map{
|
||||||
|
entity.AsyncTaskRefCol.State: it.State,
|
||||||
|
entity.AsyncTaskRefCol.OssFile: it.OssFile,
|
||||||
|
entity.AsyncTaskRefCol.ErrorMsg: errMsg,
|
||||||
|
})
|
||||||
|
out.List = append(out.List, dto.SyncAsyncTasksItem{
|
||||||
|
TaskID: it.TaskID,
|
||||||
|
State: it.State,
|
||||||
|
TableName: r.TableName,
|
||||||
|
BizID: fmt.Sprintf("%d", r.BizID),
|
||||||
|
OssFile: it.OssFile,
|
||||||
|
ErrorMsg: errMsg,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
handled++
|
||||||
|
_, _ = dao.AsyncTaskRef.UpdateByTaskID(ctx, it.TaskID, gdb.Map{
|
||||||
|
entity.AsyncTaskRefCol.State: it.State,
|
||||||
|
entity.AsyncTaskRefCol.OssFile: it.OssFile,
|
||||||
|
entity.AsyncTaskRefCol.ErrorMsg: "",
|
||||||
|
})
|
||||||
|
default:
|
||||||
|
// 其他状态:不处理
|
||||||
|
}
|
||||||
|
|
||||||
|
out.List = append(out.List, dto.SyncAsyncTasksItem{
|
||||||
|
TaskID: it.TaskID,
|
||||||
|
State: it.State,
|
||||||
|
TableName: r.TableName,
|
||||||
|
BizID: fmt.Sprintf("%d", r.BizID),
|
||||||
|
OssFile: it.OssFile,
|
||||||
|
ErrorMsg: "",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理“查不到 task_id”的情况:
|
||||||
|
// 中间件对失败重试耗尽的任务会硬删除,批量接口不会返回该 task_id。
|
||||||
|
// 业务侧把这种情况视为失败终态,并软删除绑定记录,避免重复轮询。
|
||||||
|
for _, taskID := range taskIDs {
|
||||||
|
if _, ok := seen[taskID]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
r := refMap[taskID]
|
||||||
|
if r == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
msg := "模型任务不存在已失败"
|
||||||
|
_ = s.updateBizFailed(ctx, r, msg)
|
||||||
|
_, _ = dao.AsyncTaskRef.UpdateByTaskID(ctx, taskID, gdb.Map{
|
||||||
|
entity.AsyncTaskRefCol.State: 3,
|
||||||
|
entity.AsyncTaskRefCol.ErrorMsg: msg,
|
||||||
|
"deleted_at": gtime.Now(),
|
||||||
|
})
|
||||||
|
out.List = append(out.List, dto.SyncAsyncTasksItem{
|
||||||
|
TaskID: taskID,
|
||||||
|
State: 3,
|
||||||
|
TableName: r.TableName,
|
||||||
|
BizID: fmt.Sprintf("%d", r.BizID),
|
||||||
|
OssFile: "",
|
||||||
|
ErrorMsg: msg,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
out.Handled = handled
|
||||||
|
g.Log().Infof(ctx, "[AsyncTask.Sync] total=%d handled=%d", out.Total, out.Handled)
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateBizSuccess 更新业务侧状态为成功
|
||||||
|
func (s *asyncTaskService) updateBizSuccess(ctx context.Context, ref *entity.AsyncTaskRef, ossFile string) error {
|
||||||
|
switch ref.TableName {
|
||||||
|
case public.TableNameAudio:
|
||||||
|
_, err := dao.Audio.UpdateStatus(ctx, ref.BizID, consts.AudioStatusSuccess, "", ossFile, 0, "")
|
||||||
|
return err
|
||||||
|
case public.TableNameCustomVoice:
|
||||||
|
_, err := dao.CustomVoice.UpdateStatus(ctx, ref.BizID, 1, "", ossFile)
|
||||||
|
return err
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("未知 table_name=%s", ref.TableName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateBizFailed 更新业务侧状态为失败
|
||||||
|
func (s *asyncTaskService) updateBizFailed(ctx context.Context, ref *entity.AsyncTaskRef, msg string) error {
|
||||||
|
switch ref.TableName {
|
||||||
|
case public.TableNameAudio:
|
||||||
|
_, err := dao.Audio.UpdateStatus(ctx, ref.BizID, consts.AudioStatusFailed, msg, "", 0, "")
|
||||||
|
return err
|
||||||
|
case public.TableNameCustomVoice:
|
||||||
|
_, err := dao.CustomVoice.UpdateStatus(ctx, ref.BizID, 2, msg, "")
|
||||||
|
return err
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,268 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
|
||||||
|
"ai-agent/digital-human/consts"
|
||||||
|
"ai-agent/digital-human/consts/public"
|
||||||
|
"ai-agent/digital-human/dao"
|
||||||
|
"ai-agent/digital-human/model/dto"
|
||||||
|
"ai-agent/digital-human/model/entity"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/errors/gerror"
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
"github.com/gogf/gf/v2/util/gconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type audio struct{}
|
||||||
|
|
||||||
|
// Audio 音频服务
|
||||||
|
var Audio = new(audio)
|
||||||
|
|
||||||
|
// UploadFileResponse OSS 文件上传响应结构
|
||||||
|
type UploadFileResponse struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
Data struct {
|
||||||
|
FileURL string `json:"fileURL" dc:"上传地址"`
|
||||||
|
FileSize int `json:"fileSize" dc:"文件大小"`
|
||||||
|
FileName string `json:"fileName" dc:"文件名称"`
|
||||||
|
FileFormat string `json:"fileFormat" dc:"文件格式"`
|
||||||
|
FileAddressPrefix string `json:"fileAddressPrefix"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create 创建音频
|
||||||
|
func (s *audio) Create(ctx context.Context, req *dto.CreateAudioReq) (res *dto.CreateAudioRes, err error) {
|
||||||
|
// 设置默认音色
|
||||||
|
if req.Voice == "" {
|
||||||
|
req.Voice = "Serena" // 默认音色
|
||||||
|
}
|
||||||
|
if req.VoiceType == "" {
|
||||||
|
req.VoiceType = "Preset" // 默认预设音色
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是自定义音色,验证音色是否存在
|
||||||
|
if req.VoiceType == "custom" && req.CustomVoice != "" {
|
||||||
|
customVoiceID := gconv.Int64(req.CustomVoice)
|
||||||
|
_, err := dao.CustomVoice.GetOne(ctx, customVoiceID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gerror.Wrapf(err, "自定义音色不存在: %s", req.CustomVoice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 插入数据库(初始状态为生成中)
|
||||||
|
audioID, err := dao.Audio.Insert(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通过 model-asynch 创建异步任务(由中间件执行模型调用与产物落 OSS)
|
||||||
|
// 约定:
|
||||||
|
// - custom(克隆音色) -> base 模型(需要参考音频/参考文本) 否则 -> customvoice 模型
|
||||||
|
var taskID string
|
||||||
|
if req.VoiceType == "custom" {
|
||||||
|
customVoiceID := gconv.Int64(req.CustomVoice)
|
||||||
|
// 1. 先获取自定义音色详情
|
||||||
|
cv, err := dao.CustomVoice.GetOne(ctx, customVoiceID)
|
||||||
|
if err != nil {
|
||||||
|
_, _ = dao.Audio.UpdateStatus(ctx, audioID, consts.AudioStatusFailed, "获取自定义音色失败: "+err.Error(), "", 0, "")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// 2. 调用模型生成音频
|
||||||
|
refAudioBase64 := base64.StdEncoding.EncodeToString(cv.ReferenceAudio)
|
||||||
|
xVectorOnlyMode := false
|
||||||
|
if cv.Text == "" {
|
||||||
|
xVectorOnlyMode = true
|
||||||
|
}
|
||||||
|
taskID, err = TTS.CreateBaseTask(asyncCtx(ctx), req.ScriptText, "Auto", cv.Text, cv.OssFile, refAudioBase64, xVectorOnlyMode, 1.0)
|
||||||
|
} else {
|
||||||
|
// 1. 调用模型生成音频
|
||||||
|
taskID, err = TTS.CreateCustomVoiceTask(asyncCtx(ctx), req.ScriptText, req.Voice, "Auto", "", 1.0)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
_, _ = dao.Audio.UpdateStatus(ctx, audioID, consts.AudioStatusFailed, "创建异步任务失败: "+err.Error(), "", 0, "")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, _ = dao.AsyncTaskRef.Insert(ctx, &entity.AsyncTaskRef{
|
||||||
|
TaskID: taskID,
|
||||||
|
State: 0,
|
||||||
|
TableName: public.TableNameAudio,
|
||||||
|
BizID: audioID,
|
||||||
|
})
|
||||||
|
res = &dto.CreateAudioRes{
|
||||||
|
Id: audioID,
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// List 获取音频列表
|
||||||
|
func (s *audio) List(ctx context.Context, req *dto.ListAudioReq) (res *dto.ListAudioRes, err error) {
|
||||||
|
audioList, total, err := dao.Audio.List(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res = &dto.ListAudioRes{
|
||||||
|
Total: int64(total),
|
||||||
|
List: make([]*dto.AudioListItem, 0, len(audioList)),
|
||||||
|
}
|
||||||
|
for _, audio := range audioList {
|
||||||
|
res.List = append(res.List, &dto.AudioListItem{
|
||||||
|
ID: audio.Id,
|
||||||
|
Name: audio.Name,
|
||||||
|
Description: audio.Description,
|
||||||
|
ScriptText: audio.ScriptText,
|
||||||
|
AudioURL: audio.AudioURL,
|
||||||
|
Status: audio.Status,
|
||||||
|
ErrorMsg: audio.ErrorMsg,
|
||||||
|
Duration: audio.Duration,
|
||||||
|
ExternalID: audio.ExternalID,
|
||||||
|
Voice: audio.Voice,
|
||||||
|
VoiceType: audio.VoiceType,
|
||||||
|
CustomVoice: audio.CustomVoice,
|
||||||
|
CreatedAt: audio.CreatedAt,
|
||||||
|
UpdatedAt: audio.UpdatedAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOne 获取单个音频
|
||||||
|
func (s *audio) GetOne(ctx context.Context, id int64) (*dto.GetAudioRes, error) {
|
||||||
|
audioOne, err := dao.Audio.GetOne(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &dto.GetAudioRes{
|
||||||
|
ID: audioOne.Id,
|
||||||
|
Name: audioOne.Name,
|
||||||
|
Description: audioOne.Description,
|
||||||
|
ScriptText: audioOne.ScriptText,
|
||||||
|
AudioURL: audioOne.AudioURL,
|
||||||
|
Status: audioOne.Status,
|
||||||
|
ErrorMsg: audioOne.ErrorMsg,
|
||||||
|
Duration: audioOne.Duration,
|
||||||
|
ExternalID: audioOne.ExternalID,
|
||||||
|
Voice: audioOne.Voice,
|
||||||
|
VoiceType: audioOne.VoiceType,
|
||||||
|
CustomVoice: audioOne.CustomVoice,
|
||||||
|
CreatedAt: audioOne.CreatedAt,
|
||||||
|
UpdatedAt: audioOne.UpdatedAt,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update 更新音频
|
||||||
|
func (s *audio) Update(ctx context.Context, req *dto.UpdateAudioReq) (err error) {
|
||||||
|
// 先获取原始音频信息
|
||||||
|
audioOne, err := dao.Audio.GetOne(ctx, req.ID)
|
||||||
|
if err != nil {
|
||||||
|
return gerror.Wrap(err, "获取原始音频信息失败")
|
||||||
|
}
|
||||||
|
// 修改字段
|
||||||
|
if !g.IsEmpty(req.Name) {
|
||||||
|
audioOne.Name = req.Name
|
||||||
|
}
|
||||||
|
if !g.IsEmpty(req.Description) {
|
||||||
|
audioOne.Description = req.Description
|
||||||
|
}
|
||||||
|
if !g.IsEmpty(req.Voice) {
|
||||||
|
audioOne.Voice = req.Voice
|
||||||
|
}
|
||||||
|
if !g.IsEmpty(req.VoiceType) {
|
||||||
|
audioOne.VoiceType = req.VoiceType
|
||||||
|
}
|
||||||
|
if !g.IsEmpty(req.CustomVoice) {
|
||||||
|
audioOne.CustomVoice = req.CustomVoice
|
||||||
|
}
|
||||||
|
_, err = dao.Audio.Update(ctx, req.ID, audioOne)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete 删除音频
|
||||||
|
func (s *audio) Delete(ctx context.Context, id int64) error {
|
||||||
|
_, err := dao.Audio.Delete(ctx, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate 重新生成音频
|
||||||
|
func (s *audio) Generate(ctx context.Context, req *dto.GenerateAudioReq) (res *dto.GenerateAudioRes, err error) {
|
||||||
|
// 获取音频信息
|
||||||
|
audioOne, err := dao.Audio.GetOne(ctx, req.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gerror.Wrap(err, "获取音频信息失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置状态为生成中
|
||||||
|
_, err = dao.Audio.UpdateStatus(ctx, req.ID, consts.AudioStatusGenerating, "", "", 0, "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建请求
|
||||||
|
createReq := &dto.CreateAudioReq{
|
||||||
|
Name: audioOne.Name,
|
||||||
|
Description: audioOne.Description,
|
||||||
|
ScriptText: audioOne.ScriptText,
|
||||||
|
Voice: audioOne.Voice,
|
||||||
|
VoiceType: audioOne.VoiceType,
|
||||||
|
CustomVoice: audioOne.CustomVoice,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 异步重新生成音频
|
||||||
|
var taskID string
|
||||||
|
if createReq.VoiceType == "custom" {
|
||||||
|
customVoiceID := gconv.Int64(createReq.CustomVoice)
|
||||||
|
cv, err := dao.CustomVoice.GetOne(ctx, customVoiceID)
|
||||||
|
if err != nil {
|
||||||
|
_, _ = dao.Audio.UpdateStatus(ctx, req.ID, consts.AudioStatusFailed, "获取自定义音色失败: "+err.Error(), "", 0, "")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
refAudioBase64 := ""
|
||||||
|
if cv != nil && len(cv.ReferenceAudio) > 0 {
|
||||||
|
refAudioBase64 = base64.StdEncoding.EncodeToString(cv.ReferenceAudio)
|
||||||
|
}
|
||||||
|
refText := ""
|
||||||
|
if cv != nil {
|
||||||
|
refText = cv.Text
|
||||||
|
}
|
||||||
|
xVectorOnlyMode := false
|
||||||
|
if refText == "" {
|
||||||
|
xVectorOnlyMode = true
|
||||||
|
}
|
||||||
|
taskID, err = TTS.CreateBaseTask(asyncCtx(ctx), createReq.ScriptText, "Auto", refText, cv.OssFile, refAudioBase64, xVectorOnlyMode, 1.0)
|
||||||
|
} else {
|
||||||
|
taskID, err = TTS.CreateCustomVoiceTask(asyncCtx(ctx), createReq.ScriptText, createReq.Voice, "Auto", "", 1.0)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
_, _ = dao.Audio.UpdateStatus(ctx, req.ID, consts.AudioStatusFailed, "创建异步任务失败: "+err.Error(), "", 0, "")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, _ = dao.AsyncTaskRef.Insert(ctx, &entity.AsyncTaskRef{
|
||||||
|
TaskID: taskID,
|
||||||
|
State: 0,
|
||||||
|
TableName: public.TableNameAudio,
|
||||||
|
BizID: req.ID,
|
||||||
|
})
|
||||||
|
|
||||||
|
res = &dto.GenerateAudioRes{
|
||||||
|
TaskID: gconv.String(req.ID),
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStatusOptions 获取状态选项
|
||||||
|
func (s *audio) GetStatusOptions(ctx context.Context, req *dto.GetAudioStatusOptionsReq) (res *dto.GetAudioStatusOptionsRes, err error) {
|
||||||
|
_ = ctx
|
||||||
|
_ = req
|
||||||
|
res = new(dto.GetAudioStatusOptionsRes)
|
||||||
|
res.Options = consts.GetAllAudioStatusKeyValue()
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TTS 文本转语音(使用 Qwen3-TTS)
|
||||||
|
func (s *audio) TTS(ctx context.Context, req *dto.TTSReq) (res *dto.TTSRes, err error) {
|
||||||
|
_ = ctx
|
||||||
|
_ = req
|
||||||
|
return nil, gerror.New("该接口已迁移为异步:请使用 CreateAudio 创建异步任务并通过轮询/批量领取获取结果")
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
|
||||||
|
"ai-agent/digital-human/consts/public"
|
||||||
|
"ai-agent/digital-human/dao"
|
||||||
|
"ai-agent/digital-human/model/dto"
|
||||||
|
"ai-agent/digital-human/model/entity"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/errors/gerror"
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
"github.com/gogf/gf/v2/util/gconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type customVoice struct{}
|
||||||
|
|
||||||
|
// CustomVoice 自定义音色服务
|
||||||
|
var CustomVoice = new(customVoice)
|
||||||
|
|
||||||
|
// CreateCustomVoice 创建自定义音色
|
||||||
|
func (s *customVoice) CreateCustomVoice(ctx context.Context, req *dto.CreateCustomVoiceReq) (res *dto.CreateCustomVoiceRes, err error) {
|
||||||
|
g.Log().Infof(ctx, "创建自定义音色: name=%s, voiceType=%s", req.Name, req.VoiceType)
|
||||||
|
// 插入数据库(状态:生成中)
|
||||||
|
voiceID, err := dao.CustomVoice.Insert(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch req.VoiceType {
|
||||||
|
case "design":
|
||||||
|
// 设计音频:按模型约定只传 text + instruct
|
||||||
|
taskID, err := TTS.CreateVoiceDesignTask(asyncCtx(ctx), req.Text, req.Description, "", 0)
|
||||||
|
if err != nil {
|
||||||
|
_, _ = dao.CustomVoice.UpdateStatus(ctx, voiceID, 2, "创建异步任务失败: "+err.Error(), "")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, _ = dao.AsyncTaskRef.Insert(ctx, &entity.AsyncTaskRef{
|
||||||
|
TaskID: taskID,
|
||||||
|
State: 0,
|
||||||
|
TableName: public.TableNameCustomVoice,
|
||||||
|
BizID: voiceID,
|
||||||
|
})
|
||||||
|
res = &dto.CreateCustomVoiceRes{VoiceID: gconv.String(voiceID)}
|
||||||
|
g.Log().Infof(ctx, "自定义音色创建成功: voiceId=%d taskId=%s", voiceID, taskID)
|
||||||
|
case "clone":
|
||||||
|
// TODO : 克隆音色:使用语音转文字暂预留,后续找模型对应处理
|
||||||
|
refAudioBase64 := base64.StdEncoding.EncodeToString(req.ReferenceAudio)
|
||||||
|
taskID, err := TTS.SpeechToText(asyncCtx(ctx), refAudioBase64)
|
||||||
|
if err != nil {
|
||||||
|
_, _ = dao.CustomVoice.UpdateStatus(ctx, voiceID, 2, "创建异步任务失败: "+err.Error(), "")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, _ = dao.AsyncTaskRef.Insert(ctx, &entity.AsyncTaskRef{
|
||||||
|
TaskID: taskID,
|
||||||
|
State: 0,
|
||||||
|
TableName: public.TableNameCustomVoice,
|
||||||
|
BizID: voiceID,
|
||||||
|
})
|
||||||
|
res = &dto.CreateCustomVoiceRes{VoiceID: gconv.String(voiceID)}
|
||||||
|
g.Log().Infof(ctx, "克隆音色成功: voiceId=%d taskId=%s", voiceID, taskID)
|
||||||
|
default:
|
||||||
|
return nil, gerror.New("不支持的音色类型")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListCustomVoices 获取自定义音色列表
|
||||||
|
func (s *customVoice) ListCustomVoices(ctx context.Context, req *dto.ListCustomVoiceReq) (res *dto.ListCustomVoiceRes, err error) {
|
||||||
|
customVoices, total, err := dao.CustomVoice.List(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res = &dto.ListCustomVoiceRes{
|
||||||
|
Total: int64(total),
|
||||||
|
List: make([]*dto.CustomVoiceItem, 0, len(customVoices)),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cv := range customVoices {
|
||||||
|
res.List = append(res.List, dao.CustomVoice.GetCustomVoiceItem(cv))
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteCustomVoice 删除自定义音色
|
||||||
|
func (s *customVoice) DeleteCustomVoice(ctx context.Context, req *dto.DeleteCustomVoiceReq) (err error) {
|
||||||
|
// 验证音色是否存在
|
||||||
|
voiceID := gconv.Int64(req.VoiceID)
|
||||||
|
|
||||||
|
_, err = dao.CustomVoice.GetOne(ctx, voiceID)
|
||||||
|
if err != nil {
|
||||||
|
return gerror.Wrapf(err, "音色不存在: %s", req.VoiceID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除音色
|
||||||
|
_, err = dao.CustomVoice.Delete(ctx, voiceID)
|
||||||
|
if err != nil {
|
||||||
|
return gerror.Wrapf(err, "删除音色失败: %s", req.VoiceID)
|
||||||
|
}
|
||||||
|
|
||||||
|
g.Log().Infof(ctx, "自定义音色删除成功: voiceId=%s", req.VoiceID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ai-agent/digital-human/consts"
|
||||||
|
"ai-agent/digital-human/dao"
|
||||||
|
"ai-agent/digital-human/model/dto"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/errors/gerror"
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
"github.com/gogf/gf/v2/os/gtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
type digitalHuman struct{}
|
||||||
|
|
||||||
|
// DigitalHuman 数字人形象服务
|
||||||
|
var DigitalHuman = new(digitalHuman)
|
||||||
|
|
||||||
|
// Create 创建数字人形象
|
||||||
|
func (s *digitalHuman) Create(ctx context.Context, req *dto.CreateDigitalHumanReq) (res *dto.CreateDigitalHumanRes, err error) {
|
||||||
|
count, err := dao.DigitalHuman.Count(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if count > 0 {
|
||||||
|
return nil, errors.New("数字人形象名称已存在")
|
||||||
|
}
|
||||||
|
// 插入数据库
|
||||||
|
ids, err := dao.DigitalHuman.Insert(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// PostgreSQL 使用 int64
|
||||||
|
id := ids[0].(int64)
|
||||||
|
res = &dto.CreateDigitalHumanRes{
|
||||||
|
Id: id,
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// List 获取数字人形象列表
|
||||||
|
func (s *digitalHuman) List(ctx context.Context, req *dto.ListDigitalHumanReq) (res *dto.ListDigitalHumanRes, error error) {
|
||||||
|
digitalHumanList, total, err := dao.DigitalHuman.List(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res = &dto.ListDigitalHumanRes{
|
||||||
|
Total: total,
|
||||||
|
}
|
||||||
|
b, err := json.Marshal(digitalHumanList)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(b, &res.List)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOne 获取单个数字人形象
|
||||||
|
func (s *digitalHuman) GetOne(ctx context.Context, id int64) (*dto.GetDigitalHumanRes, error) {
|
||||||
|
digitalHumanOne, err := dao.DigitalHuman.GetOne(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var createdAt, updatedAt *gtime.Time
|
||||||
|
if digitalHumanOne.CreatedAt != nil {
|
||||||
|
createdAt = digitalHumanOne.CreatedAt
|
||||||
|
}
|
||||||
|
if digitalHumanOne.UpdatedAt != nil {
|
||||||
|
updatedAt = digitalHumanOne.UpdatedAt
|
||||||
|
}
|
||||||
|
return &dto.GetDigitalHumanRes{
|
||||||
|
ID: digitalHumanOne.Id,
|
||||||
|
Name: digitalHumanOne.Name,
|
||||||
|
Description: digitalHumanOne.Description,
|
||||||
|
ImageURL: digitalHumanOne.AvatarURL,
|
||||||
|
VideoURL: digitalHumanOne.VideoURL,
|
||||||
|
Status: digitalHumanOne.Status,
|
||||||
|
Tags: digitalHumanOne.Tags,
|
||||||
|
Gender: digitalHumanOne.Gender,
|
||||||
|
Age: digitalHumanOne.Age,
|
||||||
|
Style: digitalHumanOne.Style,
|
||||||
|
ExternalID: digitalHumanOne.ExternalID,
|
||||||
|
Metadata: digitalHumanOne.Metadata,
|
||||||
|
CreatedAt: createdAt,
|
||||||
|
UpdatedAt: updatedAt,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update 更新数字人形象
|
||||||
|
func (s *digitalHuman) Update(ctx context.Context, req *dto.UpdateDigitalHumanReq) error {
|
||||||
|
// 先获取原始数字人形象信息
|
||||||
|
digitalHumanOne, err := dao.DigitalHuman.GetOne(ctx, req.ID)
|
||||||
|
if err != nil {
|
||||||
|
return gerror.Wrap(err, "获取原始数字人形象信息失败")
|
||||||
|
}
|
||||||
|
// 修改字段
|
||||||
|
if !g.IsEmpty(req.Name) {
|
||||||
|
digitalHumanOne.Name = req.Name
|
||||||
|
}
|
||||||
|
if !g.IsEmpty(req.Description) {
|
||||||
|
digitalHumanOne.Description = req.Description
|
||||||
|
}
|
||||||
|
if !g.IsEmpty(req.ImageURL) {
|
||||||
|
digitalHumanOne.AvatarURL = req.ImageURL
|
||||||
|
}
|
||||||
|
if !g.IsEmpty(req.VideoURL) {
|
||||||
|
digitalHumanOne.VideoURL = req.VideoURL
|
||||||
|
}
|
||||||
|
digitalHumanOne.Status = req.Status
|
||||||
|
if req.Tags != nil {
|
||||||
|
digitalHumanOne.Tags = req.Tags
|
||||||
|
}
|
||||||
|
if !g.IsEmpty(req.Gender) {
|
||||||
|
digitalHumanOne.Gender = req.Gender
|
||||||
|
}
|
||||||
|
if !g.IsEmpty(req.Age) {
|
||||||
|
digitalHumanOne.Age = req.Age
|
||||||
|
}
|
||||||
|
if !g.IsEmpty(req.Style) {
|
||||||
|
digitalHumanOne.Style = req.Style
|
||||||
|
}
|
||||||
|
if !g.IsEmpty(req.ExternalID) {
|
||||||
|
digitalHumanOne.ExternalID = req.ExternalID
|
||||||
|
}
|
||||||
|
if req.Metadata != nil {
|
||||||
|
digitalHumanOne.Metadata = req.Metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
return dao.DigitalHuman.Update(ctx, req.ID, digitalHumanOne)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateStatus 更新数字人形象状态
|
||||||
|
func (s *digitalHuman) UpdateStatus(ctx context.Context, id int64, status consts.DigitalHumanStatus) error {
|
||||||
|
_, err := dao.DigitalHuman.UpdateStatus(ctx, id, status)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete 删除数字人形象
|
||||||
|
func (s *digitalHuman) Delete(ctx context.Context, id int64) error {
|
||||||
|
return dao.DigitalHuman.Delete(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStatusOptions 获取状态选项
|
||||||
|
func (s *digitalHuman) GetStatusOptions(ctx context.Context, req *dto.GetDigitalHumanStatusOptionsReq) (res *dto.GetDigitalHumanStatusOptionsRes, err error) {
|
||||||
|
_ = ctx
|
||||||
|
_ = req
|
||||||
|
res = new(dto.GetDigitalHumanStatusOptionsRes)
|
||||||
|
res.Options = consts.GetAllStatusKeyValue()
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetGenderOptions 获取性别选项
|
||||||
|
func (s *digitalHuman) GetGenderOptions(ctx context.Context, req *dto.GetGenderOptionsReq) (res *dto.GetGenderOptionsRes, err error) {
|
||||||
|
_ = ctx
|
||||||
|
_ = req
|
||||||
|
res = new(dto.GetGenderOptionsRes)
|
||||||
|
res.Options = consts.GetAllGenderKeyValue()
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAgeOptions 获取年龄段选项
|
||||||
|
func (s *digitalHuman) GetAgeOptions(ctx context.Context, req *dto.GetAgeOptionsReq) (res *dto.GetAgeOptionsRes, err error) {
|
||||||
|
_ = ctx
|
||||||
|
_ = req
|
||||||
|
res = new(dto.GetAgeOptionsRes)
|
||||||
|
res.Options = consts.GetAllAgeKeyValue()
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStyleOptions 获取风格选项
|
||||||
|
func (s *digitalHuman) GetStyleOptions(ctx context.Context, req *dto.GetStyleOptionsReq) (res *dto.GetStyleOptionsRes, err error) {
|
||||||
|
_ = ctx
|
||||||
|
_ = req
|
||||||
|
res = new(dto.GetStyleOptionsRes)
|
||||||
|
res.Options = consts.GetAllStyleKeyValue()
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
|
stdhttp "net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
commonHttp "gitea.redpowerfuture.com/red-future/common/http"
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/utils"
|
||||||
|
"github.com/gogf/gf/v2/errors/gerror"
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
)
|
||||||
|
|
||||||
|
var commonHttpTransportMu sync.Mutex
|
||||||
|
|
||||||
|
// asyncCtx 异步上下文处理
|
||||||
|
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 user, uErr := utils.GetUserInfo(ctx); uErr == nil && user != nil {
|
||||||
|
asyncCtx = context.WithValue(asyncCtx, "user", user)
|
||||||
|
}
|
||||||
|
return asyncCtx
|
||||||
|
}
|
||||||
|
|
||||||
|
// setCommonHttpResponseHeaderTimeout 调整公共 HTTP 客户端响应头超时,避免长时推理被 30s 默认值打断。
|
||||||
|
func setCommonHttpResponseHeaderTimeout(d time.Duration) {
|
||||||
|
if d <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
commonHttpTransportMu.Lock()
|
||||||
|
defer commonHttpTransportMu.Unlock()
|
||||||
|
if tr, ok := commonHttp.Httpclient.Transport.(*stdhttp.Transport); ok && tr != nil {
|
||||||
|
if tr.ResponseHeaderTimeout < d {
|
||||||
|
tr.ResponseHeaderTimeout = d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// forwardHeaders 透传调用链路中必须的头信息,优先使用异步上下文里固化的 token。
|
||||||
|
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 r := g.RequestFromCtx(ctx); r != nil {
|
||||||
|
if headers["Authorization"] == "" {
|
||||||
|
if token := r.Header.Get("Authorization"); token != "" {
|
||||||
|
headers["Authorization"] = token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if userInfo := r.Header.Get("X-User-Info"); userInfo != "" {
|
||||||
|
headers["X-User-Info"] = userInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return headers
|
||||||
|
}
|
||||||
|
|
||||||
|
// commonPostJSON 使用 common/http 的底层客户端直连 JSON 接口,适配非统一响应包装结构。
|
||||||
|
func commonPostJSON(ctx context.Context, url string, headers map[string]string, req any, resp any) error {
|
||||||
|
client := commonHttp.Httpclient.Clone().ContentJson()
|
||||||
|
if deadline, ok := ctx.Deadline(); ok {
|
||||||
|
if d := time.Until(deadline); d > 0 {
|
||||||
|
client.SetTimeout(d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(headers) > 0 {
|
||||||
|
client.SetHeaderMap(headers)
|
||||||
|
}
|
||||||
|
r, err := client.DoRequest(ctx, stdhttp.MethodPost, url, req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer r.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
return gerror.Wrap(err, "读取响应失败")
|
||||||
|
}
|
||||||
|
if r.StatusCode != stdhttp.StatusOK {
|
||||||
|
return gerror.Newf("HTTP状态码异常: %d, body: %s", r.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, resp); err != nil {
|
||||||
|
return gerror.Wrapf(err, "解析响应失败, body: %s", string(body))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func commonPostMultipartFile(ctx context.Context, url string, headers map[string]string, form map[string]string, fileField string, filePath string, resp any) error {
|
||||||
|
body := &bytes.Buffer{}
|
||||||
|
writer := multipart.NewWriter(body)
|
||||||
|
|
||||||
|
for k, v := range form {
|
||||||
|
if v == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := writer.WriteField(k, v); err != nil {
|
||||||
|
return gerror.Wrapf(err, "写入表单字段失败: %s", k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return gerror.Wrapf(err, "打开文件失败: %s", filePath)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
part, err := writer.CreateFormFile(fileField, filepath.Base(filePath))
|
||||||
|
if err != nil {
|
||||||
|
return gerror.Wrapf(err, "创建表单文件失败: %s", fileField)
|
||||||
|
}
|
||||||
|
if _, err := io.Copy(part, f); err != nil {
|
||||||
|
return gerror.Wrap(err, "写入文件内容失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType := writer.FormDataContentType()
|
||||||
|
if err := writer.Close(); err != nil {
|
||||||
|
return gerror.Wrap(err, "关闭表单写入器失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
client := commonHttp.Httpclient.Clone()
|
||||||
|
if deadline, ok := ctx.Deadline(); ok {
|
||||||
|
if d := time.Until(deadline); d > 0 {
|
||||||
|
client.SetTimeout(d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if headers == nil {
|
||||||
|
headers = make(map[string]string)
|
||||||
|
}
|
||||||
|
headers["Content-Type"] = contentType
|
||||||
|
client.SetHeaderMap(headers)
|
||||||
|
|
||||||
|
r, err := client.DoRequest(ctx, stdhttp.MethodPost, url, body.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer r.Close()
|
||||||
|
|
||||||
|
raw, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
return gerror.Wrap(err, "读取响应失败")
|
||||||
|
}
|
||||||
|
if r.StatusCode != stdhttp.StatusOK {
|
||||||
|
return gerror.Newf("HTTP状态码异常: %d, body: %s", r.StatusCode, string(raw))
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw, resp); err != nil {
|
||||||
|
return gerror.Wrapf(err, "解析响应失败, body: %s", string(raw))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------- model-asynch 调用封装 --------------------------
|
||||||
|
|
||||||
|
const modelAsynchServiceName = "model-asynch"
|
||||||
|
|
||||||
|
type modelAsynchCreateTaskReq struct {
|
||||||
|
ModelName string `json:"modelName"`
|
||||||
|
InputRef string `json:"inputRef,omitempty"`
|
||||||
|
RequestPayload any `json:"requestPayload"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type modelAsynchCreateTaskRes struct {
|
||||||
|
TaskID string `json:"taskId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// createModelAsynchTask 调用 model-asynch 创建任务
|
||||||
|
// 注意:路由以 GoFrame 默认输出为准(通常为 /task/create-task)
|
||||||
|
func createModelAsynchTask(ctx context.Context, modelName string, payload any, inputRef string) (taskID string, err error) {
|
||||||
|
taskUrl := g.Cfg().MustGet(ctx, "model-asynch.addr", "127.0.0.1:8080")
|
||||||
|
headers := forwardHeaders(ctx)
|
||||||
|
req := &modelAsynchCreateTaskReq{
|
||||||
|
ModelName: modelName,
|
||||||
|
InputRef: inputRef,
|
||||||
|
RequestPayload: payload,
|
||||||
|
}
|
||||||
|
var res modelAsynchCreateTaskRes
|
||||||
|
if err := commonHttp.Post(ctx, fmt.Sprintf("%s/task/createTask", taskUrl), headers, &res, req); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return res.TaskID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type modelAsynchBatchReq struct {
|
||||||
|
TaskIDs []string `json:"taskIds"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type modelAsynchBatchItem struct {
|
||||||
|
TaskID string `json:"taskId"`
|
||||||
|
State int `json:"state"`
|
||||||
|
OssFile string `json:"ossFile"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type modelAsynchBatchRes struct {
|
||||||
|
List []modelAsynchBatchItem `json:"list"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// getModelAsynchTaskBatch 批量查询任务(成功 2->4 的逻辑由中间件内部处理)
|
||||||
|
func getModelAsynchTaskBatch(ctx context.Context, taskIDs []string) (items []modelAsynchBatchItem, err error) {
|
||||||
|
taskUrl := g.Cfg().MustGet(ctx, "model-asynch.addr", "127.0.0.1:8080")
|
||||||
|
headers := forwardHeaders(ctx)
|
||||||
|
req := &modelAsynchBatchReq{TaskIDs: taskIDs}
|
||||||
|
var res modelAsynchBatchRes
|
||||||
|
if err := commonHttp.Post(ctx, fmt.Sprintf("%s/task/getTaskBatch", taskUrl), headers, &res, req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return res.List, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
|
||||||
|
"ai-agent/digital-human/consts/public"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/errors/gerror"
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
)
|
||||||
|
|
||||||
|
type tts struct{}
|
||||||
|
|
||||||
|
// TTS 统一的模型异步调用封装(通过 model-asynch 中间件)
|
||||||
|
var TTS = new(tts)
|
||||||
|
|
||||||
|
// CreateVoiceDesignTask 设计音频任务(VoiceDesign)
|
||||||
|
func (s *tts) CreateVoiceDesignTask(
|
||||||
|
ctx context.Context,
|
||||||
|
text string,
|
||||||
|
instruct string,
|
||||||
|
language string, // 空则 Auto
|
||||||
|
speed float64, // <=0 则 1.0
|
||||||
|
) (taskID string, err error) {
|
||||||
|
if language == "" {
|
||||||
|
language = "Auto"
|
||||||
|
}
|
||||||
|
if speed <= 0 {
|
||||||
|
speed = 1.0
|
||||||
|
}
|
||||||
|
payload := map[string]any{
|
||||||
|
"text": text,
|
||||||
|
"language": language,
|
||||||
|
"instruct": instruct,
|
||||||
|
"speed": speed,
|
||||||
|
"response_format": "wav",
|
||||||
|
}
|
||||||
|
g.Log().Info(ctx, "[CreateVoiceDesignTask] %v", payload)
|
||||||
|
return createModelAsynchTask(ctx, public.ModelNameVoiceDesign, payload, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateCustomVoiceTask 预设音色(CustomVoice)任务
|
||||||
|
// - speaker: 预设说话人(如 Vivian/Serena/Ryan/...)
|
||||||
|
// - instruct: 可选,情绪/风格控制
|
||||||
|
func (s *tts) CreateCustomVoiceTask(
|
||||||
|
ctx context.Context,
|
||||||
|
text string,
|
||||||
|
speaker string,
|
||||||
|
language string, // 例如 "Chinese"/"English"/"Auto",空则默认 "Auto"
|
||||||
|
instruct string, // 可空
|
||||||
|
speed float64, // 0.5~2.0,<=0 则默认 1.0
|
||||||
|
) (taskID string, err error) {
|
||||||
|
if language == "" {
|
||||||
|
language = "Auto"
|
||||||
|
}
|
||||||
|
if speed <= 0 {
|
||||||
|
speed = 1.0
|
||||||
|
}
|
||||||
|
payload := map[string]any{
|
||||||
|
"text": text,
|
||||||
|
"language": language,
|
||||||
|
"speaker": speaker,
|
||||||
|
"instruct": instruct,
|
||||||
|
"speed": speed,
|
||||||
|
"response_format": "wav", // 建议统一用 wav
|
||||||
|
}
|
||||||
|
g.Log().Info(ctx, "[CreateCustomVoiceTask] %v", payload)
|
||||||
|
return createModelAsynchTask(ctx, public.ModelNameCustomVoice, payload, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateBaseTask 声音克隆(Base / clone)任务
|
||||||
|
// 说明:ref_audio_url 与 ref_audio_base64 二选一
|
||||||
|
func (s *tts) CreateBaseTask(
|
||||||
|
ctx context.Context,
|
||||||
|
text string,
|
||||||
|
language string, // 例如 "Chinese"/"English"/"Auto",空则默认 "Auto"
|
||||||
|
refText string, // 当 xVectorOnlyMode=false 时必填
|
||||||
|
refAudioURL string, // 可空
|
||||||
|
refAudioBase64 string, // 可空(不带 data: 前缀也可以)
|
||||||
|
xVectorOnlyMode bool, // true=不需要 refText,但质量可能下降
|
||||||
|
speed float64, // 0.5~2.0,<=0 则默认 1.0
|
||||||
|
) (taskID string, err error) {
|
||||||
|
if language == "" {
|
||||||
|
language = "Auto"
|
||||||
|
}
|
||||||
|
if speed <= 0 {
|
||||||
|
speed = 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := map[string]any{
|
||||||
|
"text": text,
|
||||||
|
"language": language,
|
||||||
|
"ref_text": refText,
|
||||||
|
"ref_audio_url": refAudioURL,
|
||||||
|
"ref_audio_base64": refAudioBase64,
|
||||||
|
"x_vector_only_mode": xVectorOnlyMode,
|
||||||
|
"speed": speed,
|
||||||
|
"response_format": "wav",
|
||||||
|
}
|
||||||
|
g.Log().Info(ctx, "[CreateBaseTask] %v", payload)
|
||||||
|
return createModelAsynchTask(ctx, public.ModelNameBase, payload, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SpeechToText 语音转文本(预留)
|
||||||
|
// audioBase64:base64 编码的音频数据(WAV/MP3等)
|
||||||
|
func (s *tts) SpeechToText(ctx context.Context, audioBase64 string) (text string, err error) {
|
||||||
|
_ = ctx
|
||||||
|
if audioBase64 == "" {
|
||||||
|
return "", gerror.New("audioBase64 不能为空")
|
||||||
|
}
|
||||||
|
// 简单校验 base64 合法性
|
||||||
|
if _, err := base64.StdEncoding.DecodeString(audioBase64); err != nil {
|
||||||
|
return "", gerror.Wrap(err, "audioBase64 非法")
|
||||||
|
}
|
||||||
|
return "", gerror.New("SpeechToText 暂未实现:后续接入语音识别模型后补齐")
|
||||||
|
}
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ai-agent/digital-human/consts"
|
||||||
|
"ai-agent/digital-human/dao"
|
||||||
|
"ai-agent/digital-human/model/dto"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/errors/gerror"
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
"github.com/gogf/gf/v2/os/gtime"
|
||||||
|
"github.com/gogf/gf/v2/util/gconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type video struct{}
|
||||||
|
|
||||||
|
// Video 视频服务
|
||||||
|
var Video = new(video)
|
||||||
|
|
||||||
|
// Create 创建视频
|
||||||
|
func (s *video) Create(ctx context.Context, req *dto.CreateVideoReq) (res *dto.CreateVideoRes, err error) {
|
||||||
|
// 验证数字人形象是否存在且启用
|
||||||
|
digitalHumanOne, err := dao.DigitalHuman.GetOne(ctx, req.DigitalHumanID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gerror.Wrap(err, "数字人形象不存在")
|
||||||
|
}
|
||||||
|
if digitalHumanOne.Status != consts.DigitalHumanStatusActive {
|
||||||
|
return nil, errors.New("数字人形象未启用")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证音频是否存在且已生成成功
|
||||||
|
audioOne, err := dao.Audio.GetOne(ctx, req.AudioID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gerror.Wrap(err, "音频不存在")
|
||||||
|
}
|
||||||
|
if audioOne.Status != consts.AudioStatusSuccess {
|
||||||
|
return nil, errors.New("音频未生成成功,无法合成视频")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建视频记录(初始状态为生成中)
|
||||||
|
ids, err := dao.Video.Insert(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存视频ID(PostgreSQL 使用 int64)
|
||||||
|
videoID := ids[0].(int64)
|
||||||
|
|
||||||
|
// 异步生成视频
|
||||||
|
go s.generateVideo(ctx, req.DigitalHumanID, digitalHumanOne.Name, req.AudioID, audioOne.AudioURL, audioOne.Duration, req.Resolution, videoID)
|
||||||
|
|
||||||
|
res = &dto.CreateVideoRes{
|
||||||
|
Id: videoID,
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateVideo 生成视频(异步处理)
|
||||||
|
func (s *video) generateVideo(ctx context.Context, digitalHumanID int64, digitalHumanName string, audioID int64, audioURL string, duration int, resolution consts.Resolution, videoID int64) {
|
||||||
|
// 更新视频状态,设置音频URL和时长
|
||||||
|
_, _ = dao.Video.UpdateStatus(ctx, videoID, consts.VideoStatusGenerating, "", audioURL, duration, "", "")
|
||||||
|
|
||||||
|
// 调用数字人形象与音频合成服务
|
||||||
|
videoURL, thumbnailURL, externalTaskID, err := s.synthesizeVideo(ctx, digitalHumanID, audioURL, resolution)
|
||||||
|
if err != nil {
|
||||||
|
// 视频合成失败
|
||||||
|
_, _ = dao.Video.UpdateStatus(ctx, videoID, consts.VideoStatusFailed, "视频合成失败: "+err.Error(), "", 0, "", "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新视频生成状态为成功
|
||||||
|
_, _ = dao.Video.UpdateStatus(ctx, videoID, consts.VideoStatusSuccess, "", videoURL, duration, thumbnailURL, externalTaskID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// synthesizeVideo 合成视频(模拟)
|
||||||
|
func (s *video) synthesizeVideo(ctx context.Context, digitalHumanID int64, audioURL string, resolution consts.Resolution) (videoURL string, thumbnailURL string, externalTaskID string, err error) {
|
||||||
|
// TODO: 调用真实的数字人视频合成服务API
|
||||||
|
// 这里模拟返回
|
||||||
|
g.Log().Info(ctx, "合成视频,数字人ID:", digitalHumanID, "音频URL:", audioURL, "分辨率:", resolution)
|
||||||
|
|
||||||
|
// 模拟外部任务ID(使用雪花算法或UUID)
|
||||||
|
externalTaskID = gconv.String(digitalHumanID) + "-" + gconv.String(gtime.Timestamp())
|
||||||
|
|
||||||
|
// 模拟视频URL(实际应该从视频合成服务获取)
|
||||||
|
videoURL = "https://example.com/video/" + externalTaskID + ".mp4"
|
||||||
|
|
||||||
|
// 模拟缩略图URL
|
||||||
|
thumbnailURL = "https://example.com/video/" + externalTaskID + "_thumb.jpg"
|
||||||
|
|
||||||
|
return videoURL, thumbnailURL, externalTaskID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// List 获取视频列表
|
||||||
|
func (s *video) List(ctx context.Context, req *dto.ListVideoReq) (res *dto.ListVideoRes, error error) {
|
||||||
|
videoList, total, err := dao.Video.List(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res = &dto.ListVideoRes{
|
||||||
|
Total: total,
|
||||||
|
}
|
||||||
|
b, err := json.Marshal(videoList)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(b, &res.List)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOne 获取单个视频
|
||||||
|
func (s *video) GetOne(ctx context.Context, id int64) (*dto.GetVideoRes, error) {
|
||||||
|
videoOne, err := dao.Video.GetOne(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var createdAt, updatedAt *gtime.Time
|
||||||
|
if videoOne.CreatedAt != nil {
|
||||||
|
createdAt = videoOne.CreatedAt
|
||||||
|
}
|
||||||
|
if videoOne.UpdatedAt != nil {
|
||||||
|
updatedAt = videoOne.UpdatedAt
|
||||||
|
}
|
||||||
|
return &dto.GetVideoRes{
|
||||||
|
ID: videoOne.Id,
|
||||||
|
Name: videoOne.Name,
|
||||||
|
Description: videoOne.Description,
|
||||||
|
DigitalHumanID: videoOne.DigitalHumanID,
|
||||||
|
DigitalHumanName: videoOne.DigitalHumanName,
|
||||||
|
AudioID: videoOne.AudioID,
|
||||||
|
AudioURL: "",
|
||||||
|
VideoURL: videoOne.VideoURL,
|
||||||
|
Status: videoOne.Status,
|
||||||
|
ErrorMsg: videoOne.ErrorMsg,
|
||||||
|
Duration: videoOne.Duration,
|
||||||
|
Resolution: videoOne.Resolution,
|
||||||
|
ThumbnailURL: videoOne.ThumbnailURL,
|
||||||
|
ExternalTaskID: videoOne.ExternalID,
|
||||||
|
CreatedAt: createdAt,
|
||||||
|
UpdatedAt: updatedAt,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update 更新视频
|
||||||
|
func (s *video) Update(ctx context.Context, req *dto.UpdateVideoReq) error {
|
||||||
|
// 先获取原始视频信息
|
||||||
|
videoOne, err := dao.Video.GetOne(ctx, req.ID)
|
||||||
|
if err != nil {
|
||||||
|
return gerror.Wrap(err, "获取原始视频信息失败")
|
||||||
|
}
|
||||||
|
// 修改字段
|
||||||
|
if !g.IsEmpty(req.Name) {
|
||||||
|
videoOne.Name = req.Name
|
||||||
|
}
|
||||||
|
if !g.IsEmpty(req.Description) {
|
||||||
|
videoOne.Description = req.Description
|
||||||
|
}
|
||||||
|
|
||||||
|
return dao.Video.Update(ctx, req.ID, videoOne)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete 删除视频
|
||||||
|
func (s *video) Delete(ctx context.Context, id int64) error {
|
||||||
|
return dao.Video.Delete(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate 重新生成视频
|
||||||
|
func (s *video) Generate(ctx context.Context, req *dto.GenerateVideoReq) (res *dto.GenerateVideoRes, err error) {
|
||||||
|
// 获取视频信息
|
||||||
|
videoOne, err := dao.Video.GetOne(ctx, req.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gerror.Wrap(err, "获取视频信息失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证音频是否仍然有效(已生成成功)
|
||||||
|
if videoOne.AudioID != 0 {
|
||||||
|
audioOne, err := dao.Audio.GetOne(ctx, videoOne.AudioID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gerror.Wrap(err, "获取音频信息失败")
|
||||||
|
}
|
||||||
|
if audioOne.Status != consts.AudioStatusSuccess {
|
||||||
|
return nil, errors.New("音频未生成成功,无法合成视频")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置状态为生成中
|
||||||
|
_, err = dao.Video.UpdateStatus(ctx, req.ID, consts.VideoStatusGenerating, "", "", 0, "", "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 异步重新生成视频
|
||||||
|
go s.generateVideo(ctx, videoOne.DigitalHumanID, videoOne.DigitalHumanName, videoOne.AudioID, "", videoOne.Duration, videoOne.Resolution, req.ID)
|
||||||
|
|
||||||
|
res = &dto.GenerateVideoRes{
|
||||||
|
TaskID: gconv.String(req.ID),
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStatusOptions 获取状态选项
|
||||||
|
func (s *video) GetStatusOptions(ctx context.Context, req *dto.GetVideoStatusOptionsReq) (res *dto.GetVideoStatusOptionsRes, err error) {
|
||||||
|
_ = ctx
|
||||||
|
_ = req
|
||||||
|
res = new(dto.GetVideoStatusOptionsRes)
|
||||||
|
res.Options = consts.GetAllVideoStatusKeyValue()
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetResolutionOptions 获取分辨率选项
|
||||||
|
func (s *video) GetResolutionOptions(ctx context.Context, req *dto.GetResolutionOptionsReq) (res *dto.GetResolutionOptionsRes, err error) {
|
||||||
|
_ = ctx
|
||||||
|
_ = req
|
||||||
|
res = new(dto.GetResolutionOptionsRes)
|
||||||
|
res.Options = consts.GetResolutionOptions()
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
+698
@@ -0,0 +1,698 @@
|
|||||||
|
# Qwen3-TTS Docker 快速部署文档
|
||||||
|
|
||||||
|
## 模型版本选择
|
||||||
|
|
||||||
|
**当前部署模型:Qwen3-TTS-24Hz-1.7B-Base-VoiceClone**
|
||||||
|
|
||||||
|
这是 Qwen3-TTS 系列中**功能最全面、音质最高**的模型版本,支持高质量声音克隆。
|
||||||
|
|
||||||
|
### 模型对比表
|
||||||
|
|
||||||
|
| 模型名称 | 采样率 | 参数量 | 体积 | 音质 | 速度 | 特殊功能 | 适用场景 |
|
||||||
|
|---------|--------|--------|------|------|------|---------|---------|
|
||||||
|
| Qwen3-TTS-12Hz-0.6B-CustomVoice | 12kHz | 0.6B | 1.7GB | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 9种预设声音 | 性能优先 |
|
||||||
|
| Qwen3-TTS-12Hz-1.7B-CustomVoice | 12kHz | 1.7B | ~4GB | ⭐⭐⭐⭐ | ⭐⭐⭐ | 9种预设声音 | 需要更高音质 |
|
||||||
|
| Qwen3-TTS-24Hz-0.6B-CustomVoice | 24kHz | 0.6B | ~2GB | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 9种预设声音 | 高质量需求 |
|
||||||
|
| Qwen3-TTS-24Hz-1.7B-CustomVoice | 24kHz | 1.7B | ~5GB | ⭐⭐⭐⭐⭐ | ⭐⭐ | 9种预设声音 | 最高音质(无克隆) |
|
||||||
|
| Qwen3-TTS-12Hz-Base-VoiceClone | 12kHz | 0.6B | ~2GB | ⭐⭐⭐ | ⭐⭐⭐⭐ | 声音克隆 | 自定义声音 |
|
||||||
|
| **Qwen3-TTS-24Hz-1.7B-Base-VoiceClone** | **24kHz** | **1.7B** | **~6.8GB** | **⭐⭐⭐⭐⭐** | **⭐⭐** | **声音克隆** | **功能最全面** |
|
||||||
|
|
||||||
|
**当前模型特点:**
|
||||||
|
- **24kHz 采样率**:双倍于 12Hz 模型,音质更清晰自然
|
||||||
|
- **1.7B 参数**:模型表达能力最强
|
||||||
|
- **声音克隆**:支持自定义声音训练和生成
|
||||||
|
- **功能最全面**:兼具基础模型 + 声音克隆功能
|
||||||
|
|
||||||
|
**推荐方案:**
|
||||||
|
- **默认当前选择**:`Qwen3-TTS-24Hz-1.7B-Base-VoiceClone`(功能最全面 + 最高音质)
|
||||||
|
- **性能优先**:`Qwen3-TTS-12Hz-0.6B-CustomVoice`
|
||||||
|
- **高质量(无克隆)**:`Qwen3-TTS-24Hz-1.7B-CustomVoice`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 快速开始(3 步部署)
|
||||||
|
|
||||||
|
### 步骤 1:下载模型
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 进入工作目录
|
||||||
|
cd ~/Qwen3-TTS
|
||||||
|
mkdir -p model
|
||||||
|
cd model
|
||||||
|
|
||||||
|
# 安装 ModelScope CLI(国内用户推荐)
|
||||||
|
python3 -m pip install -U modelscope
|
||||||
|
|
||||||
|
# 下载 Tokenizer(约 651MB)
|
||||||
|
modelscope download --model Qwen/Qwen3-TTS-Tokenizer-24Hz --local_dir ./Qwen3-TTS-Tokenizer-24Hz
|
||||||
|
|
||||||
|
# 下载 Qwen3-TTS-24Hz-1.7B-Base-VoiceClone 模型(约 6.8GB)
|
||||||
|
# 这是功能最全面、音质最高的模型,支持声音克隆
|
||||||
|
modelscope download --model Qwen/Qwen3-TTS-24Hz-1.7B-Base-VoiceClone --local_dir ./Qwen3-TTS-24Hz-1.7B-Base-VoiceClone
|
||||||
|
```
|
||||||
|
|
||||||
|
**说明:**
|
||||||
|
- **ModelScope**:阿里云模型托管平台,国内下载速度快
|
||||||
|
- **Tokenizer**:分词器,将文本转换为模型能理解的 token(24Hz 版本)
|
||||||
|
- **TTS 模型**:核心模型,1.7B 参数 + 24Hz 采样率 + 声音克隆功能
|
||||||
|
|
||||||
|
### 步骤 2:创建文件
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/Qwen3-TTS
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 创建 app.py(已配置为 24Hz-1.7B-Base-VoiceClone 模型)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cat > app.py << 'EOF'
|
||||||
|
from fastapi import FastAPI, Body
|
||||||
|
import soundfile as sf
|
||||||
|
import io
|
||||||
|
import torch
|
||||||
|
import sys
|
||||||
|
import base64
|
||||||
|
|
||||||
|
# 添加路径以支持导入
|
||||||
|
sys.path.insert(0, '/app')
|
||||||
|
|
||||||
|
from qwen_tts import Qwen3TTSModel
|
||||||
|
|
||||||
|
# ========== 当前配置:Qwen3-TTS-24Hz-1.7B-Base-VoiceClone ==========
|
||||||
|
# 功能最全面、音质最高的模型,支持声音克隆
|
||||||
|
# - 24kHz 采样率:音质更清晰自然
|
||||||
|
# - 1.7B 参数:模型表达能力最强
|
||||||
|
# - Base-VoiceClone:支持自定义声音克隆
|
||||||
|
MODEL_PATH = '/app/model/Qwen3-TTS-24Hz-1.7B-Base-VoiceClone'
|
||||||
|
TOKENIZER_PATH = '/app/model/Qwen3-TTS-Tokenizer-24Hz'
|
||||||
|
|
||||||
|
# 如需切换到其他模型,请修改以下路径并重新构建镜像
|
||||||
|
# ==========================================================
|
||||||
|
|
||||||
|
app = FastAPI(title="Qwen3-TTS API")
|
||||||
|
|
||||||
|
# 全局模型
|
||||||
|
model = None
|
||||||
|
|
||||||
|
@app.on_event("startup")
|
||||||
|
async def startup_event():
|
||||||
|
global model
|
||||||
|
print("正在加载 TTS 模型...")
|
||||||
|
print(f"模型路径: {MODEL_PATH}")
|
||||||
|
print(f"模型版本: Qwen3-TTS-24Hz-1.7B-Base-VoiceClone")
|
||||||
|
print("提示: 1.7B 模型加载需要较长时间,请耐心等待...")
|
||||||
|
model = Qwen3TTSModel.from_pretrained(
|
||||||
|
MODEL_PATH,
|
||||||
|
tokenizer_path=TOKENIZER_PATH,
|
||||||
|
device_map='cpu',
|
||||||
|
dtype=torch.float32
|
||||||
|
)
|
||||||
|
print("TTS 模型初始化完成")
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def root():
|
||||||
|
return {
|
||||||
|
"status": "running",
|
||||||
|
"mode": "production",
|
||||||
|
"model": "Qwen3-TTS-24Hz-1.7B-Base-VoiceClone",
|
||||||
|
"features": ["voice_clone", "high_quality", "24khz"]
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.post("/tts")
|
||||||
|
async def tts(text: str = Body(..., media_type='application/json')):
|
||||||
|
"""
|
||||||
|
注意:必须使用 Body(...) 解析请求体,否则会返回 422 错误
|
||||||
|
请求格式: POST /tts,Body 为 JSON 字符串 "文本内容"
|
||||||
|
|
||||||
|
超时设置:长文本推理可能需要较长时间,建议客户端设置超时时间 > 120 秒
|
||||||
|
CPU 推理速度:约 0.5-2 秒/字符(1.7B 模型较慢,比 0.6B 模型慢约 2-3 倍)
|
||||||
|
"""
|
||||||
|
if not model:
|
||||||
|
return {"code": 500, "msg": "模型未初始化"}
|
||||||
|
|
||||||
|
try:
|
||||||
|
print(f"收到TTS请求,文本长度: {len(text)} 字符")
|
||||||
|
wavs, sr = model.generate_custom_voice(text, speaker='serena')
|
||||||
|
print(f"音频生成完成,采样率: {sr}, 音频时长: {len(wavs[0])/sr:.2f}秒")
|
||||||
|
|
||||||
|
# 转换为 WAV 格式
|
||||||
|
buffer = io.BytesIO()
|
||||||
|
sf.write(buffer, wavs[0], sr, format='WAV')
|
||||||
|
buffer.seek(0)
|
||||||
|
|
||||||
|
audio_data = buffer.read()
|
||||||
|
audio_b64 = base64.b64encode(audio_data).decode('utf-8')
|
||||||
|
print(f"编码完成,base64 长度: {len(audio_b64)}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"code": 0,
|
||||||
|
"msg": "success",
|
||||||
|
"text": text,
|
||||||
|
"audio": audio_b64
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
print(f"Error: {traceback.format_exc()}")
|
||||||
|
return {"code": 500, "msg": f"TTS处理错误: {str(e)}"}
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 创建 requirements.txt
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cat > requirements.txt << 'EOF'
|
||||||
|
fastapi>=0.104.0
|
||||||
|
uvicorn>=0.24.0
|
||||||
|
numpy>=1.24.0
|
||||||
|
torch>=2.0.0
|
||||||
|
librosa>=0.10.0
|
||||||
|
soundfile>=0.12.0
|
||||||
|
safetensors>=0.4.0
|
||||||
|
qwen-tts>=0.1.0
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 创建 Dockerfile
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cat > Dockerfile.bak << 'EOF'
|
||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 安装系统依赖
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
git \
|
||||||
|
ffmpeg \
|
||||||
|
libsndfile1 \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# 安装 qwen-tts(包含 qwen_tts 模块)
|
||||||
|
RUN pip install --no-cache-dir qwen-tts
|
||||||
|
|
||||||
|
# 复制应用文件
|
||||||
|
COPY app.py .
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
# 安装额外依赖
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
# 增加超时设置(180秒,1.7B 模型需要更长时间)
|
||||||
|
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000", "--timeout-keep-alive", "180", "--limit-concurrency", "1"]
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤 3:构建并启动
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/Qwen3-TTS
|
||||||
|
|
||||||
|
# 构建镜像
|
||||||
|
docker build -t qwen3-tts:latest .
|
||||||
|
|
||||||
|
# 启动容器(挂载模型目录,增加内存限制)
|
||||||
|
docker run -d \
|
||||||
|
--name tts-service \
|
||||||
|
-p 8000:8000 \
|
||||||
|
-v ~/Qwen3-TTS/model:/app/model:ro \
|
||||||
|
--memory="8g" \
|
||||||
|
--restart unless-stopped \
|
||||||
|
qwen3-tts:latest
|
||||||
|
|
||||||
|
# 等待服务启动(1.7B 模型需要更长时间,约15-30秒)
|
||||||
|
sleep 20
|
||||||
|
|
||||||
|
# 验证服务
|
||||||
|
curl http://localhost:8000/
|
||||||
|
```
|
||||||
|
|
||||||
|
**预期输出:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "running",
|
||||||
|
"mode": "production",
|
||||||
|
"model": "Qwen3-TTS-24Hz-1.7B-Base-VoiceClone",
|
||||||
|
"features": ["voice_clone", "high_quality", "24khz"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 切换到其他模型版本
|
||||||
|
|
||||||
|
### 切换到性能优先模型(12Hz-0.6B-CustomVoice)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 下载新模型
|
||||||
|
cd ~/Qwen3-TTS/model
|
||||||
|
modelscope download --model Qwen/Qwen3-TTS-12Hz-0.6B-CustomVoice --local_dir ./Qwen3-TTS-12Hz-0.6B-CustomVoice
|
||||||
|
modelscope download --model Qwen/Qwen3-TTS-Tokenizer-12Hz --local_dir ./Qwen3-TTS-Tokenizer-12Hz
|
||||||
|
|
||||||
|
# 2. 修改 app.py 中的 MODEL_PATH 和 TOKENIZER_PATH
|
||||||
|
# MODEL_PATH = '/app/model/Qwen3-TTS-12Hz-0.6B-CustomVoice'
|
||||||
|
# TOKENIZER_PATH = '/app/model/Qwen3-TTS-Tokenizer-12Hz'
|
||||||
|
|
||||||
|
# 3. 重新构建并启动
|
||||||
|
cd ~/Qwen3-TTS
|
||||||
|
docker stop tts-service
|
||||||
|
docker build -t qwen3-tts:v12hz .
|
||||||
|
docker run -d --name tts-service -p 8000:8000 -v ~/Qwen3-TTS/model:/app/model:ro qwen3-tts:v12hz
|
||||||
|
```
|
||||||
|
|
||||||
|
### 切换到最高音质模型(24Hz-1.7B-CustomVoice,无声音克隆)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 下载新模型
|
||||||
|
cd ~/Qwen3-TTS/model
|
||||||
|
modelscope download --model Qwen/Qwen3-TTS-24Hz-1.7B-CustomVoice --local_dir ./Qwen3-TTS-24Hz-1.7B-CustomVoice
|
||||||
|
|
||||||
|
# 2. 修改 app.py 中的路径
|
||||||
|
# MODEL_PATH = '/app/model/Qwen3-TTS-24Hz-1.7B-CustomVoice'
|
||||||
|
# TOKENIZER_PATH = '/app/model/Qwen3-TTS-Tokenizer-24Hz'
|
||||||
|
|
||||||
|
# 3. 重新构建并启动
|
||||||
|
cd ~/Qwen3-TTS
|
||||||
|
docker stop tts-service
|
||||||
|
docker build -t qwen3-tts:v24hz-noclone .
|
||||||
|
docker run -d --name tts-service -p 8000:8000 -v ~/Qwen3-TTS/model:/app/model:ro qwen3-tts:v24hz-noclone
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API 使用
|
||||||
|
|
||||||
|
### 健康检查
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8000/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 文本转语音
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/tts \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '"你好,这是一个测试"'
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意:** Body 必须是 JSON 字符串格式,不能是 JSON 对象 `{"text": "..."}`
|
||||||
|
|
||||||
|
**响应示例:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"msg": "success",
|
||||||
|
"text": "你好,这是一个测试",
|
||||||
|
"audio": "UklGRiTCAQBXQVZFZm10IBAAAAABAAEAwF0AAIC7AAACABAAZGF0YQD..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 长文本处理
|
||||||
|
|
||||||
|
**重要提示:**
|
||||||
|
- 1.7B 模型推理速度较慢(约 1-3 秒/字符)
|
||||||
|
- 短文本(< 20 字):约 15-30 秒
|
||||||
|
- 中等文本(20-50 字):约 50-120 秒
|
||||||
|
- 长文本(50-100 字):约 120-240 秒
|
||||||
|
|
||||||
|
**客户端必须设置超时时间 >= 180 秒**,否则会收到 `EOF` 错误。
|
||||||
|
|
||||||
|
### Go 调用示例(带超时设置)
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TTSResponse struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
Audio string `json:"audio"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TTS(text string) ([]byte, error) {
|
||||||
|
// 必须使用 JSON 字符串格式:`"文本内容"`
|
||||||
|
jsonText := fmt.Sprintf(`"%s"`, text)
|
||||||
|
|
||||||
|
// 创建带超时的 HTTP 客户端(180秒超时,1.7B 模型需要更长时间)
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: 180 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Post(
|
||||||
|
"http://localhost:8000/tts",
|
||||||
|
"application/json",
|
||||||
|
bytes.NewBufferString(jsonText),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var result TTSResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Code != 0 {
|
||||||
|
return nil, fmt.Errorf("TTS error: %s", result.Msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return base64.StdEncoding.DecodeString(result.Audio)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// 长文本测试
|
||||||
|
longText := "欢迎使用红动未来数字人服务平台,我们将为您提供最优质的AI数字人解决方案。人工智能技术正在改变我们的生活,让我们一起探索未来的无限可能。"
|
||||||
|
|
||||||
|
audio, err := TTS(longText)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
os.WriteFile("output.wav", audio, 0644)
|
||||||
|
fmt.Println("音频已保存到 output.wav")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Python 调用示例(带超时设置)
|
||||||
|
|
||||||
|
```python
|
||||||
|
import requests
|
||||||
|
import base64
|
||||||
|
|
||||||
|
def tts(text, timeout=180):
|
||||||
|
"""
|
||||||
|
TTS 文本转语音
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: 要转换的文本
|
||||||
|
timeout: 超时时间(秒),1.7B 模型建议 >= 180 秒
|
||||||
|
"""
|
||||||
|
# 必须使用 JSON 字符串格式
|
||||||
|
response = requests.post(
|
||||||
|
"http://localhost:8000/tts",
|
||||||
|
data=f'"{text}"',
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
timeout=timeout # 设置超时
|
||||||
|
)
|
||||||
|
|
||||||
|
result = response.json()
|
||||||
|
if result["code"] != 0:
|
||||||
|
raise Exception(f"TTS error: {result['msg']}")
|
||||||
|
|
||||||
|
return base64.b64decode(result["audio"])
|
||||||
|
|
||||||
|
# 使用
|
||||||
|
short_text = "你好,这是一个测试"
|
||||||
|
long_text = "欢迎使用红动未来数字人服务平台,我们将为您提供最优质的AI数字人解决方案。"
|
||||||
|
|
||||||
|
# 短文本(30秒超时)
|
||||||
|
audio_data = tts(short_text, timeout=30)
|
||||||
|
with open("short_output.wav", "wb") as f:
|
||||||
|
f.write(audio_data)
|
||||||
|
|
||||||
|
# 长文本(180秒超时)
|
||||||
|
audio_data = tts(long_text, timeout=180)
|
||||||
|
with open("long_output.wav", "wb") as f:
|
||||||
|
f.write(audio_data)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 服务管理
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看日志
|
||||||
|
docker logs -f tts-service
|
||||||
|
|
||||||
|
# 查看最近50行日志
|
||||||
|
docker logs --tail 50 tts-service
|
||||||
|
|
||||||
|
# 停止服务
|
||||||
|
docker stop tts-service
|
||||||
|
|
||||||
|
# 启动服务
|
||||||
|
docker start tts-service
|
||||||
|
|
||||||
|
# 重启服务
|
||||||
|
docker restart tts-service
|
||||||
|
|
||||||
|
# 删除容器
|
||||||
|
docker stop tts-service && docker rm tts-service
|
||||||
|
|
||||||
|
# 删除镜像
|
||||||
|
docker rmi qwen3-tts:latest
|
||||||
|
|
||||||
|
# 进入容器
|
||||||
|
docker exec -it tts-service /bin/bash
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 故障排查
|
||||||
|
|
||||||
|
### 1. 端口被占用
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查找占用 8000 端口的进程
|
||||||
|
lsof -ti:8000
|
||||||
|
|
||||||
|
# 停止占用端口的进程
|
||||||
|
lsof -ti:8000 | xargs kill -9
|
||||||
|
|
||||||
|
# 或修改映射端口
|
||||||
|
docker run -d -p 8001:8000 --name tts-service qwen3-tts:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. API 返回 422 错误
|
||||||
|
|
||||||
|
**原因:** 请求格式不正确,必须使用 JSON 字符串格式
|
||||||
|
|
||||||
|
**正确请求:**
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/tts \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '"你好"'
|
||||||
|
```
|
||||||
|
|
||||||
|
**错误请求:**
|
||||||
|
```bash
|
||||||
|
# ❌ 错误:这是 JSON 对象,不是字符串
|
||||||
|
curl -X POST http://localhost:8000/tts \
|
||||||
|
-d '{"text": "你好"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 长文本返回 EOF 错误
|
||||||
|
|
||||||
|
**原因:** 1.7B 模型推理更慢,长文本处理时间超过客户端超时时间
|
||||||
|
|
||||||
|
**解决方案:**
|
||||||
|
1. **客户端设置超时 >= 180 秒**
|
||||||
|
2. **缩短文本长度**(建议单次请求 < 100 字)
|
||||||
|
3. **使用 GPU 加速**(如果可用)
|
||||||
|
4. **切换到 0.6B 模型**(更快但质量略低)
|
||||||
|
|
||||||
|
**Go 客户端:**
|
||||||
|
```go
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: 180 * time.Second, // 设置 180 秒超时
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Python 客户端:**
|
||||||
|
```python
|
||||||
|
response = requests.post(
|
||||||
|
"http://localhost:8000/tts",
|
||||||
|
data=f'"{text}"',
|
||||||
|
timeout=180 # 设置 180 秒超时
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 音频无声音或文件过小
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看日志检查模型是否加载
|
||||||
|
docker logs tts-service | grep "TTS 模型初始化完成"
|
||||||
|
|
||||||
|
# 检查模型文件
|
||||||
|
docker exec tts-service ls -la /app/model/
|
||||||
|
|
||||||
|
# 测试 API 返回的音频数据大小(应该 > 10KB)
|
||||||
|
curl -s http://localhost:8000/tts -d '"测试"' | python3 -c "import json,sys; d=json.load(sys.stdin); print(len(d['audio']))"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 内存不足
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 增加内存限制(1.7B 模型建议 >= 8GB)
|
||||||
|
docker run -d --name tts-service -p 8000:8000 --memory="8g" qwen3-tts:latest
|
||||||
|
|
||||||
|
# 或增加更多内存
|
||||||
|
docker run -d --name tts-service -p 8000:8000 --memory="12g" qwen3-tts:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. 服务启动后无法访问
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查容器状态
|
||||||
|
docker ps | grep tts-service
|
||||||
|
|
||||||
|
# 检查端口映射
|
||||||
|
docker port tts-service
|
||||||
|
|
||||||
|
# 检查服务是否正常响应
|
||||||
|
curl http://localhost:8000/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. 推理速度过慢
|
||||||
|
|
||||||
|
**1.7B 模型优化方案:**
|
||||||
|
```bash
|
||||||
|
# 限制并发为 1(避免 CPU 争抢)
|
||||||
|
docker run -d --name tts-service -p 8000:8000 qwen3-tts:latest \
|
||||||
|
uvicorn app:app --limit-concurrency 1
|
||||||
|
|
||||||
|
# 增加 CPU 资源
|
||||||
|
docker run -d --name tts-service -p 8000:8000 --cpus="8.0" qwen3-tts:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
**GPU 加速(需要 NVIDIA GPU):**
|
||||||
|
```bash
|
||||||
|
# 修改 app.py 中的 device_map='cpu' 为 device_map='cuda:0'
|
||||||
|
# 重新构建镜像并运行
|
||||||
|
docker run -d --name tts-service --gpus all -p 8000:8000 qwen3-tts:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
**切换到更快的模型:**
|
||||||
|
- 如果对速度要求高,可切换到 `Qwen3-TTS-12Hz-0.6B-CustomVoice`
|
||||||
|
- 推理速度可提升 3-5 倍
|
||||||
|
|
||||||
|
### 8. 模型加载失败
|
||||||
|
|
||||||
|
**检查模型路径:**
|
||||||
|
```bash
|
||||||
|
# 查看容器内模型目录
|
||||||
|
docker exec tts-service ls -la /app/model/
|
||||||
|
|
||||||
|
# 确认 app.py 中的 MODEL_PATH 和 TOKENIZER_PATH 正确
|
||||||
|
docker exec tts-service cat /app/app.py | grep "MODEL_PATH\|TOKENIZER_PATH"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 常见问题 FAQ
|
||||||
|
|
||||||
|
**Q: 为什么选择 Qwen3-TTS-24Hz-1.7B-Base-VoiceClone?**
|
||||||
|
|
||||||
|
A: 这是 Qwen3-TTS 系列中功能最全面、音质最高的模型:
|
||||||
|
- **24kHz 采样率**:双倍于 12Hz 模型,音质更清晰自然
|
||||||
|
- **1.7B 参数**:模型表达能力最强
|
||||||
|
- **声音克隆**:支持自定义声音训练和生成
|
||||||
|
|
||||||
|
**Q: 1.7B 模型推理速度慢怎么办?**
|
||||||
|
|
||||||
|
A: 可以采取以下措施:
|
||||||
|
1. 客户端设置超时 >= 180 秒
|
||||||
|
2. 使用 GPU 加速(速度提升 10-20 倍)
|
||||||
|
3. 切换到 0.6B 模型(速度提升 3-5 倍)
|
||||||
|
4. 缩短单次请求文本长度
|
||||||
|
|
||||||
|
**Q: 为什么 Body 必须是 JSON 字符串而不是 JSON 对象?**
|
||||||
|
|
||||||
|
A: FastAPI 使用 `Body(..., media_type='application/json')` 解析时,直接接收 JSON 字符串。如果使用 `{"text": "..."}` 格式,需要修改 `app.py` 使用 Pydantic 模型。当前实现更简洁,直接传递字符串即可。
|
||||||
|
|
||||||
|
**Q: 24Hz 和 12Hz 模型有什么区别?**
|
||||||
|
|
||||||
|
A:
|
||||||
|
- **24Hz**:采样率 24kHz,音质更清晰自然,适合高质量需求
|
||||||
|
- **12Hz**:采样率 12kHz,推理速度快,适合实时应用
|
||||||
|
|
||||||
|
**Q: 1.7B 和 0.6B 模型有什么区别?**
|
||||||
|
|
||||||
|
A:
|
||||||
|
- **1.7B**:参数量更大,音质更高,但推理速度慢,内存占用大(推荐 GPU)
|
||||||
|
- **0.6B**:参数量小,推理快,内存占用少,适合 CPU 环境
|
||||||
|
|
||||||
|
**Q: CustomVoice 和 Base 模型有什么区别?**
|
||||||
|
|
||||||
|
A:
|
||||||
|
- **CustomVoice**:内置 9 种预设声音,开箱即用
|
||||||
|
- **Base**:基础模型,支持自定义训练和声音克隆
|
||||||
|
|
||||||
|
**Q: 长文本推理需要多长时间?**
|
||||||
|
|
||||||
|
A: 1.7B 模型 CPU 推理速度约 1-3 秒/字符:
|
||||||
|
- 短文本(< 20 字):约 15-30 秒
|
||||||
|
- 中等文本(20-50 字):约 50-120 秒
|
||||||
|
- 长文本(50-100 字):约 120-240 秒
|
||||||
|
|
||||||
|
**Q: 为什么长文本会返回 EOF 错误?**
|
||||||
|
|
||||||
|
A: 1.7B 模型推理时间长,如果客户端超时时间设置过短会断开连接。解决方案:
|
||||||
|
1. 客户端设置超时 >= 180 秒
|
||||||
|
2. 缩短单次请求文本长度
|
||||||
|
3. 使用 GPU 加速
|
||||||
|
4. 切换到 0.6B 模型
|
||||||
|
|
||||||
|
**Q: 支持哪些声音?**
|
||||||
|
|
||||||
|
A: CustomVoice 模型支持 9 种预设声音:serena, vivian, uncle_fu, ryan, aiden, ono_anna, sohee, eric, dylan
|
||||||
|
|
||||||
|
**Q: 可以自定义声音吗?**
|
||||||
|
|
||||||
|
A: 可以,Base-VoiceClone 模型支持声音克隆功能,详见 Qwen3-TTS 官方文档
|
||||||
|
|
||||||
|
**Q: 支持哪些语言?**
|
||||||
|
|
||||||
|
A: 中文、英文、日语、韩语、德语、法语、俄语、葡萄牙语、西班牙语、意大利语
|
||||||
|
|
||||||
|
**Q: 音频采样率是多少?**
|
||||||
|
|
||||||
|
A: 24kHz (24000 Hz) - 比标准 CD 音质(44.1kHz)略低,但比 12Hz 模型清晰很多
|
||||||
|
|
||||||
|
**Q: 生成的音频文件格式是什么?**
|
||||||
|
|
||||||
|
A: WAV 格式,Microsoft PCM, 16 bit, mono
|
||||||
|
|
||||||
|
**Q: 如何提高推理速度?**
|
||||||
|
|
||||||
|
A:
|
||||||
|
1. 使用 GPU 加速(device_map='cuda:0')
|
||||||
|
2. 使用更小的模型(0.6B 而非 1.7B)
|
||||||
|
3. 使用 12Hz 模型而非 24Hz 模型
|
||||||
|
4. 限制并发请求(limit-concurrency=1)
|
||||||
|
5. 增加 CPU 核心数(--cpus="8.0")
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
~/Qwen3-TTS/
|
||||||
|
├── app.py # FastAPI 服务代码
|
||||||
|
├── Dockerfile # Docker 镜像构建文件
|
||||||
|
├── requirements.txt # Python 依赖
|
||||||
|
└── model/ # 模型文件目录
|
||||||
|
├── Qwen3-TTS-Tokenizer-24Hz/ # 24Hz 分词器(651MB)
|
||||||
|
└── Qwen3-TTS-24Hz-1.7B-Base-VoiceClone/ # 24Hz 1.7B 模型(6.8GB)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 相关链接
|
||||||
|
|
||||||
|
- 官方文档:https://github.com/QwenLM/Qwen3-TTS
|
||||||
|
- ModelScope:https://modelscope.cn/models?name=Qwen3-TTS
|
||||||
|
- FastAPI 文档:https://fastapi.tiangolo.com/
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package gateway
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/oss"
|
||||||
|
"github.com/gogf/gf/v2/errors/gerror"
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Upload 上传文件字节到 OSS,返回可访问 URL。
|
||||||
|
// 原签名基于 workflow/model/dto 的 UploadFileBytesReq/Res,抽离时简化为直接传文件名与字节,便于业务侧解耦 workflow。
|
||||||
|
// 统一走 common/oss:multipart field=file、X-User-Info 三态注入(透传请求头 / ctx 注入 user / 解析 token)。
|
||||||
|
func Upload(ctx context.Context, fileName string, fileBytes []byte) (string, error) {
|
||||||
|
res, err := oss.UploadFileBytes(ctx, fileName, fileBytes)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return res.FileURL, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFileBytesFromURL 下载远程文件内容(把 filePrefix 前缀替换为 minioPrefix 后经 GoFrame 客户端下载)
|
||||||
|
func GetFileBytesFromURL(ctx context.Context, fileUrl string) ([]byte, error) {
|
||||||
|
newS := strings.ReplaceAll(fileUrl, g.Cfg().MustGet(ctx, "filePrefix").String(), g.Cfg().MustGet(ctx, "minioPrefix").String())
|
||||||
|
// 使用 GoFrame 客户端(自带超时、追踪、日志等能力)
|
||||||
|
resp, err := g.Client().Get(ctx, newS)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gerror.Wrapf(err, "failed to request url: %s", newS)
|
||||||
|
}
|
||||||
|
defer resp.Close()
|
||||||
|
|
||||||
|
// 校验状态码
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, gerror.Newf("request failed with status code: %d, url: %s", resp.StatusCode, newS)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取全部内容
|
||||||
|
allBytes, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gerror.Wrapf(err, "failed to read response body, url: %s", fileUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
return allBytes, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
// Package gateway ai-agent 对外部服务的调用层:模型网关(model-gateway)的查询/提交/聊天调用,以及文件下载/上传(OSS)。
|
||||||
|
// 从 workflow/service/flow 抽离为独立目录,供工作流节点与视频管线(video/)共用,业务侧不再直接依赖 workflow 引擎。
|
||||||
|
package gateway
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"ai-agent/workflow/consts/model"
|
||||||
|
"ai-agent/workflow/consts/public"
|
||||||
|
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/beans"
|
||||||
|
commonHttp "gitea.redpowerfuture.com/red-future/common/http"
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/utils"
|
||||||
|
gmq "github.com/bjang03/gmq/core/gmq"
|
||||||
|
"github.com/bjang03/gmq/mq"
|
||||||
|
"github.com/bjang03/gmq/types"
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
"github.com/gogf/gf/v2/util/gconv"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ListModelManageReq 配置列表
|
||||||
|
type ListModelManageReq struct {
|
||||||
|
*beans.Page `json:"page"`
|
||||||
|
ModelName string `p:"modelName" json:"modelName" dc:"模型名称(模糊查询,可选)"`
|
||||||
|
ModelType int `p:"modelType" json:"modelType" dc:"模型类型"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModelManageListItem 模型配置列表项。listModelManage 返回平铺的 model-gateway entity(id 在顶层),
|
||||||
|
// 与 getModelManage 的 {modelManage:{...}} 嵌套不同,故独立成结构而非复用 GetModelInfoByIdRes。
|
||||||
|
type ModelManageListItem struct {
|
||||||
|
Id int64 `json:"id" dc:"配置ID"`
|
||||||
|
ModelName string `json:"modelName" dc:"模型名称"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListModelManageRes struct {
|
||||||
|
List []*ModelManageListItem `json:"list" dc:"列表数据"`
|
||||||
|
Total int `json:"total" dc:"总数"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetModelInfoByIdReq 查询模型配置
|
||||||
|
type GetModelInfoByIdReq struct {
|
||||||
|
ModelId int64 `json:"id" dc:"模型ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetModelInfoByIdRes struct {
|
||||||
|
ModelManage struct {
|
||||||
|
Id int64 `json:"id" dc:"模型ID"`
|
||||||
|
ModelType model.ModelType `json:"modelType" description:"模型类型"`
|
||||||
|
ModelName string `json:"modelName" description:"模型名称"`
|
||||||
|
MaxTokens int `json:"maxTokens" description:"最大token数"`
|
||||||
|
ChatModel *bool `json:"chatModel" description:"是否聊天模型"`
|
||||||
|
ResponseType model.ResponseType `json:"responseType" description:"返回类型:1同步,2异步,3流"`
|
||||||
|
Enabled *bool `json:"enabled" description:"是否启用"`
|
||||||
|
RequestBodyMapping map[string]any `json:"requestBodyMapping" description:"请求体映射(约等于视频模型 schema)"`
|
||||||
|
RequestBusinessFieldMapping map[string]string `json:"requestBusinessFieldMapping" description:"请求业务字段映射"`
|
||||||
|
MinDuration int `json:"minDuration" description:"最小时长(秒)"`
|
||||||
|
MaxDuration int `json:"maxDuration" description:"最大时长(秒)"`
|
||||||
|
} `json:"modelManage" dc:"模型配置"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// modelCallTopic 生成异步模型消息主题:带业务标识(bizName/modelId/sessionId)便于排查,
|
||||||
|
// uuid 保证每次调用唯一,避免并发调用共用同一主题导致结果串扰
|
||||||
|
func modelCallTopic(bizName string, modelId int64, sessionId string) string {
|
||||||
|
return fmt.Sprintf("model-call-%s-%d-%s-%s", bizName, modelId, sessionId, uuid.NewString())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModelCallReq 调用模型网关
|
||||||
|
type ModelCallReq struct {
|
||||||
|
ModelId int64 `json:"modelId" dc:"模型ID"`
|
||||||
|
BizName string `json:"bizName" dc:"业务名称(调用方模块/系统,用于统计)"`
|
||||||
|
SessionId string `json:"sessionId" dc:"会话ID"`
|
||||||
|
RequestParams map[string]any `json:"requestParams" dc:"请求参数"`
|
||||||
|
BusinessParams map[string]any `json:"businessParams" dc:"业务参数"`
|
||||||
|
MsgTopic string `json:"msgTopic" dc:"消息主题(异步必要参数)"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ModelCallRes struct {
|
||||||
|
TaskId int64 `json:"id" dc:"任务ID"`
|
||||||
|
State int8 `json:"state" dc:"状态"`
|
||||||
|
ModelId int64 `json:"modelId" dc:"生效模型ID(引用行=解析后的系统模型ID,计价按此)"`
|
||||||
|
MediaType string `json:"mediaType" dc:"输入媒体类型(shop词汇: text/audio/video)"`
|
||||||
|
TotalTokens int64 `json:"totalTokens" dc:"总token"`
|
||||||
|
PromptTokens int64 `json:"promptTokens" dc:"输入token"`
|
||||||
|
CompletionTokens int64 `json:"completionTokens" dc:"输出token"`
|
||||||
|
Tools []ModelTool `json:"tools" dc:"工具"`
|
||||||
|
Content map[string]any `json:"content" dc:"内容"`
|
||||||
|
Cost float64 `json:"cost" dc:"费用(元)"`
|
||||||
|
Duration int64 `json:"duration" dc:"时长(秒)"`
|
||||||
|
ErrorMsg string `json:"errorMsg" dc:"错误消息"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ModelTool struct {
|
||||||
|
Id string `json:"id" dc:"工具ID"`
|
||||||
|
Type string `json:"type" dc:"工具类型"`
|
||||||
|
Function struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Arguments string `json:"arguments"`
|
||||||
|
} `json:"function"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListModelManage 配置列表
|
||||||
|
func ListModelManage(ctx context.Context, req *ListModelManageReq) (res *ListModelManageRes, err error) {
|
||||||
|
res = new(ListModelManageRes)
|
||||||
|
err = commonHttp.Get(ctx, "model-gateway/model/manage/listModelManage", utils.HeadersFromCtx(ctx, utils.HeadersOptions{TokenFromQuery: true}), res, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetModelInfoById 查询模型配置
|
||||||
|
func GetModelInfoById(ctx context.Context, req *GetModelInfoByIdReq) (res *GetModelInfoByIdRes, err error) {
|
||||||
|
res = new(GetModelInfoByIdRes)
|
||||||
|
err = commonHttp.Get(ctx, "model-gateway/model/manage/getModelManage", utils.HeadersFromCtx(ctx, utils.HeadersOptions{TokenFromQuery: true}), res, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubmitModelCall 提交模型调用(异步模型自动生成唯一 msgTopic),返回提交响应与消息主题。
|
||||||
|
// taskId 在 res.TaskId;同步模型 msgTopic 为空串。结果等待由 WaitModelCallResult 完成。
|
||||||
|
func SubmitModelCall(ctx context.Context, modelId int64, responseType model.ResponseType, sessionId string, requestParams map[string]any, businessParams map[string]any) (res *ModelCallRes, msgTopic string, err error) {
|
||||||
|
// 异步模型必须绑定消息主题接收结果:自动生成唯一主题,
|
||||||
|
// 带业务标识(bizName/modelId/sessionId)便于排查,每次调用唯一避免并发串结果
|
||||||
|
if responseType != nil && *responseType == *model.ResponseTypeAsync.Code() {
|
||||||
|
msgTopic = modelCallTopic(g.Cfg().MustGet(ctx, "server.name").String(), modelId, sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := ModelCallReq{
|
||||||
|
ModelId: modelId,
|
||||||
|
BizName: g.Cfg().MustGet(ctx, "server.name").String(),
|
||||||
|
SessionId: sessionId,
|
||||||
|
RequestParams: requestParams,
|
||||||
|
BusinessParams: businessParams,
|
||||||
|
MsgTopic: msgTopic,
|
||||||
|
}
|
||||||
|
// 克隆 commonHttp 客户端(保留 Consul 服务发现),显式设置超时和 ResponseHeaderTimeout
|
||||||
|
client := commonHttp.Httpclient.Clone()
|
||||||
|
client.SetTimeout(30 * time.Minute)
|
||||||
|
if tr, ok := client.Transport.(*http.Transport); ok {
|
||||||
|
tr.ResponseHeaderTimeout = 30 * time.Minute
|
||||||
|
}
|
||||||
|
|
||||||
|
res = new(ModelCallRes)
|
||||||
|
err = commonHttp.Post(ctx, "model-gateway/model/call/modelCall", utils.HeadersFromCtx(ctx, utils.HeadersOptions{TokenFromQuery: true}), res, &req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
if g.IsEmpty(res.TaskId) || !g.IsEmpty(res.ErrorMsg) {
|
||||||
|
return nil, "", fmt.Errorf("创建模型任务失败:%v", res.ErrorMsg)
|
||||||
|
}
|
||||||
|
return res, msgTopic, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WaitModelCallResult 订阅 msgTopic 等待异步模型结果。
|
||||||
|
// 重订阅可拿回已发布结果:gmq NATS 侧 Retention=LimitsPolicy+MaxAge=7天(消息不随 ack 删除、保留 7 天),
|
||||||
|
// 订阅用 DeliverPolicy=DeliverAllPolicy(新订阅从 stream 序头重放),崩溃后重订阅同一 msgTopic 即可恢复。
|
||||||
|
func WaitModelCallResult(ctx context.Context, msgTopic string) (responseParams *ModelCallRes, err error) {
|
||||||
|
resultCh := make(chan *ModelCallRes, 1)
|
||||||
|
errCh := make(chan error, 1)
|
||||||
|
|
||||||
|
_, err = gmq.GetGmq(public.GmqMsgPluginsName).GmqSubscribe(ctx, &mq.NatsSubMessage{
|
||||||
|
SubMessage: types.SubMessage{
|
||||||
|
Topic: msgTopic,
|
||||||
|
ConsumerName: fmt.Sprintf("model-call-result-%s", uuid.NewString()),
|
||||||
|
AutoAck: false,
|
||||||
|
AutoUnsubscribe: true,
|
||||||
|
FetchCount: 1,
|
||||||
|
HandleFunc: func(ctx context.Context, msg any) error {
|
||||||
|
r := new(ModelCallRes)
|
||||||
|
if err := gconv.Struct(msg, r); err != nil {
|
||||||
|
errCh <- err
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if g.IsEmpty(r.TaskId) || !g.IsEmpty(r.ErrorMsg) {
|
||||||
|
errCh <- fmt.Errorf("创建模型任务失败:%v", r.ErrorMsg)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
resultCh <- r
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Durable: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case responseParams = <-resultCh:
|
||||||
|
case err = <-errCh:
|
||||||
|
return nil, err
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModelCallResult 调模型网关生成一段内容并等待结果(内部复用 SubmitModelCall + WaitModelCallResult)。
|
||||||
|
// 同步模型提交即返回;异步模型为"提交+等待"一体。
|
||||||
|
func ModelCallResult(ctx context.Context, modelId int64, responseType model.ResponseType, sessionId string, requestParams map[string]any, businessParams map[string]any) (responseParams *ModelCallRes, err error) {
|
||||||
|
res, msgTopic, err := SubmitModelCall(ctx, modelId, responseType, sessionId, requestParams, businessParams)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if responseType != nil && *responseType == *model.ResponseTypeAsync.Code() {
|
||||||
|
return WaitModelCallResult(ctx, msgTopic)
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
package gateway
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
commonHttp "gitea.redpowerfuture.com/red-future/common/http"
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/utils"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
"github.com/gogf/gf/v2/util/gconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ModelCallStreamReq 调用模型网关流式(client 侧请求体)
|
||||||
|
type ModelCallStreamReq struct {
|
||||||
|
ModelId int64 `json:"modelId" dc:"模型ID"`
|
||||||
|
BizName string `json:"bizName" dc:"业务名称"`
|
||||||
|
SessionId string `json:"sessionId" dc:"会话ID"`
|
||||||
|
RequestParams map[string]any `json:"requestParams" dc:"请求参数(模板字段)"`
|
||||||
|
BusinessParams map[string]any `json:"businessParams" dc:"业务参数"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModelCallStream 流式调用模型网关:POST /modelCallStream,逐 chunk 调 onChunk(文本增量转发),
|
||||||
|
// 流结束返回 ModelCallRes:Content 为累加全量文本({"respBody": ...}),Tools 为流末工具列表。
|
||||||
|
func ModelCallStream(ctx context.Context, modelId int64, sessionId string, requestParams, businessParams map[string]any, onChunk func(chunk map[string]any) error) (*ModelCallRes, error) {
|
||||||
|
req := ModelCallStreamReq{
|
||||||
|
ModelId: modelId,
|
||||||
|
BizName: g.Cfg().MustGet(ctx, "server.name").String(),
|
||||||
|
SessionId: sessionId,
|
||||||
|
RequestParams: requestParams,
|
||||||
|
BusinessParams: businessParams,
|
||||||
|
}
|
||||||
|
body, err := commonHttp.PostStream(ctx, "model-gateway/model/call/modelCallStream", utils.HeadersFromCtx(ctx, utils.HeadersOptions{TokenFromQuery: true}), &req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer body.Close()
|
||||||
|
|
||||||
|
res := new(ModelCallRes)
|
||||||
|
var contentBuf strings.Builder
|
||||||
|
scanErr := parseSSEStream(ctx, body, func(chunk map[string]any) error {
|
||||||
|
tools, done := processStreamChunk(chunk)
|
||||||
|
if done {
|
||||||
|
res.Tools = tools
|
||||||
|
// 流末 done 事件携带该次调用累计 token 与费用:纯工具调用步骤无文本 chunk,靠它取数(>0 才覆盖,兼容旧网关无 token/cost 的 done 事件)
|
||||||
|
if total := gconv.Int64(chunk["totalTokens"]); total > 0 {
|
||||||
|
res.TotalTokens = total
|
||||||
|
res.PromptTokens = gconv.Int64(chunk["promptTokens"])
|
||||||
|
res.CompletionTokens = gconv.Int64(chunk["completionTokens"])
|
||||||
|
}
|
||||||
|
if cost := gconv.Float64(chunk["cost"]); cost > 0 {
|
||||||
|
res.Cost = cost
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if onChunk != nil {
|
||||||
|
if err := onChunk(chunk); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
contentBuf.WriteString(streamTextDelta(chunk))
|
||||||
|
// 网关端每个 chunk 携带累计 token 值,取最后一个文本 chunk 即为该步总消耗
|
||||||
|
res.TotalTokens = gconv.Int64(chunk["totalTokens"])
|
||||||
|
res.PromptTokens = gconv.Int64(chunk["promptTokens"])
|
||||||
|
res.CompletionTokens = gconv.Int64(chunk["completionTokens"])
|
||||||
|
res.Cost = gconv.Float64(chunk["cost"])
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if scanErr != nil {
|
||||||
|
// 流被中断(如 ctx 取消)时仍返回已累计的 res,调用方可读取已产生的 token 正常落库
|
||||||
|
return res, scanErr
|
||||||
|
}
|
||||||
|
if contentBuf.Len() > 0 {
|
||||||
|
res.Content = map[string]any{"respBody": contentBuf.String()}
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// processStreamChunk 处理单个流式 chunk。done 事件({"type":"done","tools":[...]})返回解析出的
|
||||||
|
// tools(可为空)且 done=true;其余为文本增量 chunk,返回 done=false。
|
||||||
|
func processStreamChunk(chunk map[string]any) (tools []ModelTool, done bool) {
|
||||||
|
if typ, _ := chunk["type"].(string); typ == "done" {
|
||||||
|
raw, _ := chunk["tools"].([]any)
|
||||||
|
for _, t := range raw {
|
||||||
|
m := gconv.Map(t)
|
||||||
|
if m == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var tool ModelTool
|
||||||
|
tool.Id = gconv.String(m["id"])
|
||||||
|
tool.Type = gconv.String(m["type"])
|
||||||
|
if fn := gconv.Map(m["function"]); fn != nil {
|
||||||
|
tool.Function.Name = gconv.String(fn["name"])
|
||||||
|
tool.Function.Arguments = gconv.String(fn["arguments"])
|
||||||
|
}
|
||||||
|
tools = append(tools, tool)
|
||||||
|
}
|
||||||
|
return tools, true
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// streamTextDelta 提取文本增量 chunk 的文本(读 content 子对象,字段名由网关端结构体统一管理)
|
||||||
|
func streamTextDelta(chunk map[string]any) string {
|
||||||
|
var sb strings.Builder
|
||||||
|
if content, ok := chunk["content"].(map[string]any); ok {
|
||||||
|
for _, v := range content {
|
||||||
|
if s := gconv.String(v); s != "" {
|
||||||
|
sb.WriteString(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseSSEStream 标准 SSE 解析:逐 data 行 JSON 回调,支持 [DONE] 与上下文取消。
|
||||||
|
// 对齐 model-gateway ParseSSEStream 语义:跳过空行、[DONE]、非 data 行。
|
||||||
|
func parseSSEStream(ctx context.Context, reader io.Reader, onChunk func(chunk map[string]any) error) error {
|
||||||
|
scanner := bufio.NewScanner(reader)
|
||||||
|
scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
|
||||||
|
var sb strings.Builder
|
||||||
|
for scanner.Scan() {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
line := scanner.Text()
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if trimmed == "" {
|
||||||
|
if sb.Len() == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
data := sb.String()
|
||||||
|
sb.Reset()
|
||||||
|
if data == "[DONE]" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var chunk map[string]any
|
||||||
|
if err := json.Unmarshal([]byte(data), &chunk); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if onChunk != nil {
|
||||||
|
if err := onChunk(chunk); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(line, "data:") {
|
||||||
|
sb.WriteString(strings.TrimSpace(strings.TrimPrefix(line, "data:")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return scanner.Err()
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
module ai-agent
|
||||||
|
|
||||||
|
go 1.26.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
gitea.redpowerfuture.com/red-future/common v0.0.33
|
||||||
|
github.com/bjang03/gmq v0.0.3
|
||||||
|
github.com/cloudwego/eino v0.9.5
|
||||||
|
github.com/cloudwego/eino-examples v0.0.0-20260611092511-bd64846fbc1d
|
||||||
|
github.com/cloudwego/eino-ext/components/model/qwen v0.1.9
|
||||||
|
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/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674
|
||||||
|
github.com/tidwall/gjson v1.18.0
|
||||||
|
github.com/tiger1103/gfast-token v1.0.10
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/BurntSushi/toml v1.5.0 // indirect
|
||||||
|
github.com/armon/go-metrics v0.4.1 // indirect
|
||||||
|
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||||
|
github.com/buger/jsonparser v1.1.1 // indirect
|
||||||
|
github.com/bwmarrin/snowflake v0.3.0 // indirect
|
||||||
|
github.com/bytedance/gopkg v0.1.4 // indirect
|
||||||
|
github.com/bytedance/sonic v1.15.0 // indirect
|
||||||
|
github.com/bytedance/sonic/loader v0.5.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/cloudwego/base64x v0.1.6 // indirect
|
||||||
|
github.com/cloudwego/eino-ext/libs/acl/openai v0.1.17 // 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/eino-contrib/jsonschema v1.0.3 // indirect
|
||||||
|
github.com/emirpasic/gods/v2 v2.0.0-alpha // indirect
|
||||||
|
github.com/evanphx/json-patch v0.5.2 // indirect
|
||||||
|
github.com/fatih/color v1.18.0 // indirect
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.13 // 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-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||||
|
github.com/gogf/gf/contrib/registry/consul/v2 v2.9.5 // indirect
|
||||||
|
github.com/gogf/gf/contrib/trace/otlphttp/v2 v2.9.5 // indirect
|
||||||
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
||||||
|
github.com/golang/glog v1.2.5 // indirect
|
||||||
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
|
github.com/golang/protobuf v1.5.4 // indirect
|
||||||
|
github.com/golang/snappy v1.0.0 // indirect
|
||||||
|
github.com/google/flatbuffers v1.12.1 // indirect
|
||||||
|
github.com/goph/emperror v0.17.2 // indirect
|
||||||
|
github.com/grokify/html-strip-tags-go v0.1.0 // indirect
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
|
||||||
|
github.com/hashicorp/consul/api v1.26.1 // indirect
|
||||||
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||||
|
github.com/hashicorp/go-hclog v1.5.0 // indirect
|
||||||
|
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
|
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
|
||||||
|
github.com/hashicorp/golang-lru v1.0.2 // indirect
|
||||||
|
github.com/hashicorp/serf v0.10.1 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/klauspost/compress v1.18.4 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/lib/pq v1.10.9 // indirect
|
||||||
|
github.com/magiconair/properties v1.8.10 // indirect
|
||||||
|
github.com/mailru/easyjson v0.9.0 // 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/meguminnnnnnnnn/go-openai v0.1.2 // indirect
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
||||||
|
github.com/nats-io/nats.go v1.49.0 // indirect
|
||||||
|
github.com/nats-io/nkeys v0.4.15 // indirect
|
||||||
|
github.com/nats-io/nuid v1.0.1 // indirect
|
||||||
|
github.com/nikolalohinski/gonja v1.5.3 // 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/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
|
github.com/pkg/errors v0.9.2-0.20201214064552-5dd12d0cfe7f // indirect
|
||||||
|
github.com/r3labs/diff/v2 v2.15.1 // indirect
|
||||||
|
github.com/rabbitmq/amqp091-go v1.10.0 // indirect
|
||||||
|
github.com/redis/go-redis/v9 v9.18.0 // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||||
|
github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f // indirect
|
||||||
|
github.com/spf13/cast v1.10.0 // indirect
|
||||||
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
|
github.com/tidwall/pretty v1.2.1 // indirect
|
||||||
|
github.com/tidwall/sjson v1.2.5 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/vcaesar/cedar v0.30.0 // indirect
|
||||||
|
github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
|
||||||
|
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||||
|
github.com/yargevad/filepathx v1.0.0 // indirect
|
||||||
|
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
||||||
|
go.opencensus.io v0.23.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.44.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/trace v1.44.0 // indirect
|
||||||
|
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
|
||||||
|
go.uber.org/atomic v1.11.0 // indirect
|
||||||
|
golang.org/x/arch v0.19.0 // indirect
|
||||||
|
golang.org/x/crypto v0.48.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 // indirect
|
||||||
|
golang.org/x/net v0.49.0 // indirect
|
||||||
|
golang.org/x/sys v0.41.0 // indirect
|
||||||
|
golang.org/x/text v0.34.0 // indirect
|
||||||
|
google.golang.org/appengine v1.6.7 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect
|
||||||
|
google.golang.org/grpc v1.75.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.8 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
@@ -0,0 +1,606 @@
|
|||||||
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
|
gitea.redpowerfuture.com/red-future/common v0.0.33 h1:AhWJ6l9zrjc1U0UEfyIZu8wkkVFxNe0hfuA51vOnOIo=
|
||||||
|
gitea.redpowerfuture.com/red-future/common v0.0.33/go.mod h1:FtI9KJJSKo4/K0emjVkbL8yoSIPHJdZXr27vnScQpmM=
|
||||||
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||||
|
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||||
|
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
|
||||||
|
github.com/airbrake/gobrake v3.6.1+incompatible/go.mod h1:wM4gu3Cn0W0K7GUuVWnlXZU11AGBXMILnrdOU8Kn00o=
|
||||||
|
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
|
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
|
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
|
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
|
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||||
|
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||||
|
github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA=
|
||||||
|
github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=
|
||||||
|
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||||
|
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||||
|
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||||
|
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||||
|
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||||
|
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||||
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
|
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||||
|
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
|
||||||
|
github.com/bjang03/gmq v0.0.3 h1:Yn9GZP1okOc8uh0f/1FFTooV5/mbO4pKrkcK9mTMjok=
|
||||||
|
github.com/bjang03/gmq v0.0.3/go.mod h1:Y7TwWGuV4Cw97WUDaM7x+NC4kyFx1z44WAvNwJV3HV8=
|
||||||
|
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
|
||||||
|
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||||
|
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||||
|
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||||
|
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||||
|
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||||
|
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||||
|
github.com/bugsnag/bugsnag-go v1.4.0/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
|
||||||
|
github.com/bugsnag/panicwrap v1.2.0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
|
||||||
|
github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0=
|
||||||
|
github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE=
|
||||||
|
github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM=
|
||||||
|
github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4=
|
||||||
|
github.com/bytedance/mockey v1.3.0 h1:ONLRdvhqmCfr9rTasUB8ZKCfvbdD2tohOg4u+4Q/ed0=
|
||||||
|
github.com/bytedance/mockey v1.3.0/go.mod h1:1BPHF9sol5R1ud/+0VEHGQq/+i2lN+GTsr3O2Q9IENY=
|
||||||
|
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||||
|
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||||
|
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||||
|
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||||
|
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||||
|
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||||
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
|
github.com/certifi/gocertifi v0.0.0-20190105021004-abcd57078448/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4=
|
||||||
|
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
|
||||||
|
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
|
||||||
|
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
|
||||||
|
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
|
||||||
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
|
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||||
|
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||||
|
github.com/cloudwego/eino v0.9.5 h1:0Nftjx9gPek/2S/hzm38LVxSjk5/6mqRr3I9VKrKvm4=
|
||||||
|
github.com/cloudwego/eino v0.9.5/go.mod h1:OBD1mrkfkt/pJa4rkg1P0VnaMeOVl7l8IAdEqY//3IQ=
|
||||||
|
github.com/cloudwego/eino-examples v0.0.0-20260611092511-bd64846fbc1d h1:NrAxhU58S5SgK5YbrYnaOQrQFwAz3x4/0qg46JM8Eo4=
|
||||||
|
github.com/cloudwego/eino-examples v0.0.0-20260611092511-bd64846fbc1d/go.mod h1:VVmcWGhnLIxLkrQAaCoOtQoEbcvOCrMQvbXgbo9O34Q=
|
||||||
|
github.com/cloudwego/eino-ext/components/model/qwen v0.1.9 h1:xCz/mp43JeWqupjPR3zLRArmwC6P29/6lTwbwh1yzYM=
|
||||||
|
github.com/cloudwego/eino-ext/components/model/qwen v0.1.9/go.mod h1:slTGTuhzkzhNavf+1UtUg1FvUSA31iNAF+rq1mT4SnI=
|
||||||
|
github.com/cloudwego/eino-ext/libs/acl/openai v0.1.17 h1:EeVcR1TslRA2IdNW1h/2LaGbPlffwGhQm99jM3zWZiI=
|
||||||
|
github.com/cloudwego/eino-ext/libs/acl/openai v0.1.17/go.mod h1:Zkcx6DPTR2NfWmtSXbhItswGw6hqUezNPhNcke0pOG8=
|
||||||
|
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dgraph-io/badger/v4 v4.2.0 h1:kJrlajbXXL9DFTNuhhu9yCx7JJa4qpYWxtE8BzuWsEs=
|
||||||
|
github.com/dgraph-io/badger/v4 v4.2.0/go.mod h1:qfCqhPoWDFJRx1gp5QwwyGo8xk1lbHUxvK9nK0OGAak=
|
||||||
|
github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8=
|
||||||
|
github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
|
||||||
|
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
|
||||||
|
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
|
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
github.com/eino-contrib/jsonschema v1.0.3 h1:2Kfsm1xlMV0ssY2nuxshS4AwbLFuqmPmzIjLVJ1Fsp0=
|
||||||
|
github.com/eino-contrib/jsonschema v1.0.3/go.mod h1:cpnX4SyKjWjGC7iN2EbhxaTdLqGjCi0e9DxpLYxddD4=
|
||||||
|
github.com/emirpasic/gods/v2 v2.0.0-alpha h1:dwFlh8pBg1VMOXWGipNMRt8v96dKAIvBehtCt6OtunU=
|
||||||
|
github.com/emirpasic/gods/v2 v2.0.0-alpha/go.mod h1:W0y4M2dtBB9U5z3YlghmpuUhiaZT2h6yoeE+C1sCp6A=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||||
|
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||||
|
github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k=
|
||||||
|
github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
|
||||||
|
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/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
|
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/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
|
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
|
||||||
|
github.com/go-check/check v0.0.0-20180628173108-788fd7840127 h1:0gkP6mzaMqkmpcJYCFOLkIBwI7xFExG03bbkOkCvUPI=
|
||||||
|
github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
|
||||||
|
github.com/go-ego/gse v1.0.2 h1:+27lYFPhQEhA9igtdOsJPRKYL/k3TwYsxBF5jr6KFv4=
|
||||||
|
github.com/go-ego/gse v1.0.2/go.mod h1:Fy35G+q7VV7Et1zIKO8o/sW1kkugV3znXap/lF/11zc=
|
||||||
|
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
|
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
|
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||||
|
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||||
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
|
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||||
|
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||||
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
|
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||||
|
github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.2 h1:u8EpP24GkprogROnJ7htMov9Fc66pTP1eVYrWxiCYOs=
|
||||||
|
github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.2/go.mod h1:GmvM3r8GVByVMi4RD2+MCs5+CfxVXPMeT8mVDkAaAXE=
|
||||||
|
github.com/gogf/gf/contrib/nosql/redis/v2 v2.10.2 h1:iTQegT+lEg/wDKvj2mi3W1wrdrwFarjokf88EXVVgu4=
|
||||||
|
github.com/gogf/gf/contrib/nosql/redis/v2 v2.10.2/go.mod h1:ZRw3GNz5cq4uYrW4TPSVyrYWaoqzujKdWro/AOcGBaE=
|
||||||
|
github.com/gogf/gf/contrib/registry/consul/v2 v2.9.5 h1:eUqwJ/qNH8lJ6yssiqskazgp1ACQuNU6zXlLOZVuXTQ=
|
||||||
|
github.com/gogf/gf/contrib/registry/consul/v2 v2.9.5/go.mod h1:sjQyMry9+0POYZCA6lHXBxO77WoNKkruJpRB4xKqk5k=
|
||||||
|
github.com/gogf/gf/contrib/trace/otlphttp/v2 v2.9.5 h1:tHUEZYB5GTqEYYVDYnlGobf1xISARKDE4KHVlgjwTec=
|
||||||
|
github.com/gogf/gf/contrib/trace/otlphttp/v2 v2.9.5/go.mod h1:cfzTn2HS9RDX8f5pUVkbGxUWcSosouqfNQ1G6cY0V88=
|
||||||
|
github.com/gogf/gf/v2 v2.10.2 h1:46IO0Uc8e85/FqdftJFskfDejJLBL0JBnGS5qOftUu8=
|
||||||
|
github.com/gogf/gf/v2 v2.10.2/go.mod h1:Svl1N+E8G/QshU2DUbh/3J/AJauqCgUnxHurXWR4Qx0=
|
||||||
|
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
|
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
|
github.com/golang/glog v1.2.5 h1:DrW6hGnjIhtvhOIiAKT6Psh/Kd/ldepEa81DKeiRJ5I=
|
||||||
|
github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
|
||||||
|
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||||
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||||
|
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||||
|
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||||
|
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
|
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
|
||||||
|
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
|
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
|
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
|
||||||
|
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
|
||||||
|
github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw=
|
||||||
|
github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||||
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/goph/emperror v0.17.2 h1:yLapQcmEsO0ipe9p5TaN22djm3OFV/TfM/fcYP0/J18=
|
||||||
|
github.com/goph/emperror v0.17.2/go.mod h1:+ZbQ+fUNO/6FNiUo0ujtMjhgad9Xa6fQL9KhH4LNHic=
|
||||||
|
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
|
||||||
|
github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
|
||||||
|
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
|
||||||
|
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
|
||||||
|
github.com/grokify/html-strip-tags-go v0.1.0 h1:03UrQLjAny8xci+R+qjCce/MYnpNXCtgzltlQbOBae4=
|
||||||
|
github.com/grokify/html-strip-tags-go v0.1.0/go.mod h1:ZdzgfHEzAfz9X6Xe5eBLVblWIxXfYSQ40S/VKrAOGpc=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
|
||||||
|
github.com/hashicorp/consul/api v1.26.1 h1:5oSXOO5fboPZeW5SN+TdGFP/BILDgBm19OrPZ/pICIM=
|
||||||
|
github.com/hashicorp/consul/api v1.26.1/go.mod h1:B4sQTeaSO16NtynqrAdwOlahJ7IUDZM9cj2420xYL8A=
|
||||||
|
github.com/hashicorp/consul/sdk v0.15.0 h1:2qK9nDrr4tiJKRoxPGhm6B7xJjLVIQqkjiab2M4aKjU=
|
||||||
|
github.com/hashicorp/consul/sdk v0.15.0/go.mod h1:r/OmRRPbHOe0yxNahLw7G9x5WG17E1BIECMtCjcPSNo=
|
||||||
|
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
|
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||||
|
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||||
|
github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c=
|
||||||
|
github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||||
|
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||||
|
github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
|
||||||
|
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||||
|
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||||
|
github.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI=
|
||||||
|
github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||||
|
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||||
|
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||||
|
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
|
||||||
|
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
|
||||||
|
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
|
||||||
|
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||||
|
github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc=
|
||||||
|
github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
|
||||||
|
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||||
|
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||||
|
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||||
|
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||||
|
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||||
|
github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI=
|
||||||
|
github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||||
|
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
|
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
|
||||||
|
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||||
|
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||||
|
github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
|
||||||
|
github.com/hashicorp/memberlist v0.5.0 h1:EtYPN8DpAURiapus508I4n9CzHs2W+8NZGbmmR/prTM=
|
||||||
|
github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0=
|
||||||
|
github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY=
|
||||||
|
github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4=
|
||||||
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
|
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||||
|
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||||
|
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||||
|
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
|
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||||
|
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
|
||||||
|
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.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||||
|
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||||
|
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=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
|
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
|
||||||
|
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||||
|
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
||||||
|
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||||
|
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||||
|
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
|
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||||
|
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||||
|
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||||
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
|
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||||
|
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
|
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||||
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
|
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
|
github.com/meguminnnnnnnnn/go-openai v0.1.2 h1:iXombGGjqjBrmE9WaSidUhhi3YQhf42QTHvHLMkgvCA=
|
||||||
|
github.com/meguminnnnnnnnn/go-openai v0.1.2/go.mod h1:qs96ysDmxhE4BZoU45I43zcyfnaYxU3X+aRzLko/htY=
|
||||||
|
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
|
||||||
|
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||||
|
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
|
||||||
|
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
|
||||||
|
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
|
||||||
|
github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs=
|
||||||
|
github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
|
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
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/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
|
||||||
|
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||||
|
github.com/nats-io/nats.go v1.49.0 h1:yh/WvY59gXqYpgl33ZI+XoVPKyut/IcEaqtsiuTJpoE=
|
||||||
|
github.com/nats-io/nats.go v1.49.0/go.mod h1:fDCn3mN5cY8HooHwE2ukiLb4p4G4ImmzvXyJt+tGwdw=
|
||||||
|
github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4=
|
||||||
|
github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs=
|
||||||
|
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||||
|
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||||
|
github.com/nikolalohinski/gonja v1.5.3 h1:GsA+EEaZDZPGJ8JtpeGN78jidhOlxeJROpqMT9fTj9c=
|
||||||
|
github.com/nikolalohinski/gonja v1.5.3/go.mod h1:RmjwxNiXAEqcq1HeK5SSMmqFJvKOfTfXhkJv6YBtPa4=
|
||||||
|
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/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
|
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||||
|
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
|
||||||
|
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.9.2-0.20201214064552-5dd12d0cfe7f h1:lJqhwddJVYAkyp72a4pwzMClI20xTwL7miDdm2W/KBM=
|
||||||
|
github.com/pkg/errors v0.9.2-0.20201214064552-5dd12d0cfe7f/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||||
|
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
|
||||||
|
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||||
|
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||||
|
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||||
|
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
|
||||||
|
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||||
|
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||||
|
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
||||||
|
github.com/r3labs/diff/v2 v2.15.1 h1:EOrVqPUzi+njlumoqJwiS/TgGgmZo83619FNDB9xQUg=
|
||||||
|
github.com/r3labs/diff/v2 v2.15.1/go.mod h1:I8noH9Fc2fjSaMxqF3G2lhDdC0b+JXCfyx85tWFM9kc=
|
||||||
|
github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=
|
||||||
|
github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
|
||||||
|
github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=
|
||||||
|
github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=
|
||||||
|
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.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
|
github.com/rollbar/rollbar-go v1.0.2/go.mod h1:AcFs5f0I+c71bpHlXNNDbOWJiKwjFDtISeXco0L5PKQ=
|
||||||
|
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||||
|
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
|
||||||
|
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||||
|
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
|
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||||
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
|
github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f h1:Z2cODYsUxQPofhpYRMQVwWz4yUVpHF+vPi+eUdruUYI=
|
||||||
|
github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f/go.mod h1:JqzWyvTuI2X4+9wOHmKSQCYxybB/8j6Ko43qVmXDuZg=
|
||||||
|
github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY=
|
||||||
|
github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec=
|
||||||
|
github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY=
|
||||||
|
github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60=
|
||||||
|
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||||
|
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/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.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
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/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
|
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||||
|
github.com/tidwall/gjson v1.18.0/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/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
|
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||||
|
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
|
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||||
|
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||||
|
github.com/tiger1103/gfast-token v1.0.10 h1:fNiBE/Dq5iTHvTGlCx3DmXa2o4hr0NtumFpffZ39k6s=
|
||||||
|
github.com/tiger1103/gfast-token v1.0.10/go.mod h1:a/21mxmj7zFeNvjhZSC0XpEAFHfb1aT2k6DXnufFU1s=
|
||||||
|
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
|
github.com/vcaesar/cedar v0.30.0 h1:9fSDpM7FTjjUdPiBUUa0MWYMRGSEcqgFXvppZcZ4d7Y=
|
||||||
|
github.com/vcaesar/cedar v0.30.0/go.mod h1:lyuGvALuZZDPNXwpzv/9LyxW+8Y6faN7zauFezNsnik=
|
||||||
|
github.com/vcaesar/tt v0.20.1 h1:D/jUeeVCNbq3ad8M7hhtB3J9x5RZ6I1n1eZ0BJp7M+4=
|
||||||
|
github.com/vcaesar/tt v0.20.1/go.mod h1:cH2+AwGAJm19Wa6xvEa+0r+sXDJBT0QgNQey6mwqLeU=
|
||||||
|
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
|
||||||
|
github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
|
||||||
|
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
||||||
|
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
||||||
|
github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg=
|
||||||
|
github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=
|
||||||
|
github.com/yargevad/filepathx v1.0.0 h1:SYcT+N3tYGi+NvazubCNlvgIPbzAk7i7y2dwg3I5FYc=
|
||||||
|
github.com/yargevad/filepathx v1.0.0/go.mod h1:BprfX/gpYNJHJfc35GjRRpVcwWXS89gGulUIU5tK3tA=
|
||||||
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
|
||||||
|
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
|
||||||
|
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
||||||
|
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||||
|
go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M=
|
||||||
|
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
|
||||||
|
go.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.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
|
||||||
|
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
|
||||||
|
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.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=
|
||||||
|
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||||
|
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||||
|
golang.org/x/arch v0.19.0 h1:LmbDQUodHThXE+htjrnmVD73M//D9GTH6wFZjyDkjyU=
|
||||||
|
golang.org/x/arch v0.19.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||||
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
|
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||||
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
|
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 h1:R9PFI6EUdfVKgwKjZef7QIwGcBKu86OEFpJ9nUEP2l4=
|
||||||
|
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc=
|
||||||
|
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.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||||
|
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||||
|
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-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
|
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
|
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
|
||||||
|
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||||
|
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
|
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/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.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||||
|
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||||
|
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||||
|
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.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||||
|
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||||
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
|
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
|
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
|
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||||
|
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
|
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
|
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
|
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||||
|
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
|
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||||
|
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc=
|
||||||
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
|
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||||
|
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||||
|
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||||
|
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||||
|
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
|
||||||
|
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||||
|
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||||
|
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||||
|
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||||
|
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||||
|
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||||
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
digitalhumanController "ai-agent/digital-human/controller"
|
||||||
|
"ai-agent/workflow/service/flow"
|
||||||
|
|
||||||
|
// 空导入:加载内置模型工具与工作流处理器(组合根统一装载,业务侧不感知实现位置)
|
||||||
|
_ "ai-agent/tools/builtin/current_time"
|
||||||
|
"ai-agent/workflow/consts/public"
|
||||||
|
workController "ai-agent/workflow/controller"
|
||||||
|
workflowController "ai-agent/workflow/controller/flow"
|
||||||
|
workflowNodeController "ai-agent/workflow/controller/node"
|
||||||
|
sessionController "ai-agent/workflow/controller/session"
|
||||||
|
workflowSkillController "ai-agent/workflow/controller/skill"
|
||||||
|
toolController "ai-agent/workflow/controller/tool"
|
||||||
|
_ "ai-agent/workflow/service/flow/processor/builtin/media"
|
||||||
|
_ "ai-agent/workflow/service/flow/processor/builtin/split_batch"
|
||||||
|
_ "ai-agent/workflow/service/flow/processor/builtin/split_segment"
|
||||||
|
_ "ai-agent/workflow/service/flow/processor/builtin/split_shots_pipeline"
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/http"
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/jaeger"
|
||||||
|
gmq "github.com/bjang03/gmq/core/gmq"
|
||||||
|
"github.com/bjang03/gmq/mq"
|
||||||
|
_ "github.com/gogf/gf/contrib/drivers/pgsql/v2"
|
||||||
|
_ "github.com/gogf/gf/contrib/nosql/redis/v2"
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ctx := context.Background()
|
||||||
|
defer jaeger.ShutDown(ctx)
|
||||||
|
|
||||||
|
// 注册HTTP路由
|
||||||
|
http.Httpserver.BindHandler("/httpNodeCallback", workflowController.FlowCallBack.HttpNodeCallback)
|
||||||
|
|
||||||
|
http.RouteRegister([]interface{}{
|
||||||
|
//digitalhuman相关接口
|
||||||
|
digitalhumanController.Audio, // 语音相关接口
|
||||||
|
digitalhumanController.CustomVoice, // 自定义语音相关接口
|
||||||
|
digitalhumanController.DigitalHuman, // 数字人相关接口
|
||||||
|
digitalhumanController.Video, // 视频相关接口
|
||||||
|
digitalhumanController.AsyncTask, // 异步任务相关接口
|
||||||
|
workController.CreationInfo,
|
||||||
|
workflowController.FlowExecution,
|
||||||
|
workflowController.FlowUser,
|
||||||
|
workflowController.FlowTemplate,
|
||||||
|
workflowNodeController.NodeLibrary,
|
||||||
|
workflowNodeController.NodePrompt,
|
||||||
|
workflowSkillController.SkillTemplate,
|
||||||
|
workflowSkillController.SkillUser,
|
||||||
|
toolController.Tool,
|
||||||
|
sessionController.Session,
|
||||||
|
})
|
||||||
|
//workflow.ExternalInterruptDemo()
|
||||||
|
//err := activePullService.ActivePullService.AllList(ctx)
|
||||||
|
//if err != nil {
|
||||||
|
// g.Log().Error(ctx, "ActivePullService err: %v", err)
|
||||||
|
//}
|
||||||
|
|
||||||
|
gmq.GmqRegister(public.GmqMsgPluginsName, &mq.NatsConn{
|
||||||
|
NatsConfig: mq.NatsConfig{
|
||||||
|
Addr: g.Config().MustGet(ctx, "nats.addr").String(),
|
||||||
|
Port: g.Config().MustGet(ctx, "nats.port").String(),
|
||||||
|
Username: g.Config().MustGet(ctx, "nats.username").String(),
|
||||||
|
Password: g.Config().MustGet(ctx, "nats.password").String(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 启动工作流恢复扫描(首次立即扫 + 周期扫,多节点靠 Redis 锁去重)
|
||||||
|
flow.StartRecoveryLoop(ctx)
|
||||||
|
|
||||||
|
// 监听退出信号,执行优雅关闭
|
||||||
|
sigCh := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
|
||||||
|
shutdownDone := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
<-sigCh
|
||||||
|
// 先置关停标记再 Close:Close 会取消各连接 ctx,正在执行的 exec 会收到 context.Canceled;
|
||||||
|
// 标记让错误分类识别为"程序关停中断"(retryable=1),下次启动恢复扫描捞起续跑,
|
||||||
|
// 而非误判为"用户已终止执行"(retryable=0,永不恢复)
|
||||||
|
// SetShuttingDown 同时取消所有运行中执行(含脱离连接的恢复执行),保证全部落终态
|
||||||
|
flow.SetShuttingDown()
|
||||||
|
flow.SessionWsService.Close()
|
||||||
|
// 等待运行中执行落完终态再退出,避免进程退出时 exec 仍卡 status=1;
|
||||||
|
// 结束后返回 main 触发 deferred 资源清理,进程自然退出(不再 select{} 挂死)
|
||||||
|
flow.WaitExecRunsDrain(15 * time.Second)
|
||||||
|
close(shutdownDone)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 保持应用运行;收到关停信号并落完终态后自然退出
|
||||||
|
<-shutdownDone
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// Package current_time 内置示例工具:返回服务器当前时间。
|
||||||
|
// 仅用于验证独立工具对话入口(/tool/agent 将全部注册工具交给模型 function calling),可按需移除。
|
||||||
|
package current_time
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/tools"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
tools.Register(CurrentTimeTool())
|
||||||
|
}
|
||||||
|
|
||||||
|
// CurrentTimeTool 返回服务器当前时间,作为 model 作用域示例工具
|
||||||
|
func CurrentTimeTool() *tools.Tool {
|
||||||
|
return &tools.Tool{
|
||||||
|
Name: "current_time",
|
||||||
|
Description: "返回服务器当前时间。当用户询问时间/日期时调用。",
|
||||||
|
Parameters: map[string]any{
|
||||||
|
"type": "object",
|
||||||
|
"properties": map[string]any{},
|
||||||
|
"required": false,
|
||||||
|
},
|
||||||
|
Func: func(ctx context.Context, args map[string]any) (tools.ToolResult, error) {
|
||||||
|
return tools.OK(map[string]any{
|
||||||
|
"now": time.Now().Format("2006-01-02 15:04:05"),
|
||||||
|
}), nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
// Package runner 工具框架的"使用面":ReAct 执行循环。
|
||||||
|
// 模型调用按模型响应类型分流:流式走 gateway.ModelCallStream(逐 chunk 推增量),同步/异步走 gateway.ModelCallResult;
|
||||||
|
// 本包只保留"模型思考→调用工具→观察结果"的循环编排,工具的"使用"统一走 tools.Default.Call。
|
||||||
|
package runner
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ai-agent/gateway"
|
||||||
|
"ai-agent/workflow/consts/model"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/util/gconv"
|
||||||
|
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/tools"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReActEventType ReAct 过程事件类型(WebSocket 场景逐条推送,HTTP 同步场景不设置 OnEvent 则忽略)
|
||||||
|
type ReActEventType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ReActEventRoundStart ReActEventType = "round_start"
|
||||||
|
ReActEventModelCall ReActEventType = "model_call" // 模型思考(step 开始)
|
||||||
|
ReActEventToolCall ReActEventType = "tool_call" // 模型请求调用工具
|
||||||
|
ReActEventToolResult ReActEventType = "tool_result" // 工具返回结果
|
||||||
|
ReActEventAnswer ReActEventType = "answer" // 最终回答
|
||||||
|
ReActEventAnswerChunk ReActEventType = "answer_chunk" // 回答内容增量(逐 chunk)
|
||||||
|
ReActEventReasoningChunk ReActEventType = "reasoning_chunk" // 思考内容增量(逐 chunk)
|
||||||
|
ReActEventError ReActEventType = "error"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReActEvent ReAct 过程事件,按发生顺序回调
|
||||||
|
type ReActEvent struct {
|
||||||
|
Type ReActEventType
|
||||||
|
Id int64
|
||||||
|
Step int
|
||||||
|
MaxStep int
|
||||||
|
Description string // 工具用途说明(tool_call/tool_result 推给前端展示,不暴露工具名/参数)
|
||||||
|
Answer string
|
||||||
|
Delta string // 流式文本增量(ReActEventAnswerChunk / ReActEventReasoningChunk 用)
|
||||||
|
Message string
|
||||||
|
Error string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReActAgent 实现 ReAct 模式的智能体:模型思考→调用工具→观察工具结果→重复→最终回答。
|
||||||
|
// 工具的"使用"统一走 tools.Default.Call:未知工具由 Server 返回结构化 not_found,
|
||||||
|
// 无需在 runner 内维护工具查找表。
|
||||||
|
type ReActAgent struct {
|
||||||
|
ModelId int64
|
||||||
|
SessionId string
|
||||||
|
ToolList []*tools.Tool
|
||||||
|
SystemPrompt string
|
||||||
|
MaxStep int
|
||||||
|
// OnEvent 过程回调(WebSocket 流式推送用;HTTP 同步调用不设置,忽略事件)
|
||||||
|
OnEvent func(ReActEvent)
|
||||||
|
// TotalTokens 本次 ReAct 循环累计 token 消耗(供调用方落库)
|
||||||
|
TotalTokens int64
|
||||||
|
// TotalCost 本次 ReAct 循环累计费用(元,各步模型调用返回的 cost 求和,供调用方落库)
|
||||||
|
TotalCost float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewReActAgent 创建 ReAct 智能体
|
||||||
|
func NewReActAgent(modelId int64, sessionId string, toolList []*tools.Tool, systemPrompt string, maxStep int) *ReActAgent {
|
||||||
|
return &ReActAgent{
|
||||||
|
ModelId: modelId,
|
||||||
|
SessionId: sessionId,
|
||||||
|
ToolList: toolList,
|
||||||
|
SystemPrompt: systemPrompt,
|
||||||
|
MaxStep: maxStep,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run 执行 ReAct 循环
|
||||||
|
// 标准流程: 思考 → 行动(调用工具) → 观察(工具结果) → 重复 → 最终回答
|
||||||
|
func (a *ReActAgent) Run(ctx context.Context, userInput string) (string, error) {
|
||||||
|
// 模型须为已启用且勾选「聊天模型」的对话模型,再按其响应类型分流调用:
|
||||||
|
// 流式走 ModelCallStream(逐 chunk 推增量),同步/异步走 ModelCallResult(一次返回)
|
||||||
|
modelInfo, err := gateway.GetModelInfoById(ctx, &gateway.GetModelInfoByIdReq{ModelId: a.ModelId})
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("获取模型配置失败: %w", err)
|
||||||
|
}
|
||||||
|
if modelInfo.ModelManage.Enabled == nil || !*modelInfo.ModelManage.Enabled {
|
||||||
|
return "", fmt.Errorf("模型 [%s] 未启用", modelInfo.ModelManage.ModelName)
|
||||||
|
}
|
||||||
|
if modelInfo.ModelManage.ChatModel == nil || !*modelInfo.ModelManage.ChatModel {
|
||||||
|
return "", fmt.Errorf("模型 [%s] 不是对话模型,请在模型配置中勾选「聊天模型」", modelInfo.ModelManage.ModelName)
|
||||||
|
}
|
||||||
|
isStream := *modelInfo.ModelManage.ResponseType == *model.ResponseTypeStream.Code()
|
||||||
|
|
||||||
|
messages := map[string]any{
|
||||||
|
"user_prompt": userInput,
|
||||||
|
"system_prompt": a.SystemPrompt,
|
||||||
|
"tools": a.rawTools(a.ToolList),
|
||||||
|
}
|
||||||
|
for step := 0; step < a.MaxStep; step++ {
|
||||||
|
a.emit(ReActEvent{Type: ReActEventModelCall, Step: step + 1, MaxStep: a.MaxStep})
|
||||||
|
var result *gateway.ModelCallRes
|
||||||
|
if isStream {
|
||||||
|
result, err = gateway.ModelCallStream(ctx, a.ModelId, a.SessionId, nil, messages, func(chunk map[string]any) error {
|
||||||
|
// 文本增量从 content 子对象取,字段名由网关端结构体统一管理
|
||||||
|
if content, ok := chunk["content"].(map[string]any); ok {
|
||||||
|
for _, v := range content {
|
||||||
|
if s := gconv.String(v); s != "" {
|
||||||
|
a.emit(ReActEvent{Type: ReActEventAnswerChunk, Delta: s})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 思考内容增量从 reasoningContent 字段取(model-gateway 按 ResponseBusinessFieldMapping 配置返回)
|
||||||
|
if s := gconv.String(chunk["reasoningContent"]); s != "" {
|
||||||
|
a.emit(ReActEvent{Type: ReActEventReasoningChunk, Delta: s})
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// 同步/异步对话模型:结果一次返回,无增量事件(异步 msgTopic 为空时由 gateway.ModelCallResult 自动生成)
|
||||||
|
result, err = gateway.ModelCallResult(ctx, a.ModelId, modelInfo.ModelManage.ResponseType, a.SessionId, nil, messages)
|
||||||
|
}
|
||||||
|
// 出错时 result 仍可能携带已累计的 token/cost(流中断返回 res),先累加再判错
|
||||||
|
if result != nil {
|
||||||
|
a.TotalTokens += result.TotalTokens
|
||||||
|
a.TotalCost += result.Cost
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
// 前端终止/上下文取消:静默停止,不推错误事件
|
||||||
|
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
a.emit(ReActEvent{Type: ReActEventError, Message: "模型调用失败", Error: err.Error()})
|
||||||
|
return "", fmt.Errorf("step %d: model call failed: %w", step, err)
|
||||||
|
}
|
||||||
|
var content string
|
||||||
|
var toolCalls []gateway.ModelTool
|
||||||
|
if isStream {
|
||||||
|
for _, v := range gconv.Map(result.Content) {
|
||||||
|
content += v.(string)
|
||||||
|
}
|
||||||
|
toolCalls = result.Tools
|
||||||
|
} else {
|
||||||
|
// 同步响应 Content 按 responseBodyMapping 抽取(content/toolCalls/finishReason),工具在 toolCalls 里
|
||||||
|
content, toolCalls = syncChatResult(result.Content)
|
||||||
|
}
|
||||||
|
// 无工具调用 → 最终回答
|
||||||
|
if len(toolCalls) == 0 {
|
||||||
|
return content, nil
|
||||||
|
}
|
||||||
|
messages["assistant_prompt"] = content
|
||||||
|
for _, tool := range toolCalls {
|
||||||
|
fn := gconv.Map(tool.Function)
|
||||||
|
name := gconv.String(fn["name"])
|
||||||
|
arguments := gconv.String(fn["arguments"])
|
||||||
|
desc := a.toolDescription(name)
|
||||||
|
a.emit(ReActEvent{Type: ReActEventToolCall, Description: desc})
|
||||||
|
// 执行每个工具调用
|
||||||
|
toolMsg := a.executeToolCall(ctx, name, arguments)
|
||||||
|
a.emit(ReActEvent{Type: ReActEventToolResult, Description: desc})
|
||||||
|
messages["tool_prompt"] = toolMsg
|
||||||
|
messages["tool_id"] = tool.Id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = fmt.Errorf("max steps reached %d, generation incomplete", a.MaxStep)
|
||||||
|
a.emit(ReActEvent{Type: ReActEventError, Message: "最大步数达到,生成失败", Error: err.Error()})
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// syncChatResult 解析同步对话响应 Content(model-gateway 按 responseBodyMapping 抽取,固定
|
||||||
|
// content/toolCalls/finishReason 三个 key):返回最终文本与工具调用列表(同步响应工具在 toolCalls 里,res.Tools 为空)。
|
||||||
|
func syncChatResult(content map[string]any) (string, []gateway.ModelTool) {
|
||||||
|
text := gconv.String(content["content"])
|
||||||
|
raw := content["toolCalls"]
|
||||||
|
if raw == nil {
|
||||||
|
raw = content["tools"]
|
||||||
|
}
|
||||||
|
var toolCalls []gateway.ModelTool
|
||||||
|
if arr, ok := raw.([]any); ok {
|
||||||
|
for _, t := range arr {
|
||||||
|
m := gconv.Map(t)
|
||||||
|
if m == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var tool gateway.ModelTool
|
||||||
|
tool.Id = gconv.String(m["id"])
|
||||||
|
tool.Type = gconv.String(m["type"])
|
||||||
|
if fn := gconv.Map(m["function"]); fn != nil {
|
||||||
|
tool.Function.Name = gconv.String(fn["name"])
|
||||||
|
tool.Function.Arguments = gconv.String(fn["arguments"])
|
||||||
|
}
|
||||||
|
toolCalls = append(toolCalls, tool)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return text, toolCalls
|
||||||
|
}
|
||||||
|
|
||||||
|
// emit 触发过程事件回调;未设置 OnEvent 时忽略(HTTP 同步调用路径)
|
||||||
|
func (a *ReActAgent) emit(ev ReActEvent) {
|
||||||
|
if a.OnEvent != nil {
|
||||||
|
a.OnEvent(ev)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// executeToolCall 执行单个工具调用,返回回灌给模型的工具结果消息。
|
||||||
|
// 未知工具由 tools.Default.Call 返回结构化 not_found;工具业务失败进 ToolResult.Code。
|
||||||
|
func (a *ReActAgent) executeToolCall(ctx context.Context, name, arguments string) string {
|
||||||
|
if arguments == "" {
|
||||||
|
return "工具参数为空"
|
||||||
|
}
|
||||||
|
|
||||||
|
var args map[string]any
|
||||||
|
if err := json.Unmarshal([]byte(arguments), &args); err != nil {
|
||||||
|
return fmt.Sprintf("参数解析失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
res, callErr := tools.Default.Call(ctx, name, args)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case callErr != nil:
|
||||||
|
msg := fmt.Sprintf("工具执行失败: %v", callErr)
|
||||||
|
return msg
|
||||||
|
case res.Code != 0:
|
||||||
|
return res.Message
|
||||||
|
default:
|
||||||
|
dataJSON, err := json.Marshal(res.Data)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf("工具结果序列化失败: %v", err)
|
||||||
|
}
|
||||||
|
return string(dataJSON)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// toolDescription 按工具名从 ToolList 中查找用途说明(推给前端展示用,不暴露工具名/参数)
|
||||||
|
func (a *ReActAgent) toolDescription(name string) string {
|
||||||
|
for _, t := range a.ToolList {
|
||||||
|
if t.Name == name {
|
||||||
|
return t.Description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// RawTools 将工具定义转为 OpenAI 原始数组,parameters 保持 JSON Schema 原样。
|
||||||
|
// businessParams 里传 tools 必须用它转换——直接传 []*tools.Tool 会被 json.Marshal 序列化 Func 字段而失败。
|
||||||
|
func (a *ReActAgent) rawTools(toolList []*tools.Tool) []any {
|
||||||
|
raw := make([]any, 0, len(toolList))
|
||||||
|
for _, t := range toolList {
|
||||||
|
params := t.Parameters
|
||||||
|
if params == nil {
|
||||||
|
params = map[string]any{}
|
||||||
|
}
|
||||||
|
raw = append(raw, map[string]any{
|
||||||
|
"type": "function",
|
||||||
|
"function": map[string]any{
|
||||||
|
"name": t.Name,
|
||||||
|
"description": t.Description,
|
||||||
|
"parameters": params,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return raw
|
||||||
|
}
|
||||||
+981
@@ -0,0 +1,981 @@
|
|||||||
|
-- -----------------------张斌2025-06-16 15:00:00-----------------------
|
||||||
|
|
||||||
|
--------------------pgsql创建digital_human_audio表语句---------------------------
|
||||||
|
-- 音频表
|
||||||
|
CREATE TABLE IF NOT EXISTS digital_human_audio (
|
||||||
|
-- 基础字段(继承 SQLBaseCol 通用字段,与 SQLBaseDO 对齐)
|
||||||
|
id BIGINT PRIMARY KEY, -- 主键ID(非自增)
|
||||||
|
tenant_id BIGINT NOT NULL DEFAULT 0, -- 租户ID int8类型
|
||||||
|
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),
|
||||||
|
|
||||||
|
-- 音频核心字段
|
||||||
|
name VARCHAR(128) NOT NULL, -- 音频名称
|
||||||
|
description TEXT DEFAULT '', -- 音频描述
|
||||||
|
script_text TEXT NOT NULL, -- 话术文本
|
||||||
|
audio_url VARCHAR(512) DEFAULT '', -- 音频文件URL
|
||||||
|
status SMALLINT NOT NULL DEFAULT 0, -- 状态:0生成中/1成功/2失败
|
||||||
|
error_msg TEXT DEFAULT '', -- 错误信息
|
||||||
|
duration INT DEFAULT 0, -- 音频时长(秒)
|
||||||
|
external_id VARCHAR(64) DEFAULT '', -- 外部音频ID
|
||||||
|
voice VARCHAR(32) DEFAULT 'serena', -- 音色:serena/vivian/uncle_fu/ryan/aiden/ono_anna/sohee/eric/dylan
|
||||||
|
voice_type VARCHAR(16) DEFAULT 'preset', -- 音色类型:preset/custom(预设/克隆)
|
||||||
|
custom_voice VARCHAR(64) DEFAULT '' -- 自定义音色ID(用于声音克隆)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 索引
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_audio_tenant_id ON digital_human_audio(tenant_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_audio_status ON digital_human_audio(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_audio_voice_type ON digital_human_audio(voice_type);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_audio_deleted_at ON digital_human_audio(deleted_at);
|
||||||
|
|
||||||
|
-- 表和字段注释
|
||||||
|
COMMENT ON TABLE digital_human_audio IS '音频表';
|
||||||
|
COMMENT ON COLUMN digital_human_audio.id IS '主键ID(非自增)';
|
||||||
|
COMMENT ON COLUMN digital_human_audio.tenant_id IS '租户ID';
|
||||||
|
COMMENT ON COLUMN digital_human_audio.creator IS '创建人';
|
||||||
|
COMMENT ON COLUMN digital_human_audio.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN digital_human_audio.updater IS '更新人';
|
||||||
|
COMMENT ON COLUMN digital_human_audio.updated_at IS '更新时间';
|
||||||
|
COMMENT ON COLUMN digital_human_audio.deleted_at IS '删除时间(软删)';
|
||||||
|
COMMENT ON COLUMN digital_human_audio.name IS '音频名称';
|
||||||
|
COMMENT ON COLUMN digital_human_audio.description IS '音频描述';
|
||||||
|
COMMENT ON COLUMN digital_human_audio.script_text IS '话术文本';
|
||||||
|
COMMENT ON COLUMN digital_human_audio.audio_url IS '音频文件URL';
|
||||||
|
COMMENT ON COLUMN digital_human_audio.status IS '状态:0生成中/1成功/2失败';
|
||||||
|
COMMENT ON COLUMN digital_human_audio.error_msg IS '错误信息';
|
||||||
|
COMMENT ON COLUMN digital_human_audio.duration IS '音频时长(秒)';
|
||||||
|
COMMENT ON COLUMN digital_human_audio.external_id IS '外部音频ID';
|
||||||
|
COMMENT ON COLUMN digital_human_audio.voice IS '音色:serena/vivian/uncle_fu/ryan/aiden/ono_anna/sohee/eric/dylan';
|
||||||
|
COMMENT ON COLUMN digital_human_audio.voice_type IS '音色类型:preset/custom(预设/克隆)';
|
||||||
|
COMMENT ON COLUMN digital_human_audio.custom_voice IS '自定义音色ID';
|
||||||
|
|
||||||
|
--------------------pgsql创建digital_human_custom_voice表语句---------------------------
|
||||||
|
-- 自定义音色表
|
||||||
|
CREATE TABLE IF NOT EXISTS digital_human_custom_voice (
|
||||||
|
-- 基础字段(继承 SQLBaseCol 通用字段)
|
||||||
|
id BIGINT PRIMARY KEY, -- 主键ID(非自增)
|
||||||
|
tenant_id BIGINT NOT NULL DEFAULT 0, -- 租户ID int8类型
|
||||||
|
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),
|
||||||
|
|
||||||
|
-- 音色核心字段
|
||||||
|
name VARCHAR(128) NOT NULL, -- 音色名称
|
||||||
|
description TEXT DEFAULT '', -- 音色描述
|
||||||
|
text TEXT DEFAULT '', -- 参考文本
|
||||||
|
reference_audio BYTEA -- 参考音频数据(二进制)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 索引
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_custom_voice_tenant_id ON digital_human_custom_voice(tenant_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_custom_voice_name ON digital_human_custom_voice(name);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_custom_voice_deleted_at ON digital_human_custom_voice(deleted_at);
|
||||||
|
|
||||||
|
-- 表和字段注释
|
||||||
|
COMMENT ON TABLE digital_human_custom_voice IS '自定义音色表';
|
||||||
|
COMMENT ON COLUMN digital_human_custom_voice.id IS '主键ID(非自增)';
|
||||||
|
COMMENT ON COLUMN digital_human_custom_voice.tenant_id IS '租户ID';
|
||||||
|
COMMENT ON COLUMN digital_human_custom_voice.creator IS '创建人';
|
||||||
|
COMMENT ON COLUMN digital_human_custom_voice.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN digital_human_custom_voice.updater IS '更新人';
|
||||||
|
COMMENT ON COLUMN digital_human_custom_voice.updated_at IS '更新时间';
|
||||||
|
COMMENT ON COLUMN digital_human_custom_voice.deleted_at IS '删除时间(软删)';
|
||||||
|
COMMENT ON COLUMN digital_human_custom_voice.name IS '音色名称';
|
||||||
|
COMMENT ON COLUMN digital_human_custom_voice.description IS '音色描述';
|
||||||
|
COMMENT ON COLUMN digital_human_custom_voice.text IS '参考文本';
|
||||||
|
COMMENT ON COLUMN digital_human_custom_voice.reference_audio IS '参考音频数据(二进制)';
|
||||||
|
|
||||||
|
-- 兼容已有库:自定义音色增加状态/结果字段(对接异步模型服务)
|
||||||
|
ALTER TABLE digital_human_custom_voice ADD COLUMN IF NOT EXISTS status SMALLINT NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE digital_human_custom_voice ADD COLUMN IF NOT EXISTS error_msg TEXT DEFAULT '';
|
||||||
|
ALTER TABLE digital_human_custom_voice ADD COLUMN IF NOT EXISTS oss_file VARCHAR(512) DEFAULT '';
|
||||||
|
COMMENT ON COLUMN digital_human_custom_voice.status IS '状态:0生成中/1成功/2失败';
|
||||||
|
COMMENT ON COLUMN digital_human_custom_voice.error_msg IS '错误信息';
|
||||||
|
COMMENT ON COLUMN digital_human_custom_voice.oss_file IS '结果文件URL(如参考音频/特征文件等)';
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_custom_voice_status ON digital_human_custom_voice(status);
|
||||||
|
|
||||||
|
--------------------pgsql创建digital_human_video表语句---------------------------
|
||||||
|
-- 视频表
|
||||||
|
CREATE TABLE IF NOT EXISTS digital_human_video (
|
||||||
|
-- 基础字段
|
||||||
|
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),
|
||||||
|
|
||||||
|
-- 视频核心字段
|
||||||
|
name VARCHAR(128) NOT NULL, -- 视频名称
|
||||||
|
description TEXT DEFAULT '', -- 视频描述
|
||||||
|
audio_id BIGINT, -- 关联音频ID
|
||||||
|
script_text TEXT NOT NULL, -- 话术文本
|
||||||
|
video_url VARCHAR(512) DEFAULT '', -- 视频文件URL
|
||||||
|
status SMALLINT NOT NULL DEFAULT 0, -- 状态:0生成中/1成功/2失败
|
||||||
|
error_msg TEXT DEFAULT '', -- 错误信息
|
||||||
|
duration INT DEFAULT 0, -- 视频时长(秒)
|
||||||
|
thumbnail_url VARCHAR(512) DEFAULT '', -- 缩略图URL
|
||||||
|
external_id VARCHAR(64) DEFAULT '', -- 外部视频ID
|
||||||
|
digital_human_id BIGINT DEFAULT 0, -- 数字人ID(雪花算法ID)
|
||||||
|
digital_human_name VARCHAR(128) DEFAULT '' -- 数字人名称(冗余字段)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 索引
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_video_tenant_id ON digital_human_video(tenant_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_video_audio_id ON digital_human_video(audio_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_video_status ON digital_human_video(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_video_deleted_at ON digital_human_video(deleted_at);
|
||||||
|
|
||||||
|
-- 表和字段注释
|
||||||
|
COMMENT ON TABLE digital_human_video IS '视频表';
|
||||||
|
COMMENT ON COLUMN digital_human_video.id IS '主键ID(非自增)';
|
||||||
|
COMMENT ON COLUMN digital_human_video.tenant_id IS '租户ID';
|
||||||
|
COMMENT ON COLUMN digital_human_video.audio_id IS '关联音频ID';
|
||||||
|
COMMENT ON COLUMN digital_human_video.name IS '视频名称';
|
||||||
|
COMMENT ON COLUMN digital_human_video.description IS '视频描述';
|
||||||
|
COMMENT ON COLUMN digital_human_video.script_text IS '话术文本';
|
||||||
|
COMMENT ON COLUMN digital_human_video.video_url IS '视频文件URL';
|
||||||
|
COMMENT ON COLUMN digital_human_video.status IS '状态:0生成中/1成功/2失败';
|
||||||
|
COMMENT ON COLUMN digital_human_video.error_msg IS '错误信息';
|
||||||
|
COMMENT ON COLUMN digital_human_video.duration IS '视频时长(秒)';
|
||||||
|
COMMENT ON COLUMN digital_human_video.thumbnail_url IS '缩略图URL';
|
||||||
|
COMMENT ON COLUMN digital_human_video.external_id IS '外部视频ID';
|
||||||
|
COMMENT ON COLUMN digital_human_video.digital_human_id IS '数字人ID';
|
||||||
|
COMMENT ON COLUMN digital_human_video.digital_human_name IS '数字人名称(冗余字段)';
|
||||||
|
|
||||||
|
--------------------pgsql创建digital_human表语句---------------------------
|
||||||
|
-- 数字人表
|
||||||
|
CREATE TABLE IF NOT EXISTS digital_human (
|
||||||
|
-- 基础字段
|
||||||
|
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),
|
||||||
|
|
||||||
|
-- 数字人核心字段
|
||||||
|
name VARCHAR(128) NOT NULL, -- 数字人名称
|
||||||
|
description TEXT DEFAULT '', -- 数字人描述
|
||||||
|
avatar_url VARCHAR(512) DEFAULT '', -- 头像URL
|
||||||
|
video_url VARCHAR(512) DEFAULT '', -- 形象视频URL
|
||||||
|
voice VARCHAR(32) DEFAULT 'serena', -- 默认音色
|
||||||
|
status SMALLINT NOT NULL DEFAULT 1 -- 状态:1启用/0停用
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 索引
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_digital_human_tenant_id ON digital_human(tenant_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_digital_human_name ON digital_human(name);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_digital_human_status ON digital_human(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_digital_human_deleted_at ON digital_human(deleted_at);
|
||||||
|
|
||||||
|
-- 表和字段注释
|
||||||
|
COMMENT ON TABLE digital_human IS '数字人表';
|
||||||
|
COMMENT ON COLUMN digital_human.id IS '主键ID(非自增)';
|
||||||
|
COMMENT ON COLUMN digital_human.tenant_id IS '租户ID';
|
||||||
|
COMMENT ON COLUMN digital_human.creator IS '创建人';
|
||||||
|
COMMENT ON COLUMN digital_human.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN digital_human.updater IS '更新人';
|
||||||
|
COMMENT ON COLUMN digital_human.updated_at IS '更新时间';
|
||||||
|
COMMENT ON COLUMN digital_human.deleted_at IS '删除时间(软删)';
|
||||||
|
COMMENT ON COLUMN digital_human.name IS '数字人名称';
|
||||||
|
COMMENT ON COLUMN digital_human.description IS '数字人描述';
|
||||||
|
COMMENT ON COLUMN digital_human.avatar_url IS '头像URL';
|
||||||
|
COMMENT ON COLUMN digital_human.video_url IS '形象视频URL';
|
||||||
|
COMMENT ON COLUMN digital_human.voice IS '默认音色';
|
||||||
|
COMMENT ON COLUMN digital_human.status IS '状态:1启用/0停用';
|
||||||
|
|
||||||
|
--------------------pgsql创建digital_human_async_task_ref表语句---------------------------
|
||||||
|
-- 异步任务绑定表(task_id -> 业务表+业务ID)
|
||||||
|
CREATE TABLE IF NOT EXISTS digital_human_async_task_ref (
|
||||||
|
-- 基础字段
|
||||||
|
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),
|
||||||
|
|
||||||
|
-- 绑定字段
|
||||||
|
task_id VARCHAR(64) NOT NULL, -- 异步任务ID(model-asynch)
|
||||||
|
state SMALLINT NOT NULL DEFAULT 0, -- 任务状态(与 model-asynch 对齐:0/1/2/3/4)
|
||||||
|
table_name VARCHAR(64) NOT NULL, -- 业务表名:digital_human_audio / digital_human_custom_voice
|
||||||
|
biz_id BIGINT NOT NULL, -- 业务表主键ID(audio/custom_voice 的 id)
|
||||||
|
oss_file VARCHAR(512) DEFAULT '', -- 已转移后的业务侧OSS地址(可选)
|
||||||
|
error_msg TEXT DEFAULT '' -- 错误信息(可选)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 索引
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS uk_async_task_ref_tenant_task_id ON digital_human_async_task_ref(tenant_id, task_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_async_task_ref_tenant_state ON digital_human_async_task_ref(tenant_id, state);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_async_task_ref_table_biz ON digital_human_async_task_ref(table_name, biz_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_async_task_ref_deleted_at ON digital_human_async_task_ref(deleted_at);
|
||||||
|
|
||||||
|
-- 注释
|
||||||
|
COMMENT ON TABLE digital_human_async_task_ref IS '异步任务绑定表(task_id -> 业务表+业务ID)';
|
||||||
|
COMMENT ON COLUMN digital_human_async_task_ref.task_id IS '异步任务ID(model-asynch)';
|
||||||
|
COMMENT ON COLUMN digital_human_async_task_ref.state IS '任务状态(与 model-asynch 对齐:0/1/2/3/4)';
|
||||||
|
COMMENT ON COLUMN digital_human_async_task_ref.table_name IS '业务表名';
|
||||||
|
COMMENT ON COLUMN digital_human_async_task_ref.biz_id IS '业务表主键ID';
|
||||||
|
COMMENT ON COLUMN digital_human_async_task_ref.oss_file IS '已转移后的业务侧OSS地址';
|
||||||
|
COMMENT ON COLUMN digital_human_async_task_ref.error_msg IS '错误信息';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- =============================================================
|
||||||
|
-- 低代码流程编排平台 - 数据库表结构
|
||||||
|
-- Author: AI Assistant
|
||||||
|
-- =============================================================
|
||||||
|
|
||||||
|
-- 素材/创作信息表
|
||||||
|
CREATE TABLE IF NOT EXISTS black_deacon_creation_info (
|
||||||
|
-- 基础字段(完全对齐项目规范)
|
||||||
|
id BIGINT PRIMARY KEY, -- 主键ID(非自增)
|
||||||
|
tenant_id BIGINT NOT NULL DEFAULT 0, -- 租户ID int8
|
||||||
|
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),
|
||||||
|
|
||||||
|
-- 业务字段
|
||||||
|
html_file_url VARCHAR(512) DEFAULT '', -- HTML文件地址
|
||||||
|
image_urls TEXT[] DEFAULT '{}', -- 图片地址列表
|
||||||
|
content_type VARCHAR(255) DEFAULT '', -- 素材类型
|
||||||
|
theme VARCHAR(255) DEFAULT '', -- 主题
|
||||||
|
title VARCHAR(255) NOT NULL -- 标题
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 索引(高频查询)
|
||||||
|
CREATE INDEX idx_creation_tenant_id ON black_deacon_creation_info(tenant_id);
|
||||||
|
CREATE INDEX idx_creation_content_type ON black_deacon_creation_info(content_type);
|
||||||
|
CREATE INDEX idx_creation_theme ON black_deacon_creation_info(theme);
|
||||||
|
CREATE INDEX idx_creation_title ON black_deacon_creation_info(title);
|
||||||
|
CREATE INDEX idx_creation_deleted_at ON black_deacon_creation_info(deleted_at);
|
||||||
|
|
||||||
|
-- 表和字段注释
|
||||||
|
COMMENT ON TABLE black_deacon_creation_info IS '素材/创作信息表';
|
||||||
|
COMMENT ON COLUMN black_deacon_creation_info.id IS '主键ID(非自增)';
|
||||||
|
COMMENT ON COLUMN black_deacon_creation_info.tenant_id IS '租户ID';
|
||||||
|
COMMENT ON COLUMN black_deacon_creation_info.creator IS '创建人';
|
||||||
|
COMMENT ON COLUMN black_deacon_creation_info.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN black_deacon_creation_info.updater IS '更新人';
|
||||||
|
COMMENT ON COLUMN black_deacon_creation_info.updated_at IS '更新时间';
|
||||||
|
COMMENT ON COLUMN black_deacon_creation_info.deleted_at IS '删除时间(软删)';
|
||||||
|
COMMENT ON COLUMN black_deacon_creation_info.html_file_url IS 'HTML文件地址';
|
||||||
|
COMMENT ON COLUMN black_deacon_creation_info.image_urls IS '图片地址列表';
|
||||||
|
COMMENT ON COLUMN black_deacon_creation_info.content_type IS '素材类型';
|
||||||
|
COMMENT ON COLUMN black_deacon_creation_info.theme IS '主题';
|
||||||
|
COMMENT ON COLUMN black_deacon_creation_info.title IS '标题';
|
||||||
|
|
||||||
|
--------------------pgsql创建creation_info表语句---------------------------
|
||||||
|
|
||||||
|
--------------------pgsql创建black_deacon_file_temp表语句---------------------------
|
||||||
|
-- 临时文件表
|
||||||
|
CREATE TABLE IF NOT EXISTS black_deacon_file_temp (
|
||||||
|
-- 基础字段(完全对齐项目规范)
|
||||||
|
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),
|
||||||
|
|
||||||
|
-- 业务字段
|
||||||
|
business_id VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
file_url VARCHAR(512) NOT NULL DEFAULT ''
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 索引
|
||||||
|
CREATE INDEX idx_file_temp_tenant_id ON black_deacon_file_temp(tenant_id);
|
||||||
|
CREATE INDEX idx_file_temp_business_id ON black_deacon_file_temp(business_id);
|
||||||
|
CREATE INDEX idx_file_temp_file_url ON black_deacon_file_temp(file_url);
|
||||||
|
CREATE INDEX idx_file_temp_deleted_at ON black_deacon_file_temp(deleted_at);
|
||||||
|
|
||||||
|
-- 注释
|
||||||
|
COMMENT ON TABLE black_deacon_file_temp IS '临时文件表';
|
||||||
|
COMMENT ON COLUMN black_deacon_file_temp.id IS '主键ID';
|
||||||
|
COMMENT ON COLUMN black_deacon_file_temp.tenant_id IS '租户ID';
|
||||||
|
COMMENT ON COLUMN black_deacon_file_temp.creator IS '创建人';
|
||||||
|
COMMENT ON COLUMN black_deacon_file_temp.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN black_deacon_file_temp.updater IS '更新人';
|
||||||
|
COMMENT ON COLUMN black_deacon_file_temp.updated_at IS '更新时间';
|
||||||
|
COMMENT ON COLUMN black_deacon_file_temp.deleted_at IS '删除时间(软删)';
|
||||||
|
COMMENT ON COLUMN black_deacon_file_temp.business_id IS '业务ID';
|
||||||
|
COMMENT ON COLUMN black_deacon_file_temp.file_url IS '文件地址';
|
||||||
|
--------------------pgsql创建black_deacon_file_temp表语句---------------------------
|
||||||
|
|
||||||
|
--------------------pgsql创建black_deacon_skill_template表语句---------------------------
|
||||||
|
-- 技能模板表
|
||||||
|
CREATE TABLE IF NOT EXISTS black_deacon_skill_template (
|
||||||
|
-- 基础字段(完全对齐项目规范)
|
||||||
|
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),
|
||||||
|
|
||||||
|
-- 业务字段
|
||||||
|
name VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
|
description TEXT DEFAULT '',
|
||||||
|
file_name VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
file_url VARCHAR(512) NOT NULL DEFAULT ''
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 索引
|
||||||
|
CREATE INDEX idx_skill_template_tenant_id ON black_deacon_skill_template(tenant_id);
|
||||||
|
CREATE INDEX idx_skill_template_deleted_at ON black_deacon_skill_template(deleted_at);
|
||||||
|
|
||||||
|
-- 注释
|
||||||
|
COMMENT ON TABLE black_deacon_skill_template IS '技能模板表';
|
||||||
|
COMMENT ON COLUMN black_deacon_skill_template.id IS '主键ID';
|
||||||
|
COMMENT ON COLUMN black_deacon_skill_template.tenant_id IS '租户ID';
|
||||||
|
COMMENT ON COLUMN black_deacon_skill_template.creator IS '创建人';
|
||||||
|
COMMENT ON COLUMN black_deacon_skill_template.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN black_deacon_skill_template.updater IS '更新人';
|
||||||
|
COMMENT ON COLUMN black_deacon_skill_template.updated_at IS '更新时间';
|
||||||
|
COMMENT ON COLUMN black_deacon_skill_template.deleted_at IS '删除时间(软删)';
|
||||||
|
COMMENT ON COLUMN black_deacon_skill_template.name IS '技能模板名称';
|
||||||
|
COMMENT ON COLUMN black_deacon_skill_template.description IS '描述';
|
||||||
|
COMMENT ON COLUMN black_deacon_skill_template.file_name IS '文件名称';
|
||||||
|
COMMENT ON COLUMN black_deacon_skill_template.file_url IS '文件地址';
|
||||||
|
--------------------pgsql创建black_deacon_skill_template表语句---------------------------
|
||||||
|
|
||||||
|
--------------------pgsql创建black_deacon_skill_user表语句---------------------------
|
||||||
|
-- 技能用户表
|
||||||
|
CREATE TABLE IF NOT EXISTS black_deacon_skill_user (
|
||||||
|
-- 基础字段(完全对齐项目规范)
|
||||||
|
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),
|
||||||
|
|
||||||
|
-- 业务字段
|
||||||
|
name VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
|
description TEXT DEFAULT '',
|
||||||
|
file_name VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
file_url VARCHAR(512) NOT NULL DEFAULT ''
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 索引
|
||||||
|
CREATE INDEX idx_skill_user_tenant_id ON black_deacon_skill_user(tenant_id);
|
||||||
|
CREATE INDEX idx_skill_user_deleted_at ON black_deacon_skill_user(deleted_at);
|
||||||
|
|
||||||
|
-- 注释
|
||||||
|
COMMENT ON TABLE black_deacon_skill_user IS '技能用户表';
|
||||||
|
COMMENT ON COLUMN black_deacon_skill_user.id IS '主键ID';
|
||||||
|
COMMENT ON COLUMN black_deacon_skill_user.tenant_id IS '租户ID';
|
||||||
|
COMMENT ON COLUMN black_deacon_skill_user.creator IS '创建人';
|
||||||
|
COMMENT ON COLUMN black_deacon_skill_user.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN black_deacon_skill_user.updater IS '更新人';
|
||||||
|
COMMENT ON COLUMN black_deacon_skill_user.updated_at IS '更新时间';
|
||||||
|
COMMENT ON COLUMN black_deacon_skill_user.deleted_at IS '删除时间(软删)';
|
||||||
|
COMMENT ON COLUMN black_deacon_skill_user.name IS '技能名称';
|
||||||
|
COMMENT ON COLUMN black_deacon_skill_user.description IS '描述';
|
||||||
|
COMMENT ON COLUMN black_deacon_skill_user.file_name IS '文件名称';
|
||||||
|
COMMENT ON COLUMN black_deacon_skill_user.file_url IS '文件地址';
|
||||||
|
--------------------pgsql创建black_deacon_skill_user表语句---------------------------
|
||||||
|
|
||||||
|
--------------------pgsql创建black_deacon_flow_execution表语句---------------------------
|
||||||
|
-- 流程执行记录表
|
||||||
|
CREATE TABLE IF NOT EXISTS black_deacon_flow_execution (
|
||||||
|
-- 基础字段(完全对齐项目规范)
|
||||||
|
id BIGINT PRIMARY KEY, -- 主键ID(非自增)
|
||||||
|
tenant_id BIGINT NOT NULL DEFAULT 0, -- 租户ID int8
|
||||||
|
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),
|
||||||
|
|
||||||
|
-- 业务字段
|
||||||
|
flow_user_id BIGINT NOT NULL, -- 流程ID
|
||||||
|
flow_name VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
|
node_group_id varchar(64) NOT NULL DEFAULT '',
|
||||||
|
total_tokens integer NOT NULL DEFAULT 0,
|
||||||
|
trigger_type VARCHAR(32) NOT NULL DEFAULT '', -- 触发类型
|
||||||
|
duration_ms BIGINT NOT NULL DEFAULT 0, -- 执行时长(毫秒)
|
||||||
|
status SMALLINT NOT NULL DEFAULT 1, -- 状态:1-运行中,2-成功,3-失败
|
||||||
|
flow_content JSONB DEFAULT '{}', -- 流程模板内容
|
||||||
|
node_input_params JSONB DEFAULT '[]'::JSONB,
|
||||||
|
output_params JSONB DEFAULT '[]'::JSONB,
|
||||||
|
error_message TEXT DEFAULT '', -- 错误信息
|
||||||
|
trace_id VARCHAR(64) DEFAULT '', -- 跟踪ID
|
||||||
|
session_id VARCHAR(64) DEFAULT '' -- 会话ID
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 索引(高频查询)
|
||||||
|
CREATE INDEX idx_bfe_tenant_id ON black_deacon_flow_execution(tenant_id);
|
||||||
|
CREATE INDEX idx_bfe_flow_user_id ON black_deacon_flow_execution(flow_user_id);
|
||||||
|
CREATE INDEX idx_bfe_trace_id ON black_deacon_flow_execution(trace_id);
|
||||||
|
CREATE INDEX idx_bfe_status ON black_deacon_flow_execution(status);
|
||||||
|
CREATE INDEX idx_bfe_deleted_at ON black_deacon_flow_execution(deleted_at);
|
||||||
|
|
||||||
|
-- 表和字段注释
|
||||||
|
COMMENT ON TABLE black_deacon_flow_execution IS '流程执行记录表';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_execution.id IS '主键ID(非自增)';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_execution.tenant_id IS '租户ID';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_execution.creator IS '创建人';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_execution.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_execution.updater IS '更新人';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_execution.updated_at IS '更新时间';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_execution.deleted_at IS '删除时间(软删)';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_execution.flow_user_id IS '流程ID';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_execution.flow_name IS '流程名称';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_execution.total_tokens IS '总token消耗';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_execution.node_group_id IS '节点组ID';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_execution.trigger_type IS '触发类型';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_execution.duration_ms IS '执行时长(毫秒)';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_execution.status IS '状态:1-运行中,2-成功,3-失败';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_execution.flow_content IS '流程模板内容';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_execution.node_input_params IS '节点输入参数';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_execution.output_params IS '输出参数';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_execution.error_message IS '错误信息';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_execution.trace_id IS '跟踪ID';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_execution.session_id IS '会话ID';
|
||||||
|
--------------------pgsql创建black_deacon_flow_execution表语句---------------------------
|
||||||
|
|
||||||
|
--------------------pgsql创建black_deacon_flow_user表语句---------------------------
|
||||||
|
-- 用户流程表
|
||||||
|
CREATE TABLE IF NOT EXISTS black_deacon_flow_user (
|
||||||
|
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),
|
||||||
|
|
||||||
|
flow_name VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
|
description TEXT DEFAULT '',
|
||||||
|
flow_content JSONB DEFAULT '{}',
|
||||||
|
node_input_params JSONB DEFAULT '[]'::JSONB,
|
||||||
|
access_level VARCHAR(32) NOT NULL DEFAULT '1',
|
||||||
|
source_flow_template_id BIGINT NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
CREATE INDEX idx_flow_user_tenant ON black_deacon_flow_user(tenant_id);
|
||||||
|
COMMENT ON TABLE black_deacon_flow_user IS '用户流程表';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_user.flow_name IS '流程名称';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_user.description IS '流程描述';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_user.flow_content IS '流程内容';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_user.node_input_params IS '节点输入参数';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_user.access_level IS '访问权限:1私有,2团队,3公开';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_user.source_flow_template_id IS '来源流程模板ID';
|
||||||
|
--------------------pgsql创建black_deacon_flow_user表语句---------------------------
|
||||||
|
|
||||||
|
--------------------pgsql创建black_deacon_flow_template表语句---------------------------
|
||||||
|
-- 流程模板表
|
||||||
|
CREATE TABLE IF NOT EXISTS black_deacon_flow_template (
|
||||||
|
-- 基础字段(完全对齐项目规范)
|
||||||
|
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),
|
||||||
|
|
||||||
|
-- 业务字段
|
||||||
|
flow_template_name VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
|
description TEXT DEFAULT '',
|
||||||
|
category_code VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
|
category_name VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
|
flow_content JSONB DEFAULT '{}',
|
||||||
|
node_input_params JSONB DEFAULT '[]',
|
||||||
|
status SMALLINT NOT NULL DEFAULT 1
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 索引
|
||||||
|
CREATE INDEX idx_flow_template_tenant_id ON black_deacon_flow_template(tenant_id);
|
||||||
|
CREATE INDEX idx_flow_template_category_code ON black_deacon_flow_template(category_code);
|
||||||
|
CREATE INDEX idx_flow_template_status ON black_deacon_flow_template(status);
|
||||||
|
CREATE INDEX idx_flow_template_deleted_at ON black_deacon_flow_template(deleted_at);
|
||||||
|
|
||||||
|
-- 注释
|
||||||
|
COMMENT ON TABLE black_deacon_flow_template IS '流程模板表';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_template.id IS '主键ID';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_template.tenant_id IS '租户ID';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_template.creator IS '创建人';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_template.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_template.updater IS '更新人';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_template.updated_at IS '更新时间';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_template.deleted_at IS '删除时间(软删)';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_template.flow_template_name IS '流程模板名称';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_template.description IS '流程描述';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_template.category_code IS '流程分类编码';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_template.category_name IS '流程分类名称';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_template.flow_content IS '流程内容';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_template.node_input_params IS '节点输入参数';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_template.status IS '流程状态:1启用/0停用';
|
||||||
|
--------------------pgsql创建black_deacon_flow_template表语句---------------------------
|
||||||
|
|
||||||
|
--------------------pgsql创建black_deacon_active_pull表语句---------------------------
|
||||||
|
-- 主动拉取记录表
|
||||||
|
CREATE TABLE IF NOT EXISTS black_deacon_active_pull (
|
||||||
|
-- 基础字段(完全对齐项目规范)
|
||||||
|
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),
|
||||||
|
|
||||||
|
-- 业务字段
|
||||||
|
type VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
|
request_parament JSONB DEFAULT '{}',
|
||||||
|
response_parament JSONB DEFAULT '{}',
|
||||||
|
extension JSONB DEFAULT '{}'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 索引
|
||||||
|
CREATE INDEX idx_active_pull_tenant_id ON black_deacon_active_pull(tenant_id);
|
||||||
|
CREATE INDEX idx_active_pull_type ON black_deacon_active_pull("type");
|
||||||
|
CREATE INDEX idx_active_pull_deleted_at ON black_deacon_active_pull(deleted_at);
|
||||||
|
|
||||||
|
-- 注释
|
||||||
|
COMMENT ON TABLE black_deacon_active_pull IS '主动拉取记录表';
|
||||||
|
COMMENT ON COLUMN black_deacon_active_pull.id IS '主键ID';
|
||||||
|
COMMENT ON COLUMN black_deacon_active_pull.tenant_id IS '租户ID';
|
||||||
|
COMMENT ON COLUMN black_deacon_active_pull.creator IS '创建人';
|
||||||
|
COMMENT ON COLUMN black_deacon_active_pull.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN black_deacon_active_pull.updater IS '更新人';
|
||||||
|
COMMENT ON COLUMN black_deacon_active_pull.updated_at IS '更新时间';
|
||||||
|
COMMENT ON COLUMN black_deacon_active_pull.deleted_at IS '删除时间(软删)';
|
||||||
|
COMMENT ON COLUMN black_deacon_active_pull.type IS '类型';
|
||||||
|
COMMENT ON COLUMN black_deacon_active_pull.request_parament IS '请求参数';
|
||||||
|
COMMENT ON COLUMN black_deacon_active_pull.response_parament IS '响应参数';
|
||||||
|
COMMENT ON COLUMN black_deacon_active_pull.extension IS '扩展信息';
|
||||||
|
--------------------pgsql创建black_deacon_active_pull表语句---------------------------
|
||||||
|
|
||||||
|
--------------------pgsql创建black_deacon_node_prompt表语句---------------------------
|
||||||
|
-- 节点提示词配置表
|
||||||
|
CREATE TABLE IF NOT EXISTS black_deacon_node_prompt (
|
||||||
|
-- 基础字段(完全对齐项目规范)
|
||||||
|
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),
|
||||||
|
|
||||||
|
-- 业务字段
|
||||||
|
node_type VARCHAR(64) NOT NULL DEFAULT '', -- 节点类型
|
||||||
|
prompt TEXT NOT NULL DEFAULT '', -- 提示词内容
|
||||||
|
source_type SMALLINT NOT NULL DEFAULT 1 -- 来源:1=系统初始化,2=用户自定义
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 索引
|
||||||
|
CREATE INDEX idx_node_prompt_tenant_id ON black_deacon_node_prompt(tenant_id);
|
||||||
|
CREATE INDEX idx_node_prompt_node_type ON black_deacon_node_prompt(node_type);
|
||||||
|
CREATE INDEX idx_node_prompt_source_type ON black_deacon_node_prompt(source_type);
|
||||||
|
CREATE INDEX idx_node_prompt_deleted_at ON black_deacon_node_prompt(deleted_at);
|
||||||
|
|
||||||
|
-- 注释
|
||||||
|
COMMENT ON TABLE black_deacon_node_prompt IS '节点提示词配置表';
|
||||||
|
COMMENT ON COLUMN black_deacon_node_prompt.id IS '主键ID';
|
||||||
|
COMMENT ON COLUMN black_deacon_node_prompt.tenant_id IS '租户ID';
|
||||||
|
COMMENT ON COLUMN black_deacon_node_prompt.creator IS '创建人';
|
||||||
|
COMMENT ON COLUMN black_deacon_node_prompt.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN black_deacon_node_prompt.updater IS '更新人';
|
||||||
|
COMMENT ON COLUMN black_deacon_node_prompt.updated_at IS '更新时间';
|
||||||
|
COMMENT ON COLUMN black_deacon_node_prompt.deleted_at IS '删除时间(软删)';
|
||||||
|
COMMENT ON COLUMN black_deacon_node_prompt.node_type IS '节点类型';
|
||||||
|
COMMENT ON COLUMN black_deacon_node_prompt.prompt IS '提示词内容';
|
||||||
|
COMMENT ON COLUMN black_deacon_node_prompt.source_type IS '数据来源:1=系统初始化,2=用户自定义';
|
||||||
|
--------------------pgsql创建black_deacon_node_prompt表语句---------------------------
|
||||||
|
|
||||||
|
--------------------pgsql创建black_deacon_node_execution表语句---------------------------
|
||||||
|
-- 节点执行记录表
|
||||||
|
-- 记录每个节点的入参、出参、token消耗、执行状态等详细信息
|
||||||
|
CREATE TABLE IF NOT EXISTS black_deacon_node_execution (
|
||||||
|
-- 基础字段(完全对齐项目规范)
|
||||||
|
id BIGINT PRIMARY KEY, -- 主键ID(非自增)
|
||||||
|
tenant_id BIGINT NOT NULL DEFAULT 0, -- 租户ID int8
|
||||||
|
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),
|
||||||
|
|
||||||
|
-- 业务字段
|
||||||
|
flow_execution_id BIGINT NOT NULL, -- 流程执行ID
|
||||||
|
node_id VARCHAR(64) NOT NULL DEFAULT '', -- 节点ID
|
||||||
|
node_name VARCHAR(128) NOT NULL DEFAULT '', -- 节点名称
|
||||||
|
node_group_id VARCHAR(64) NOT NULL DEFAULT '', -- 节点分组ID
|
||||||
|
input_params JSONB DEFAULT '{}', -- 节点输入参数
|
||||||
|
input_params_path VARCHAR(256) DEFAULT '', -- 节点输入参数路径
|
||||||
|
output_params JSONB DEFAULT '{}', -- 节点输出参数
|
||||||
|
output_params_path VARCHAR(256) DEFAULT '',
|
||||||
|
prompt_tokens INTEGER NOT NULL DEFAULT 0, -- 提示词token消耗
|
||||||
|
completion_tokens INTEGER NOT NULL DEFAULT 0, -- 补全token消耗
|
||||||
|
total_tokens INTEGER NOT NULL DEFAULT 0, -- 总token消耗
|
||||||
|
token_info JSONB DEFAULT '[]', -- 节点token明细(汇总节点聚合 total_tokens/total_fee 用)
|
||||||
|
status SMALLINT NOT NULL DEFAULT 1, -- 执行状态:1-运行中,2-成功,3-失败,4-暂停,5-等待执行
|
||||||
|
duration_ms BIGINT NOT NULL DEFAULT 0, -- 执行时长(毫秒)
|
||||||
|
error_message TEXT DEFAULT '' -- 错误信息
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 存量库补列
|
||||||
|
ALTER TABLE black_deacon_node_execution ADD COLUMN IF NOT EXISTS token_info JSONB DEFAULT '[]';
|
||||||
|
|
||||||
|
-- 索引(高频查询)
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_bne_tenant_id ON black_deacon_node_execution(tenant_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_bne_flow_execution_id ON black_deacon_node_execution(flow_execution_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_bne_node_id ON black_deacon_node_execution(node_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_bne_status ON black_deacon_node_execution(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_bne_deleted_at ON black_deacon_node_execution(deleted_at);
|
||||||
|
|
||||||
|
-- 表和字段注释
|
||||||
|
COMMENT ON TABLE black_deacon_node_execution IS '节点执行记录表';
|
||||||
|
COMMENT ON COLUMN black_deacon_node_execution.id IS '主键ID(非自增)';
|
||||||
|
COMMENT ON COLUMN black_deacon_node_execution.tenant_id IS '租户ID';
|
||||||
|
COMMENT ON COLUMN black_deacon_node_execution.creator IS '创建人';
|
||||||
|
COMMENT ON COLUMN black_deacon_node_execution.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN black_deacon_node_execution.updater IS '更新人';
|
||||||
|
COMMENT ON COLUMN black_deacon_node_execution.updated_at IS '更新时间';
|
||||||
|
COMMENT ON COLUMN black_deacon_node_execution.deleted_at IS '删除时间(软删)';
|
||||||
|
COMMENT ON COLUMN black_deacon_node_execution.flow_execution_id IS '流程执行ID';
|
||||||
|
COMMENT ON COLUMN black_deacon_node_execution.node_id IS '节点ID';
|
||||||
|
COMMENT ON COLUMN black_deacon_node_execution.node_name IS '节点名称';
|
||||||
|
COMMENT ON COLUMN black_deacon_node_execution.node_group_id IS '节点分组ID';
|
||||||
|
COMMENT ON COLUMN black_deacon_node_execution.input_params IS '节点输入参数';
|
||||||
|
COMMENT ON COLUMN black_deacon_node_execution.input_params_path IS '节点输入参数路径';
|
||||||
|
COMMENT ON COLUMN black_deacon_node_execution.output_params IS '节点输出参数';
|
||||||
|
COMMENT ON COLUMN black_deacon_node_execution.output_params_path IS '节点输出参数路径';
|
||||||
|
COMMENT ON COLUMN black_deacon_node_execution.prompt_tokens IS '提示词token消耗';
|
||||||
|
COMMENT ON COLUMN black_deacon_node_execution.completion_tokens IS '补全token消耗';
|
||||||
|
COMMENT ON COLUMN black_deacon_node_execution.total_tokens IS '总token消耗';
|
||||||
|
COMMENT ON COLUMN black_deacon_node_execution.token_info IS '节点token明细(汇总节点聚合 total_tokens/total_fee 用)';
|
||||||
|
COMMENT ON COLUMN black_deacon_node_execution.status IS '执行状态:1-运行中,2-成功,3-失败,4-暂停,5-等待执行';
|
||||||
|
COMMENT ON COLUMN black_deacon_node_execution.duration_ms IS '执行时长(毫秒)';
|
||||||
|
COMMENT ON COLUMN black_deacon_node_execution.error_message IS '错误信息';
|
||||||
|
--------------------pgsql创建black_deacon_node_execution表语句---------------------------
|
||||||
|
|
||||||
|
-- ========== 会话记录 + 工作流/普通会话结果表(2026-08-13) ==========
|
||||||
|
|
||||||
|
--------------------pgsql创建black_deacon_session表语句---------------------------
|
||||||
|
-- 会话记录表
|
||||||
|
CREATE TABLE IF NOT EXISTS black_deacon_session (
|
||||||
|
-- 基础字段(完全对齐项目规范)
|
||||||
|
id BIGINT PRIMARY KEY, -- 主键ID(非自增)
|
||||||
|
tenant_id BIGINT NOT NULL DEFAULT 0, -- 租户ID int8
|
||||||
|
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),
|
||||||
|
|
||||||
|
-- 业务字段
|
||||||
|
session_id VARCHAR(64) NOT NULL, -- 会话ID(前端传入,雪花ID)
|
||||||
|
session_name VARCHAR(128) NOT NULL DEFAULT '' -- 会话名称
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 索引(高频查询)
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS uk_session_tenant_session_id ON black_deacon_session(tenant_id, session_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_session_tenant_id ON black_deacon_session(tenant_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_session_creator ON black_deacon_session(creator);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_session_deleted_at ON black_deacon_session(deleted_at);
|
||||||
|
|
||||||
|
-- 表和字段注释
|
||||||
|
COMMENT ON TABLE black_deacon_session IS '会话记录表';
|
||||||
|
COMMENT ON COLUMN black_deacon_session.id IS '主键ID(非自增)';
|
||||||
|
COMMENT ON COLUMN black_deacon_session.tenant_id IS '租户ID';
|
||||||
|
COMMENT ON COLUMN black_deacon_session.creator IS '创建人';
|
||||||
|
COMMENT ON COLUMN black_deacon_session.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN black_deacon_session.updater IS '更新人';
|
||||||
|
COMMENT ON COLUMN black_deacon_session.updated_at IS '更新时间';
|
||||||
|
COMMENT ON COLUMN black_deacon_session.deleted_at IS '删除时间(软删)';
|
||||||
|
COMMENT ON COLUMN black_deacon_session.session_id IS '会话ID(前端传入,雪花ID)';
|
||||||
|
COMMENT ON COLUMN black_deacon_session.session_name IS '会话名称';
|
||||||
|
--------------------pgsql创建black_deacon_session表语句---------------------------
|
||||||
|
|
||||||
|
--------------------pgsql创建black_deacon_exec_chat表语句---------------------------
|
||||||
|
-- 普通会话执行记录表
|
||||||
|
CREATE TABLE IF NOT EXISTS black_deacon_exec_chat (
|
||||||
|
-- 基础字段(完全对齐项目规范)
|
||||||
|
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),
|
||||||
|
|
||||||
|
-- 业务字段
|
||||||
|
session_id VARCHAR(64) NOT NULL DEFAULT '', -- 会话ID
|
||||||
|
duration BIGINT NOT NULL DEFAULT 0, -- 执行时长(秒)
|
||||||
|
request_params JSONB DEFAULT '{}', -- 请求参数
|
||||||
|
result_file_url VARCHAR(512) DEFAULT '', -- 结果文件路径(OSS)
|
||||||
|
total_tokens INTEGER NOT NULL DEFAULT 0, -- 总token消耗
|
||||||
|
total_fee DOUBLE PRECISION NOT NULL DEFAULT 0, -- 总费用
|
||||||
|
error_message TEXT DEFAULT '', -- 错误信息(友好提示)
|
||||||
|
error TEXT DEFAULT '' -- 错误明细(原始错误)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 索引(高频查询)
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_exec_chat_tenant_id ON black_deacon_exec_chat(tenant_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_exec_chat_session_id ON black_deacon_exec_chat(session_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_exec_chat_deleted_at ON black_deacon_exec_chat(deleted_at);
|
||||||
|
|
||||||
|
-- 表和字段注释
|
||||||
|
COMMENT ON TABLE black_deacon_exec_chat IS '普通会话执行记录表';
|
||||||
|
COMMENT ON COLUMN black_deacon_exec_chat.id IS '主键ID';
|
||||||
|
COMMENT ON COLUMN black_deacon_exec_chat.tenant_id IS '租户ID';
|
||||||
|
COMMENT ON COLUMN black_deacon_exec_chat.creator IS '创建人';
|
||||||
|
COMMENT ON COLUMN black_deacon_exec_chat.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN black_deacon_exec_chat.updater IS '更新人';
|
||||||
|
COMMENT ON COLUMN black_deacon_exec_chat.updated_at IS '更新时间';
|
||||||
|
COMMENT ON COLUMN black_deacon_exec_chat.deleted_at IS '删除时间(软删)';
|
||||||
|
COMMENT ON COLUMN black_deacon_exec_chat.session_id IS '会话ID';
|
||||||
|
COMMENT ON COLUMN black_deacon_exec_chat.duration IS '执行时长(秒)';
|
||||||
|
COMMENT ON COLUMN black_deacon_exec_chat.request_params IS '请求参数';
|
||||||
|
COMMENT ON COLUMN black_deacon_exec_chat.result_file_url IS '结果文件路径(OSS)';
|
||||||
|
COMMENT ON COLUMN black_deacon_exec_chat.total_tokens IS '总token消耗';
|
||||||
|
COMMENT ON COLUMN black_deacon_exec_chat.total_fee IS '总费用';
|
||||||
|
COMMENT ON COLUMN black_deacon_exec_chat.error_message IS '错误信息(友好提示)';
|
||||||
|
COMMENT ON COLUMN black_deacon_exec_chat.error IS '错误明细(原始错误)';
|
||||||
|
-- 兼容已有库:错误信息拆分,error_message 存友好提示、error 存原始错误
|
||||||
|
ALTER TABLE black_deacon_exec_chat ADD COLUMN IF NOT EXISTS error TEXT DEFAULT '';
|
||||||
|
--------------------pgsql创建black_deacon_exec_chat表语句---------------------------
|
||||||
|
|
||||||
|
--------------------pgsql创建black_deacon_exec_workflow表语句---------------------------
|
||||||
|
-- 工作流执行记录表
|
||||||
|
CREATE TABLE IF NOT EXISTS black_deacon_exec_workflow (
|
||||||
|
-- 基础字段(完全对齐项目规范)
|
||||||
|
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),
|
||||||
|
|
||||||
|
-- 业务字段
|
||||||
|
session_id VARCHAR(64) NOT NULL DEFAULT '', -- 会话ID
|
||||||
|
flow_id BIGINT NOT NULL DEFAULT 0, -- 工作流ID
|
||||||
|
duration BIGINT NOT NULL DEFAULT 0, -- 执行时长(秒)
|
||||||
|
request_params JSONB DEFAULT '{}', -- 请求参数
|
||||||
|
status SMALLINT NOT NULL DEFAULT 1, -- 状态:1-运行中,2-成功,3-失败
|
||||||
|
total_tokens INTEGER NOT NULL DEFAULT 0, -- 总token消耗
|
||||||
|
total_fee DOUBLE PRECISION NOT NULL DEFAULT 0, -- 总费用
|
||||||
|
error_message TEXT DEFAULT '', -- 错误信息(友好提示)
|
||||||
|
error TEXT DEFAULT '' -- 错误明细(原始错误)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 索引(高频查询)
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_exec_workflow_tenant_id ON black_deacon_exec_workflow(tenant_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_exec_workflow_session_id ON black_deacon_exec_workflow(session_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_exec_workflow_flow_id ON black_deacon_exec_workflow(flow_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_exec_workflow_status ON black_deacon_exec_workflow(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_exec_workflow_deleted_at ON black_deacon_exec_workflow(deleted_at);
|
||||||
|
|
||||||
|
-- 表和字段注释
|
||||||
|
COMMENT ON TABLE black_deacon_exec_workflow IS '工作流执行记录表';
|
||||||
|
COMMENT ON COLUMN black_deacon_exec_workflow.id IS '主键ID';
|
||||||
|
COMMENT ON COLUMN black_deacon_exec_workflow.tenant_id IS '租户ID';
|
||||||
|
COMMENT ON COLUMN black_deacon_exec_workflow.creator IS '创建人';
|
||||||
|
COMMENT ON COLUMN black_deacon_exec_workflow.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN black_deacon_exec_workflow.updater IS '更新人';
|
||||||
|
COMMENT ON COLUMN black_deacon_exec_workflow.updated_at IS '更新时间';
|
||||||
|
COMMENT ON COLUMN black_deacon_exec_workflow.deleted_at IS '删除时间(软删)';
|
||||||
|
COMMENT ON COLUMN black_deacon_exec_workflow.session_id IS '会话ID';
|
||||||
|
COMMENT ON COLUMN black_deacon_exec_workflow.flow_id IS '工作流ID';
|
||||||
|
COMMENT ON COLUMN black_deacon_exec_workflow.duration IS '执行时长(秒)';
|
||||||
|
COMMENT ON COLUMN black_deacon_exec_workflow.request_params IS '请求参数';
|
||||||
|
COMMENT ON COLUMN black_deacon_exec_workflow.status IS '状态:1-运行中,2-成功,3-失败';
|
||||||
|
COMMENT ON COLUMN black_deacon_exec_workflow.total_tokens IS '总token消耗';
|
||||||
|
COMMENT ON COLUMN black_deacon_exec_workflow.total_fee IS '总费用';
|
||||||
|
COMMENT ON COLUMN black_deacon_exec_workflow.error_message IS '错误信息(友好提示)';
|
||||||
|
COMMENT ON COLUMN black_deacon_exec_workflow.error IS '错误明细(原始错误)';
|
||||||
|
-- 兼容已有库:错误信息拆分,error_message 存友好提示、error 存原始错误
|
||||||
|
ALTER TABLE black_deacon_exec_workflow ADD COLUMN IF NOT EXISTS error TEXT DEFAULT '';
|
||||||
|
--------------------pgsql创建black_deacon_exec_workflow表语句---------------------------
|
||||||
|
|
||||||
|
--------------------pgsql创建black_deacon_exec_workflow_result表语句---------------------------
|
||||||
|
-- 工作流执行结果表
|
||||||
|
CREATE TABLE IF NOT EXISTS black_deacon_exec_workflow_result (
|
||||||
|
-- 基础字段(完全对齐项目规范)
|
||||||
|
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),
|
||||||
|
|
||||||
|
-- 业务字段
|
||||||
|
session_id VARCHAR(64) NOT NULL DEFAULT '', -- 会话ID
|
||||||
|
flow_id BIGINT NOT NULL DEFAULT 0, -- 工作流ID
|
||||||
|
exec_id BIGINT NOT NULL DEFAULT 0, -- 执行ID
|
||||||
|
result_file_url VARCHAR(512) DEFAULT '' -- 结果文件路径(OSS)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 索引(高频查询)
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_exec_workflow_result_tenant_id ON black_deacon_exec_workflow_result(tenant_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_exec_workflow_result_session_id ON black_deacon_exec_workflow_result(session_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_exec_workflow_result_exec_id ON black_deacon_exec_workflow_result(exec_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_exec_workflow_result_deleted_at ON black_deacon_exec_workflow_result(deleted_at);
|
||||||
|
|
||||||
|
-- 表和字段注释
|
||||||
|
COMMENT ON TABLE black_deacon_exec_workflow_result IS '工作流执行结果表';
|
||||||
|
COMMENT ON COLUMN black_deacon_exec_workflow_result.id IS '主键ID';
|
||||||
|
COMMENT ON COLUMN black_deacon_exec_workflow_result.tenant_id IS '租户ID';
|
||||||
|
COMMENT ON COLUMN black_deacon_exec_workflow_result.creator IS '创建人';
|
||||||
|
COMMENT ON COLUMN black_deacon_exec_workflow_result.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN black_deacon_exec_workflow_result.updater IS '更新人';
|
||||||
|
COMMENT ON COLUMN black_deacon_exec_workflow_result.updated_at IS '更新时间';
|
||||||
|
COMMENT ON COLUMN black_deacon_exec_workflow_result.deleted_at IS '删除时间(软删)';
|
||||||
|
COMMENT ON COLUMN black_deacon_exec_workflow_result.session_id IS '会话ID';
|
||||||
|
COMMENT ON COLUMN black_deacon_exec_workflow_result.flow_id IS '工作流ID';
|
||||||
|
COMMENT ON COLUMN black_deacon_exec_workflow_result.exec_id IS '执行ID';
|
||||||
|
COMMENT ON COLUMN black_deacon_exec_workflow_result.result_file_url IS '结果文件路径(OSS)';
|
||||||
|
--------------------pgsql创建black_deacon_exec_workflow_result表语句---------------------------
|
||||||
|
--------------------pgsql创建black_deacon_flow_segment_result表语句---------------------------
|
||||||
|
-- 视频节点段级生成结果表(只存成功段,断点续跑复用;失败段不落库视为需重新生成)
|
||||||
|
CREATE TABLE IF NOT EXISTS black_deacon_flow_segment_result (
|
||||||
|
-- 基础字段(完全对齐项目规范)
|
||||||
|
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),
|
||||||
|
|
||||||
|
-- 业务字段
|
||||||
|
execution_id BIGINT NOT NULL DEFAULT 0, -- 执行ID(exec_workflow;reExecute 复用同一条)
|
||||||
|
node_id VARCHAR(64) NOT NULL DEFAULT '', -- 视频生成节点ID
|
||||||
|
segment_index INT NOT NULL DEFAULT 0, -- 段序号
|
||||||
|
video_key VARCHAR(128) NOT NULL DEFAULT '', -- 视频输出字段key(重建输出记录保 key 一致,避免下游引用失配)
|
||||||
|
video_url VARCHAR(512) NOT NULL DEFAULT '' -- 已生成成功的视频地址
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 唯一键 + 高频查询索引(ListByNode 命中 (execution_id, node_id) 前缀)
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS uk_segment_result_exec_node_idx ON black_deacon_flow_segment_result(execution_id, node_id, segment_index);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_segment_result_tenant_id ON black_deacon_flow_segment_result(tenant_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_segment_result_deleted_at ON black_deacon_flow_segment_result(deleted_at);
|
||||||
|
|
||||||
|
-- 表和字段注释
|
||||||
|
COMMENT ON TABLE black_deacon_flow_segment_result IS '视频节点段级生成结果表';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_segment_result.id IS '主键ID';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_segment_result.tenant_id IS '租户ID';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_segment_result.creator IS '创建人';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_segment_result.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_segment_result.updater IS '更新人';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_segment_result.updated_at IS '更新时间';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_segment_result.deleted_at IS '删除时间';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_segment_result.execution_id IS '执行ID';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_segment_result.node_id IS '视频生成节点ID';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_segment_result.segment_index IS '段序号';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_segment_result.video_key IS '视频输出字段key';
|
||||||
|
COMMENT ON COLUMN black_deacon_flow_segment_result.video_url IS '已生成成功的视频地址';
|
||||||
|
--------------------pgsql创建black_deacon_flow_segment_result表语句---------------------------
|
||||||
|
|
||||||
|
--------------------工作流重试兜底:exec_workflow 新增重试/心跳列---------------------
|
||||||
|
ALTER TABLE black_deacon_exec_workflow ADD COLUMN IF NOT EXISTS retryable SMALLINT NOT NULL DEFAULT 0; -- 0=终局不重试(用户取消/计费门禁拦截),1=可重试(程序报错/关停中断/超时等)
|
||||||
|
ALTER TABLE black_deacon_exec_workflow ADD COLUMN IF NOT EXISTS retry_count INTEGER NOT NULL DEFAULT 0; -- 已重试次数
|
||||||
|
ALTER TABLE black_deacon_exec_workflow ADD COLUMN IF NOT EXISTS last_heartbeat BIGINT NOT NULL DEFAULT 0; -- 最后心跳(毫秒时间戳)
|
||||||
|
COMMENT ON COLUMN black_deacon_exec_workflow.retryable IS '是否可重试:0-用户取消,1-程序报错';
|
||||||
|
COMMENT ON COLUMN black_deacon_exec_workflow.retry_count IS '已重试次数';
|
||||||
|
COMMENT ON COLUMN black_deacon_exec_workflow.last_heartbeat IS '最后心跳时间(毫秒时间戳)';
|
||||||
|
ALTER TABLE black_deacon_exec_workflow ADD COLUMN IF NOT EXISTS charge_order_id BIGINT NOT NULL DEFAULT 0; -- 关联计费单ID(shop-user-trade pricing,0=未建单)
|
||||||
|
COMMENT ON COLUMN black_deacon_exec_workflow.charge_order_id IS '关联计费单ID(shop-user-trade pricing,0=未建单)';
|
||||||
|
|
||||||
|
--------------------工作流重试兜底:flow_async_task 统一异步任务表(Task 2 使用)---------------------
|
||||||
|
CREATE TABLE IF NOT EXISTS black_deacon_flow_async_task (
|
||||||
|
-- 基础字段(完全对齐项目规范)
|
||||||
|
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),
|
||||||
|
-- 业务字段
|
||||||
|
execution_id BIGINT NOT NULL DEFAULT 0, -- 所属执行ID
|
||||||
|
node_id VARCHAR(64) NOT NULL DEFAULT '', -- 所属节点ID
|
||||||
|
segment_index INT NOT NULL DEFAULT -1, -- 段序号;非段调用为-1(哨兵,不用NULL)
|
||||||
|
model_id BIGINT NOT NULL DEFAULT 0, -- 模型ID
|
||||||
|
task_id BIGINT NOT NULL DEFAULT 0, -- model-gateway任务ID
|
||||||
|
msg_topic VARCHAR(255) NOT NULL DEFAULT '', -- 结果消息主题(恢复重订阅拿回结果)
|
||||||
|
state SMALLINT NOT NULL DEFAULT 0, -- 0=in-flight,1=done,2=failed
|
||||||
|
result JSONB DEFAULT '{}' -- 成功结果JSON(ModelCallRes)
|
||||||
|
);
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS uk_async_task_exec_node_seg ON black_deacon_flow_async_task(execution_id, node_id, segment_index);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_async_task_tenant_id ON black_deacon_flow_async_task(tenant_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_async_task_deleted_at ON black_deacon_flow_async_task(deleted_at);
|
||||||
|
|
||||||
|
--------------------崩溃恢复补全用户:exec_workflow.user_id(恢复续跑补 X-User-Info 过 model-gateway 最低余额门禁)---------------------
|
||||||
|
ALTER TABLE black_deacon_exec_workflow ADD COLUMN IF NOT EXISTS user_id BIGINT NOT NULL DEFAULT 0; -- 执行用户ID(数字;creator 仅 userName,恢复续跑须按此补全用户 Id)
|
||||||
|
COMMENT ON COLUMN black_deacon_exec_workflow.user_id IS '执行用户ID(数字,恢复续跑补 X-User-Info 用)';
|
||||||
|
|
||||||
|
--------------------终局回填业务实扣:exec_workflow.actual_amount(用户钱包实际扣除金额,元;区别于 total_fee=模型按次费用合计)---------------------
|
||||||
|
ALTER TABLE black_deacon_exec_workflow ADD COLUMN IF NOT EXISTS actual_amount NUMERIC(15,2) NOT NULL DEFAULT 0; -- 业务扣费(settle/cancel 结算实收;失败/未结算=0)
|
||||||
|
COMMENT ON COLUMN black_deacon_exec_workflow.actual_amount IS '业务扣费(用户钱包实际扣除金额,元;结算/取消回填实收,失败/未结算=0,区别于 total_fee=模型按次费用合计)';
|
||||||
|
|
||||||
|
--------------------三表键按逻辑运行(node_group_id)隔离:exec_workflow 补列回填 + async/segment 加列换键 + checkpoint 活行归组(2026-09-04,详见《工作流状态表键按逻辑运行隔离技术设计.md》)--------------------
|
||||||
|
-- 根因:flow_checkpoint 软删(execution_id 键)后同键 ON CONFLICT 重存不清 deleted_at → 续跑读不到断点从图头转圈。
|
||||||
|
-- 方案:exec_workflow 持久化 node_group_id(逻辑运行标识:续跑复用、forceNewRun 换新 uuid);
|
||||||
|
-- checkpoint_id / async / segment 唯一键一律纳入该组;同组重复只 Upsert 重置、软删即终态不复活、无物理删。
|
||||||
|
-- 幂等:全程 ADD COLUMN IF NOT EXISTS / UPDATE 仅改空值 / DROP+CREATE UNIQUE IF EXISTS。
|
||||||
|
-- 1) exec_workflow.node_group_id:仓库此前漏建该列 DDL(生产早期手工加列则此处 no-op)。
|
||||||
|
-- 历史行回填自身 id(=其遗留 checkpoint/async/segment 旧键):部署前中断/失败可续跑的 exec 无缝衔接到新组键续跑;
|
||||||
|
-- 已带真实 uuid 现役组的行(新代码产生的)不受影响。
|
||||||
|
ALTER TABLE black_deacon_exec_workflow ADD COLUMN IF NOT EXISTS node_group_id VARCHAR(64) NOT NULL DEFAULT '';
|
||||||
|
COMMENT ON COLUMN black_deacon_exec_workflow.node_group_id IS '节点组ID(逻辑运行标识:续跑复用,forceNewRun 换新 uuid)';
|
||||||
|
UPDATE black_deacon_exec_workflow SET node_group_id = id::text WHERE node_group_id IS NULL OR node_group_id = '';
|
||||||
|
|
||||||
|
-- 2) async/segment 加列并按 exec 现役组回填。NULLIF 防空组并到 execution_id::text(=旧"exec 作用域"读集,等价不丢不并)
|
||||||
|
ALTER TABLE black_deacon_flow_async_task ADD COLUMN IF NOT EXISTS node_group_id VARCHAR(64);
|
||||||
|
ALTER TABLE black_deacon_flow_segment_result ADD COLUMN IF NOT EXISTS node_group_id VARCHAR(64);
|
||||||
|
UPDATE black_deacon_flow_async_task a SET node_group_id = COALESCE(NULLIF(e.node_group_id,''), a.execution_id::text)
|
||||||
|
FROM black_deacon_exec_workflow e WHERE a.node_group_id IS NULL AND e.id = a.execution_id;
|
||||||
|
UPDATE black_deacon_flow_segment_result s SET node_group_id = COALESCE(NULLIF(e.node_group_id,''), s.execution_id::text)
|
||||||
|
FROM black_deacon_exec_workflow e WHERE s.node_group_id IS NULL AND e.id = s.execution_id;
|
||||||
|
-- exec 行缺失的孤儿兜底(理论无):按 execution_id 文本伪组,保证唯一性成立
|
||||||
|
UPDATE black_deacon_flow_async_task SET node_group_id = execution_id::text WHERE node_group_id IS NULL;
|
||||||
|
UPDATE black_deacon_flow_segment_result SET node_group_id = execution_id::text WHERE node_group_id IS NULL;
|
||||||
|
ALTER TABLE black_deacon_flow_async_task ALTER COLUMN node_group_id SET NOT NULL;
|
||||||
|
ALTER TABLE black_deacon_flow_segment_result ALTER COLUMN node_group_id SET NOT NULL;
|
||||||
|
|
||||||
|
-- 3) 换唯一键:旧 (execution_id,node_id,segment_index) → 新 (node_group_id,node_id,segment_index)
|
||||||
|
DROP INDEX IF EXISTS uk_async_task_exec_node_seg;
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS uk_async_task_group_node_seg ON black_deacon_flow_async_task(node_group_id, node_id, segment_index);
|
||||||
|
DROP INDEX IF EXISTS uk_segment_result_exec_node_idx;
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS uk_segment_result_group_node_idx ON black_deacon_flow_segment_result(node_group_id, node_id, segment_index);
|
||||||
|
|
||||||
|
-- 4) checkpoint 表零 DDL:把仍活的断点从"checkpoint_id=executionId(数值串)"归一到"checkpoint_id=exec 现役组",
|
||||||
|
-- 使新组键读到部署前中断/失败的断点。历史 exec 组=自身 id → 天然不变;仅 uuid 现役组发生重命名
|
||||||
|
-- (单 exec 至多一行活断点;组为 uuid 不与遗留数值串冲突;守卫排除空串/自同值防并桶)
|
||||||
|
UPDATE black_deacon_flow_checkpoint c SET checkpoint_id = e.node_group_id
|
||||||
|
FROM black_deacon_exec_workflow e
|
||||||
|
WHERE c.deleted_at IS NULL AND c.checkpoint_id = e.id::text
|
||||||
|
AND e.node_group_id IS NOT NULL AND e.node_group_id <> '' AND e.node_group_id <> e.id::text;
|
||||||
|
|
||||||
|
-- 5) 存量毒槽自愈:旧 forceNewRun 三清把"该 exec 仍可续跑"的 checkpoint 槽软删成了墓碑;uk_checkpoint_id 无条件唯一、
|
||||||
|
-- 墓碑仍占槽,Save 的 ON CONFLICT 只会再 upsert 进墓碑且 deleted_at 不清 → 续跑永远读不到断点 = "从头转圈"现场。
|
||||||
|
-- 软删即终态、不复活:被占槽不可再用,故给"可续跑(status 1/3)但当前槽无活断点"的 exec 重指全新 uuid 组,
|
||||||
|
-- 下次续跑写新活行、此后断点正常落库可断点续跑。代价:这批 exec 历史段/异步缓存(按旧 id 组)随之失联,
|
||||||
|
-- 属毒槽必然代价、只影响存量毒槽行;正常失败(槽上活断点在)的 exec 不动、断点续跑无缝衔接。
|
||||||
|
UPDATE black_deacon_exec_workflow e
|
||||||
|
SET node_group_id = gen_random_uuid()::text
|
||||||
|
WHERE e.status IN (1, 3)
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM black_deacon_flow_checkpoint c
|
||||||
|
WHERE c.deleted_at IS NULL AND c.checkpoint_id = e.node_group_id);
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package flow
|
||||||
|
|
||||||
|
import "github.com/gogf/gf/v2/util/gconv"
|
||||||
|
|
||||||
|
var (
|
||||||
|
FlowExecutionStatusRunning = newFlowExecutionStatus(gconv.PtrInt8(1), "running") // 运行中
|
||||||
|
FlowExecutionStatusSuccess = newFlowExecutionStatus(gconv.PtrInt8(2), "success") // 成功
|
||||||
|
FlowExecutionStatusFailed = newFlowExecutionStatus(gconv.PtrInt8(3), "failed") // 失败
|
||||||
|
FlowExecutionStatusCancel = newFlowExecutionStatus(gconv.PtrInt8(4), "cancel") // 取消
|
||||||
|
)
|
||||||
|
|
||||||
|
type FlowExecutionStatus *int8
|
||||||
|
|
||||||
|
type flowExecutionStatus struct {
|
||||||
|
code FlowExecutionStatus
|
||||||
|
desc string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s flowExecutionStatus) Code() FlowExecutionStatus {
|
||||||
|
return s.code
|
||||||
|
}
|
||||||
|
func (s flowExecutionStatus) Desc() string {
|
||||||
|
return s.desc
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFlowExecutionStatus(code FlowExecutionStatus, desc string) flowExecutionStatus {
|
||||||
|
return flowExecutionStatus{code: code, desc: desc}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package flow
|
||||||
|
|
||||||
|
import "github.com/gogf/gf/v2/util/gconv"
|
||||||
|
|
||||||
|
type FlowExecutionTriggerType *int8
|
||||||
|
|
||||||
|
var (
|
||||||
|
FlowExecutionTriggerTypeManual = newFlowExecutionTriggerType(gconv.PtrInt8(1), "manual")
|
||||||
|
FlowExecutionTriggerTypeAuto = newFlowExecutionTriggerType(gconv.PtrInt8(2), "auto")
|
||||||
|
FlowExecutionTriggerTypeTime = newFlowExecutionTriggerType(gconv.PtrInt8(3), "time")
|
||||||
|
)
|
||||||
|
|
||||||
|
type flowExecutionTriggerType struct {
|
||||||
|
code FlowExecutionTriggerType
|
||||||
|
desc string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s flowExecutionTriggerType) Code() FlowExecutionTriggerType {
|
||||||
|
return s.code
|
||||||
|
}
|
||||||
|
func (s flowExecutionTriggerType) Desc() string {
|
||||||
|
return s.desc
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFlowExecutionTriggerType(code FlowExecutionTriggerType, desc string) flowExecutionTriggerType {
|
||||||
|
return flowExecutionTriggerType{code: code, desc: desc}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package flow
|
||||||
|
|
||||||
|
import "github.com/gogf/gf/v2/util/gconv"
|
||||||
|
|
||||||
|
var (
|
||||||
|
FlowTemplateStatusDisable = newFlowTemplateStatus(gconv.PtrInt8(0), "disable")
|
||||||
|
FlowTemplateStatusEnable = newFlowTemplateStatus(gconv.PtrInt8(1), "enable")
|
||||||
|
)
|
||||||
|
|
||||||
|
type FlowTemplateStatus *int8
|
||||||
|
|
||||||
|
type flowTemplateStatus struct {
|
||||||
|
code FlowTemplateStatus
|
||||||
|
desc string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s flowTemplateStatus) Code() FlowTemplateStatus {
|
||||||
|
return s.code
|
||||||
|
}
|
||||||
|
func (s flowTemplateStatus) Desc() string {
|
||||||
|
return s.desc
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFlowTemplateStatus(code FlowTemplateStatus, desc string) flowTemplateStatus {
|
||||||
|
return flowTemplateStatus{code: code, desc: desc}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package flow
|
||||||
|
|
||||||
|
import "github.com/gogf/gf/v2/util/gconv"
|
||||||
|
|
||||||
|
var (
|
||||||
|
FlowUserAccessLevelPrivate = newFlowUserAccessLevel(gconv.PtrInt8(1), "private")
|
||||||
|
FlowUserAccessLevelTeam = newFlowUserAccessLevel(gconv.PtrInt8(2), "team")
|
||||||
|
FlowUserAccessLevelPublic = newFlowUserAccessLevel(gconv.PtrInt8(3), "public")
|
||||||
|
)
|
||||||
|
|
||||||
|
type FlowUserAccessLevel *int8
|
||||||
|
|
||||||
|
type flowUserAccessLevel struct {
|
||||||
|
code FlowUserAccessLevel
|
||||||
|
desc string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s flowUserAccessLevel) Code() FlowUserAccessLevel {
|
||||||
|
return s.code
|
||||||
|
}
|
||||||
|
func (s flowUserAccessLevel) Desc() string {
|
||||||
|
return s.desc
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFlowUserAccessLevel(code FlowUserAccessLevel, desc string) flowUserAccessLevel {
|
||||||
|
return flowUserAccessLevel{code: code, desc: desc}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
// 模型类型编码常量
|
||||||
|
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
|
||||||
@@ -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}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package node
|
||||||
|
|
||||||
|
import "github.com/gogf/gf/v2/util/gconv"
|
||||||
|
|
||||||
|
var (
|
||||||
|
NodeExecutionStatusRunning = newNodeExecutionStatus(gconv.PtrInt8(1), "running") // 运行中
|
||||||
|
NodeExecutionStatusSuccess = newNodeExecutionStatus(gconv.PtrInt8(2), "success") // 成功
|
||||||
|
NodeExecutionStatusFailed = newNodeExecutionStatus(gconv.PtrInt8(3), "failed") // 失败
|
||||||
|
NodeExecutionStatusPaused = newNodeExecutionStatus(gconv.PtrInt8(4), "paused") // 暂停
|
||||||
|
NodeExecutionStatusWait = newNodeExecutionStatus(gconv.PtrInt8(5), "wait") // 等待执行
|
||||||
|
)
|
||||||
|
|
||||||
|
type NodeExecutionStatus *int8
|
||||||
|
|
||||||
|
type nodeExecutionStatus struct {
|
||||||
|
code NodeExecutionStatus
|
||||||
|
desc string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s nodeExecutionStatus) Code() NodeExecutionStatus {
|
||||||
|
return s.code
|
||||||
|
}
|
||||||
|
func (s nodeExecutionStatus) Desc() string {
|
||||||
|
return s.desc
|
||||||
|
}
|
||||||
|
|
||||||
|
func newNodeExecutionStatus(code NodeExecutionStatus, desc string) nodeExecutionStatus {
|
||||||
|
return nodeExecutionStatus{code: code, desc: desc}
|
||||||
|
}
|
||||||
@@ -0,0 +1,493 @@
|
|||||||
|
package node
|
||||||
|
|
||||||
|
// NodeGroup 节点分组标识
|
||||||
|
type NodeGroup string
|
||||||
|
|
||||||
|
// NodeType 节点类型标识
|
||||||
|
type NodeType string
|
||||||
|
|
||||||
|
// 分组常量定义
|
||||||
|
const (
|
||||||
|
NodeGroupBase NodeGroup = "base"
|
||||||
|
NodeGroupSystem NodeGroup = "system"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 节点类型常量定义
|
||||||
|
const (
|
||||||
|
NodeTypeStart NodeType = "__start__"
|
||||||
|
NodeTypeModel NodeType = "model"
|
||||||
|
NodeTypeDataMerge NodeType = "data_merge"
|
||||||
|
NodeTypeForm NodeType = "form"
|
||||||
|
NodeTypeSubFlow NodeType = "sub_flow"
|
||||||
|
NodeTypeHttp NodeType = "http"
|
||||||
|
NodeTypeSystemSum NodeType = "system_sum"
|
||||||
|
NodeTypeScriptTranscribe NodeType = "script_transcribe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NodeGroupMeta 节点分组元数据
|
||||||
|
type NodeGroupMeta struct {
|
||||||
|
Key NodeGroup `json:"key"` // 后端存储标识
|
||||||
|
Name string `json:"name"` // 前端展示中文名称
|
||||||
|
}
|
||||||
|
|
||||||
|
// NodeTypeMeta 节点元数据
|
||||||
|
type NodeTypeMeta struct {
|
||||||
|
Key NodeType `json:"key"` // 节点类型标识
|
||||||
|
Name string `json:"name"` // 节点中文名称
|
||||||
|
Group NodeGroup `json:"group"` // 所属分组
|
||||||
|
Sort int `json:"sort"` // UI排序字段
|
||||||
|
Desc string `json:"desc,omitempty"` // 可选:节点简介
|
||||||
|
PatchLayout bool `json:"patchLayout"`
|
||||||
|
IsMultiParameter bool `json:"isMultiParameter"`
|
||||||
|
BatchExecOption bool `json:"batchExecOption"`
|
||||||
|
PreToolOption []NodePresetField `json:"preToolOption"`
|
||||||
|
PostToolOption []NodePresetField `json:"postToolOption"`
|
||||||
|
IsSaveFileOption bool `json:"isSaveFileOption"`
|
||||||
|
FormConfigOption bool `json:"formConfigOption"`
|
||||||
|
ModelConfigOption bool `json:"modelConfigOption"`
|
||||||
|
SkillOption bool `json:"skillOption"`
|
||||||
|
PromptOption bool `json:"promptOption"`
|
||||||
|
NegativePromptOption bool `json:"negativePromptOption"`
|
||||||
|
PresetOption []NodePresetField `json:"presetOption"`
|
||||||
|
OutputField []NodeOutputField `json:"outputField"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NodeOutputField struct {
|
||||||
|
Field string `json:"field"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NodePresetField struct {
|
||||||
|
Value string `json:"value"`
|
||||||
|
ValueSource []ValueSource `json:"valueSource"`
|
||||||
|
Field string `json:"field"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
IsFormField bool `json:"isFormField"`
|
||||||
|
Constraint FieldConstraint `json:"constraint"`
|
||||||
|
Required bool `json:"required"`
|
||||||
|
Options []SelectOption `json:"options"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ValueSource struct {
|
||||||
|
NodeId string `json:"nodeId"`
|
||||||
|
Field string `json:"field"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FieldConstraint 字段约束
|
||||||
|
type FieldConstraint struct {
|
||||||
|
// 数字类型:int、float、double、string
|
||||||
|
Type string `json:"type" dc:"类型"`
|
||||||
|
Min any `json:"min" dc:"最小值"`
|
||||||
|
Max any `json:"max" dc:"最大值"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SelectOption struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
Config []NodePresetField `json:"config"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NodeGroupTree 对外输出树形结构:分组 + 分组下所有节点
|
||||||
|
type NodeGroupTree struct {
|
||||||
|
Group NodeGroupMeta `json:"group"`
|
||||||
|
Nodes []NodeTypeMeta `json:"nodes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== 元数据集中注册区(英文标识+中文文案统一维护) =====================
|
||||||
|
var NodeGroupMetaList = []NodeGroupMeta{
|
||||||
|
{
|
||||||
|
Key: NodeGroupBase,
|
||||||
|
Name: "基础",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var NodeTypeMetaList = []NodeTypeMeta{
|
||||||
|
{
|
||||||
|
Key: NodeTypeModel,
|
||||||
|
Name: "模型",
|
||||||
|
Group: NodeGroupBase,
|
||||||
|
Sort: 1,
|
||||||
|
Desc: "模型调用节点,可配置模型参数、模型配置、技能、提示语、结果汇集、结果保存、结果返回、结果展示等信息。",
|
||||||
|
PatchLayout: true,
|
||||||
|
IsMultiParameter: true,
|
||||||
|
BatchExecOption: true,
|
||||||
|
PreToolOption: []NodePresetField{
|
||||||
|
{
|
||||||
|
Field: "perTool",
|
||||||
|
Label: "前置方法",
|
||||||
|
Type: "select",
|
||||||
|
Required: false,
|
||||||
|
Options: []SelectOption{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PostToolOption: []NodePresetField{
|
||||||
|
{
|
||||||
|
Field: "postTool",
|
||||||
|
Label: "后置方法",
|
||||||
|
Type: "select",
|
||||||
|
Required: false,
|
||||||
|
Options: []SelectOption{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
IsSaveFileOption: true,
|
||||||
|
FormConfigOption: false,
|
||||||
|
ModelConfigOption: true,
|
||||||
|
SkillOption: false,
|
||||||
|
PromptOption: true,
|
||||||
|
NegativePromptOption: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: NodeTypeDataMerge,
|
||||||
|
Name: "结果汇集",
|
||||||
|
Group: NodeGroupBase,
|
||||||
|
Sort: 2,
|
||||||
|
Desc: "结果汇集节点,可配置结果汇集方式、结果保存、结果返回、结果展示等信息。",
|
||||||
|
IsMultiParameter: false,
|
||||||
|
BatchExecOption: false,
|
||||||
|
IsSaveFileOption: false,
|
||||||
|
FormConfigOption: false,
|
||||||
|
ModelConfigOption: false,
|
||||||
|
SkillOption: false,
|
||||||
|
PromptOption: false,
|
||||||
|
NegativePromptOption: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: NodeTypeForm,
|
||||||
|
Name: "表单",
|
||||||
|
Group: NodeGroupBase,
|
||||||
|
Sort: 3,
|
||||||
|
Desc: "表单节点,可配置表单字段、表单配置、结果汇集、结果保存、结果返回、结果展示等信息。",
|
||||||
|
IsMultiParameter: false,
|
||||||
|
BatchExecOption: false,
|
||||||
|
IsSaveFileOption: false,
|
||||||
|
FormConfigOption: true,
|
||||||
|
ModelConfigOption: false,
|
||||||
|
SkillOption: false,
|
||||||
|
PromptOption: false,
|
||||||
|
NegativePromptOption: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: NodeTypeSubFlow,
|
||||||
|
Name: "子流程",
|
||||||
|
Group: NodeGroupBase,
|
||||||
|
Sort: 4,
|
||||||
|
PresetOption: []NodePresetField{
|
||||||
|
{
|
||||||
|
Field: "maxConcurrency",
|
||||||
|
Label: "生成次数",
|
||||||
|
Type: "input",
|
||||||
|
Value: "1",
|
||||||
|
IsFormField: true,
|
||||||
|
Constraint: FieldConstraint{
|
||||||
|
Type: "int",
|
||||||
|
Min: 1,
|
||||||
|
Max: 10,
|
||||||
|
},
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: NodeTypeHttp,
|
||||||
|
Name: "HTTP(S)接口",
|
||||||
|
Group: NodeGroupBase,
|
||||||
|
Sort: 5,
|
||||||
|
Desc: "HTTP(S)接口节点,可配置HTTP(S)接口地址、请求方式、请求头、请求体、结果返回结构、结果返回方式、结果返回结构、结果返回方式等信息。",
|
||||||
|
BatchExecOption: false,
|
||||||
|
IsSaveFileOption: true,
|
||||||
|
FormConfigOption: false,
|
||||||
|
ModelConfigOption: false,
|
||||||
|
SkillOption: false,
|
||||||
|
PromptOption: false,
|
||||||
|
NegativePromptOption: false,
|
||||||
|
PresetOption: []NodePresetField{
|
||||||
|
{
|
||||||
|
Field: "method",
|
||||||
|
Label: "请求方式",
|
||||||
|
Type: "select",
|
||||||
|
Required: true,
|
||||||
|
Options: []SelectOption{
|
||||||
|
{Key: "GET", Value: "GET"},
|
||||||
|
{Key: "POST", Value: "POST"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Field: "url",
|
||||||
|
Label: "请求地址",
|
||||||
|
Type: "input",
|
||||||
|
Constraint: FieldConstraint{
|
||||||
|
Type: "string",
|
||||||
|
Min: 1,
|
||||||
|
Max: -1,
|
||||||
|
},
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Field: "headers",
|
||||||
|
Label: "请求头(支持Authorization鉴权)",
|
||||||
|
Type: "keyValue",
|
||||||
|
Required: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Field: "bodyType",
|
||||||
|
Label: "请求体类型",
|
||||||
|
Type: "select",
|
||||||
|
Required: true,
|
||||||
|
Options: []SelectOption{
|
||||||
|
{Key: "None", Value: "无"},
|
||||||
|
{Key: "JSON", Value: "JSON"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Field: "body",
|
||||||
|
Label: "请求体内容",
|
||||||
|
Type: "schemaJson",
|
||||||
|
Required: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Field: "response",
|
||||||
|
Label: "结果返回结构",
|
||||||
|
Type: "schemaJson",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Field: "responseType",
|
||||||
|
Label: "结果返回方式",
|
||||||
|
Type: "select",
|
||||||
|
Required: true,
|
||||||
|
Options: []SelectOption{
|
||||||
|
{Key: "sync", Value: "同步返回"},
|
||||||
|
{Key: "callback", Value: "等候回调", Config: []NodePresetField{
|
||||||
|
{
|
||||||
|
Field: "callbackUrl",
|
||||||
|
Label: "回调地址(只需要填写字段名称)",
|
||||||
|
Type: "input",
|
||||||
|
Constraint: FieldConstraint{
|
||||||
|
Type: "string",
|
||||||
|
Min: 1,
|
||||||
|
Max: -1,
|
||||||
|
},
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
{Key: "pull", Value: "主动拉取", Config: []NodePresetField{
|
||||||
|
{
|
||||||
|
Field: "method",
|
||||||
|
Label: "请求方式",
|
||||||
|
Type: "select",
|
||||||
|
Required: true,
|
||||||
|
Options: []SelectOption{
|
||||||
|
{Key: "GET", Value: "GET"},
|
||||||
|
{Key: "POST", Value: "POST"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Field: "url",
|
||||||
|
Label: "请求地址",
|
||||||
|
Type: "input",
|
||||||
|
Constraint: FieldConstraint{
|
||||||
|
Type: "string",
|
||||||
|
Min: 1,
|
||||||
|
Max: -1,
|
||||||
|
},
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Field: "headers",
|
||||||
|
Label: "请求头(支持Authorization鉴权)",
|
||||||
|
Type: "keyValue",
|
||||||
|
Required: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Field: "bodyType",
|
||||||
|
Label: "请求体类型",
|
||||||
|
Type: "select",
|
||||||
|
Required: true,
|
||||||
|
Options: []SelectOption{
|
||||||
|
{Key: "None", Value: "无"},
|
||||||
|
{Key: "JSON", Value: "JSON"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Field: "body",
|
||||||
|
Label: "请求体内容",
|
||||||
|
Type: "schemaJson",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Field: "response",
|
||||||
|
Label: "结果返回结构",
|
||||||
|
Type: "schemaJson",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: NodeTypeScriptTranscribe,
|
||||||
|
Name: "脚本转写",
|
||||||
|
Group: NodeGroupBase,
|
||||||
|
Sort: 6,
|
||||||
|
Desc: "脚本转写节点,把文案/视频分析结果通过大模型转写为结构化分镜脚本([]Shot),作为视频生成节点的 shots 输入。",
|
||||||
|
IsMultiParameter: false,
|
||||||
|
BatchExecOption: false,
|
||||||
|
IsSaveFileOption: false,
|
||||||
|
FormConfigOption: false,
|
||||||
|
ModelConfigOption: true,
|
||||||
|
SkillOption: false,
|
||||||
|
PromptOption: false,
|
||||||
|
NegativePromptOption: false,
|
||||||
|
PresetOption: []NodePresetField{
|
||||||
|
{
|
||||||
|
Field: "totalDuration",
|
||||||
|
Label: "视频总时长",
|
||||||
|
Type: "input",
|
||||||
|
Constraint: FieldConstraint{
|
||||||
|
Type: "int",
|
||||||
|
Min: 1,
|
||||||
|
Max: 300,
|
||||||
|
},
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Field: "modelId",
|
||||||
|
Label: "视频模型",
|
||||||
|
Type: "select",
|
||||||
|
Required: true,
|
||||||
|
Options: []SelectOption{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
OutputField: []NodeOutputField{
|
||||||
|
{
|
||||||
|
Field: "prompt",
|
||||||
|
Label: "转写内容",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Field: "duration",
|
||||||
|
Label: "时长",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Field: "seed",
|
||||||
|
Label: "种子",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Field: "reference_urls",
|
||||||
|
Label: "参考链接",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: NodeTypeSystemSum,
|
||||||
|
Name: "工作流内置节点-保存结果",
|
||||||
|
Group: NodeGroupSystem,
|
||||||
|
Sort: 7,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== 缓存map & 工具函数 =====================
|
||||||
|
var (
|
||||||
|
nodeTypeMetaMap map[NodeType]NodeTypeMeta
|
||||||
|
nodeGroupMap map[NodeGroup]NodeGroupMeta
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// 初始化分组缓存
|
||||||
|
nodeGroupMap = make(map[NodeGroup]NodeGroupMeta, len(NodeGroupMetaList))
|
||||||
|
for _, meta := range NodeGroupMetaList {
|
||||||
|
nodeGroupMap[meta.Key] = meta
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化节点缓存
|
||||||
|
nodeTypeMetaMap = make(map[NodeType]NodeTypeMeta, len(NodeTypeMetaList))
|
||||||
|
for _, meta := range NodeTypeMetaList {
|
||||||
|
nodeTypeMetaMap[meta.Key] = meta
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNodeTypeMeta 根据节点类型获取元信息
|
||||||
|
func GetNodeTypeMeta(typ NodeType) (NodeTypeMeta, bool) {
|
||||||
|
meta, ok := nodeTypeMetaMap[typ]
|
||||||
|
return meta, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNodeTypeName 快速获取节点中文名称,不存在返回原始key
|
||||||
|
func GetNodeTypeName(typ NodeType) string {
|
||||||
|
meta, ok := nodeTypeMetaMap[typ]
|
||||||
|
if !ok {
|
||||||
|
return string(typ)
|
||||||
|
}
|
||||||
|
return meta.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNodeGroupMeta 根据分组标识获取分组元信息
|
||||||
|
func GetNodeGroupMeta(g NodeGroup) (NodeGroupMeta, bool) {
|
||||||
|
meta, ok := nodeGroupMap[g]
|
||||||
|
return meta, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNodeGroupName 快速获取分组中文名称
|
||||||
|
func GetNodeGroupName(g NodeGroup) string {
|
||||||
|
meta, ok := nodeGroupMap[g]
|
||||||
|
if !ok {
|
||||||
|
return string(g)
|
||||||
|
}
|
||||||
|
return meta.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllNodeTypeMeta 返回全部节点元数据副本
|
||||||
|
func GetAllNodeTypeMeta() []NodeTypeMeta {
|
||||||
|
list := make([]NodeTypeMeta, len(NodeTypeMetaList))
|
||||||
|
copy(list, NodeTypeMetaList)
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNodeGroupTree 获取【分组+节点】树形列表,完整所有节点
|
||||||
|
func GetNodeGroupTree() []NodeGroupTree {
|
||||||
|
return getFilteredNodeTree(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFilterNodeTree 根据指定分组过滤节点树形结构
|
||||||
|
// groups: 需要展示的分组key列表,传入nil返回全部
|
||||||
|
func GetFilterNodeTree(groups []NodeGroup) []NodeGroupTree {
|
||||||
|
return getFilteredNodeTree(groups)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getFilteredNodeTree 内部通用树形构建逻辑
|
||||||
|
func getFilteredNodeTree(filterGroups []NodeGroup) []NodeGroupTree {
|
||||||
|
// 构建分组过滤集合
|
||||||
|
filterSet := make(map[NodeGroup]bool)
|
||||||
|
for _, g := range filterGroups {
|
||||||
|
filterSet[g] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
groupNodeMap := make(map[NodeGroup][]NodeTypeMeta)
|
||||||
|
for _, node := range NodeTypeMetaList {
|
||||||
|
// 有过滤条件 && 当前分组不在过滤列表,则跳过
|
||||||
|
if len(filterSet) > 0 && !filterSet[node.Group] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
groupNodeMap[node.Group] = append(groupNodeMap[node.Group], node)
|
||||||
|
}
|
||||||
|
|
||||||
|
var tree []NodeGroupTree
|
||||||
|
// 按定义顺序组装分组
|
||||||
|
for _, gMeta := range NodeGroupMetaList {
|
||||||
|
// 如果开启过滤,跳过不在筛选列表中的分组
|
||||||
|
if len(filterSet) > 0 && !filterSet[gMeta.Key] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
nodes := groupNodeMap[gMeta.Key]
|
||||||
|
if len(nodes) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tree = append(tree, NodeGroupTree{
|
||||||
|
Group: gMeta,
|
||||||
|
Nodes: nodes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return tree
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package node
|
||||||
|
|
||||||
|
import "github.com/gogf/gf/v2/util/gconv"
|
||||||
|
|
||||||
|
var (
|
||||||
|
SourceTypeSystem = newSourceType(gconv.PtrInt8(1), "系统初始化")
|
||||||
|
SourceTypeUser = newSourceType(gconv.PtrInt8(2), "用户自定义")
|
||||||
|
)
|
||||||
|
|
||||||
|
type SourceType *int8
|
||||||
|
|
||||||
|
type sourceType struct {
|
||||||
|
code SourceType
|
||||||
|
desc string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s sourceType) Code() SourceType {
|
||||||
|
return s.code
|
||||||
|
}
|
||||||
|
func (s sourceType) Desc() string {
|
||||||
|
return s.desc
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSourceType(code SourceType, desc string) sourceType {
|
||||||
|
return sourceType{code: code, desc: desc}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package public
|
||||||
|
|
||||||
|
const GmqMsgPluginsName = "gmq_model_msg"
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package public
|
||||||
|
|
||||||
|
// 数据库名称
|
||||||
|
const (
|
||||||
|
DbNameBlackDeacon = "black_deacon"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 数据库表名
|
||||||
|
const (
|
||||||
|
TableNameCreationInfo = "creation_info"
|
||||||
|
TableNameFlowExecution = "flow_execution"
|
||||||
|
TableNameFlowTemplate = "flow_template"
|
||||||
|
TableNameFlowUser = "flow_user"
|
||||||
|
TableNameSkillTemplate = "skill_template"
|
||||||
|
TableNameSkillUser = "skill_user"
|
||||||
|
TableNameFileTemp = "file_temp"
|
||||||
|
TableNameActivePull = "active_pull"
|
||||||
|
TableNameFlowCheckpoint = "flow_checkpoint"
|
||||||
|
TableNameNodePrompt = "node_prompt"
|
||||||
|
TableNameNodeExecution = "node_execution"
|
||||||
|
TableNameSession = "session"
|
||||||
|
TableNameExecChat = "exec_chat"
|
||||||
|
TableNameExecWorkflow = "exec_workflow"
|
||||||
|
TableNameExecWorkflowResult = "exec_workflow_result"
|
||||||
|
TableNameFlowSegmentResult = "flow_segment_result"
|
||||||
|
TableNameFlowAsyncTask = "flow_async_task"
|
||||||
|
)
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ai-agent/workflow/model/dto"
|
||||||
|
"ai-agent/workflow/service"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/beans"
|
||||||
|
)
|
||||||
|
|
||||||
|
type creationInfo struct{}
|
||||||
|
|
||||||
|
var CreationInfo = new(creationInfo)
|
||||||
|
|
||||||
|
func (c *creationInfo) GraphInvoke(ctx context.Context, req *dto.CreationInput) (res *beans.ResponseEmpty, err error) {
|
||||||
|
err = service.CreationInfoService.Creation(ctx, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *creationInfo) List(ctx context.Context, req *dto.ListCreationInfoReq) (res *dto.ListCreationInfoRes, err error) {
|
||||||
|
return service.CreationInfoService.List(ctx, req)
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package flow
|
||||||
|
|
||||||
|
import (
|
||||||
|
flowService "ai-agent/workflow/service/flow"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
"github.com/gogf/gf/v2/net/ghttp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type flowCallBack struct{}
|
||||||
|
|
||||||
|
var FlowCallBack = new(flowCallBack)
|
||||||
|
|
||||||
|
func (c *flowCallBack) HttpNodeCallback(r *ghttp.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
err := flowService.FlowExecutionService.HttpNodeCallback(ctx)
|
||||||
|
if err != nil {
|
||||||
|
r.Response.WriteJson(g.Map{"code": 500, "message": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.Response.WriteJson(g.Map{"code": 0, "message": "success"})
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package flow
|
||||||
|
|
||||||
|
import (
|
||||||
|
flowDto "ai-agent/workflow/model/dto/flow"
|
||||||
|
flowService "ai-agent/workflow/service/flow"
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type flowExecution struct{}
|
||||||
|
|
||||||
|
var FlowExecution = new(flowExecution)
|
||||||
|
|
||||||
|
//func (c *flowExecution) Execute(ctx context.Context, req *flowDto.ExecuteReq) (res *flowDto.ExecuteRes, err error) {
|
||||||
|
// return flowService.FlowExecutionService.Execute(ctx, req)
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//func (c *flowExecution) ReExecute(ctx context.Context, req *flowDto.ReExecuteReq) (res *flowDto.ExecuteRes, err error) {
|
||||||
|
// return flowService.FlowExecutionService.ReExecute(ctx, req)
|
||||||
|
//}
|
||||||
|
|
||||||
|
func (c *flowExecution) Get(ctx context.Context, req *flowDto.GetFlowExecutionReq) (res *flowDto.VOFlowExecution, err error) {
|
||||||
|
return flowService.FlowExecutionService.Get(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *flowExecution) List(ctx context.Context, req *flowDto.ListFlowExecutionReq) (res *flowDto.ListFlowExecutionTreeRes, err error) {
|
||||||
|
return flowService.FlowExecutionService.List(ctx, req)
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package flow
|
||||||
|
|
||||||
|
import (
|
||||||
|
flowDto "ai-agent/workflow/model/dto/flow"
|
||||||
|
flowService "ai-agent/workflow/service/flow"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/beans"
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
)
|
||||||
|
|
||||||
|
type flowTemplate struct{}
|
||||||
|
|
||||||
|
var FlowTemplate = new(flowTemplate)
|
||||||
|
|
||||||
|
func (c *flowTemplate) Create(ctx context.Context, req *flowDto.CreateFlowTemplateReq) (res *flowDto.CreateFlowTemplateRes, err error) {
|
||||||
|
res, err = flowService.FlowTemplateService.Create(ctx, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *flowTemplate) Update(ctx context.Context, req *flowDto.UpdateFlowTemplateReq) (res *beans.ResponseEmpty, err error) {
|
||||||
|
err = flowService.FlowTemplateService.Update(ctx, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *flowTemplate) Delete(ctx context.Context, req *flowDto.DeleteFlowTemplateReq) (res *beans.ResponseEmpty, err error) {
|
||||||
|
err = flowService.FlowTemplateService.Delete(ctx, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *flowTemplate) Get(ctx context.Context, req *flowDto.GetFlowTemplateReq) (res *flowDto.FlowTemplateVO, err error) {
|
||||||
|
return flowService.FlowTemplateService.Get(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *flowTemplate) List(ctx context.Context, req *flowDto.ListFlowTemplateReq) (res *flowDto.ListFlowTemplateRes, err error) {
|
||||||
|
if !g.IsEmpty(req.Page) {
|
||||||
|
req.Page = &beans.Page{PageNum: 1, PageSize: 20}
|
||||||
|
}
|
||||||
|
res, err = flowService.FlowTemplateService.List(ctx, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package flow
|
||||||
|
|
||||||
|
import (
|
||||||
|
flowDto "ai-agent/workflow/model/dto/flow"
|
||||||
|
flowService "ai-agent/workflow/service/flow"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/beans"
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
)
|
||||||
|
|
||||||
|
type flowUser struct{}
|
||||||
|
|
||||||
|
var FlowUser = new(flowUser)
|
||||||
|
|
||||||
|
func (c *flowUser) Create(ctx context.Context, req *flowDto.CreateFlowUserReq) (res *flowDto.CreateFlowUserRes, err error) {
|
||||||
|
res, err = flowService.FlowUserService.Create(ctx, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *flowUser) Update(ctx context.Context, req *flowDto.UpdateFlowUserReq) (res *flowDto.CreateFlowUserRes, err error) {
|
||||||
|
return flowService.FlowUserService.Update(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *flowUser) Delete(ctx context.Context, req *flowDto.DeleteFlowUserReq) (res *beans.ResponseEmpty, err error) {
|
||||||
|
err = flowService.FlowUserService.Delete(ctx, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *flowUser) Get(ctx context.Context, req *flowDto.GetFlowUserReq) (res *flowDto.FlowUserVO, err error) {
|
||||||
|
return flowService.FlowUserService.Get(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *flowUser) List(ctx context.Context, req *flowDto.ListFlowUserReq) (res *flowDto.ListFlowRes, err error) {
|
||||||
|
if !g.IsEmpty(req.Page) {
|
||||||
|
req.Page = &beans.Page{PageNum: 1, PageSize: 20}
|
||||||
|
}
|
||||||
|
res, err = flowService.FlowUserService.List(ctx, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package node
|
||||||
|
|
||||||
|
import (
|
||||||
|
nodeDto "ai-agent/workflow/model/dto/node"
|
||||||
|
nodeService "ai-agent/workflow/service/node"
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type nodeLibrary struct{}
|
||||||
|
|
||||||
|
var NodeLibrary = new(nodeLibrary)
|
||||||
|
|
||||||
|
func (c *nodeLibrary) List(ctx context.Context, req *nodeDto.WorkflowNodeTreeReq) (res *nodeDto.WorkflowNodeTreeRes, err error) {
|
||||||
|
return nodeService.NodeLibraryService.GetNodeLibrary(ctx, req)
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package node
|
||||||
|
|
||||||
|
import (
|
||||||
|
nodeDto "ai-agent/workflow/model/dto/node"
|
||||||
|
nodeService "ai-agent/workflow/service/node"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/beans"
|
||||||
|
)
|
||||||
|
|
||||||
|
type nodePrompt struct{}
|
||||||
|
|
||||||
|
var NodePrompt = new(nodePrompt)
|
||||||
|
|
||||||
|
// Create 创建节点提示词
|
||||||
|
func (c *nodePrompt) Create(ctx context.Context, req *nodeDto.CreateNodePromptReq) (res *nodeDto.CreateNodePromptRes, err error) {
|
||||||
|
return nodeService.NodePromptService.Create(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update 更新节点提示词
|
||||||
|
func (c *nodePrompt) Update(ctx context.Context, req *nodeDto.UpdateNodePromptReq) (res *beans.ResponseEmpty, err error) {
|
||||||
|
err = nodeService.NodePromptService.Update(ctx, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete 删除节点提示词
|
||||||
|
func (c *nodePrompt) Delete(ctx context.Context, req *nodeDto.DeleteNodePromptReq) (res *beans.ResponseEmpty, err error) {
|
||||||
|
err = nodeService.NodePromptService.Delete(ctx, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get 根据ID查询节点提示词详情
|
||||||
|
func (c *nodePrompt) Get(ctx context.Context, req *nodeDto.GetNodePromptReq) (res *nodeDto.NodePromptResp, err error) {
|
||||||
|
return nodeService.NodePromptService.GetById(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListMy 查询当前用户自己创建的节点提示词列表
|
||||||
|
func (c *nodePrompt) ListMy(ctx context.Context, req *nodeDto.ListMyNodePromptReq) (res *nodeDto.ListNodePromptResp, err error) {
|
||||||
|
return nodeService.NodePromptService.ListMy(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List 查询节点提示词列表,包含系统和当前创建人自定义
|
||||||
|
func (c *nodePrompt) List(ctx context.Context, req *nodeDto.ListNodePromptReq) (res *nodeDto.ListNodePromptResp, err error) {
|
||||||
|
return nodeService.NodePromptService.ListWithSystem(ctx, req)
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package session
|
||||||
|
|
||||||
|
import (
|
||||||
|
flowDto "ai-agent/workflow/model/dto/flow"
|
||||||
|
sessionDto "ai-agent/workflow/model/dto/session"
|
||||||
|
"ai-agent/workflow/service/flow"
|
||||||
|
sessionService "ai-agent/workflow/service/session"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/beans"
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
"github.com/gogf/gf/v2/os/glog"
|
||||||
|
)
|
||||||
|
|
||||||
|
type session struct{}
|
||||||
|
|
||||||
|
var Session = new(session)
|
||||||
|
|
||||||
|
func (c *session) WsExecute(ctx context.Context, req *sessionDto.WebSocketConnectReq) (res *beans.ResponseEmpty, err error) {
|
||||||
|
r := g.RequestFromCtx(ctx)
|
||||||
|
// 首次连接仅升级 WebSocket,不区分普通对话/工作流;由后续消息 type 路由到对应处理器
|
||||||
|
err = flow.WsConnect(ctx, r, req)
|
||||||
|
if err != nil {
|
||||||
|
glog.Errorf(ctx, "ws connect failed: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// 标记响应缓冲区,使 MiddlewareHandlerResponse 跳过
|
||||||
|
r.Response.Writeln("")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *session) Get(ctx context.Context, req *sessionDto.GetSessionInfoReq) (res *sessionDto.GetSessionInfoRes, err error) {
|
||||||
|
return sessionService.SessionService.Get(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *session) List(ctx context.Context, req *sessionDto.ListSessionReq) (res *sessionDto.ListSessionRes, err error) {
|
||||||
|
return sessionService.SessionService.List(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *session) Delete(ctx context.Context, req *sessionDto.DeleteSessionReq) (res *beans.ResponseEmpty, err error) {
|
||||||
|
if err = sessionService.SessionService.Delete(ctx, req); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return &beans.ResponseEmpty{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *session) DeleteRecord(ctx context.Context, req *sessionDto.DeleteSessionRecordReq) (res *beans.ResponseEmpty, err error) {
|
||||||
|
if err = sessionService.SessionService.DeleteRecord(ctx, req); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return &beans.ResponseEmpty{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *session) ResultList(ctx context.Context, req *sessionDto.ListWorkflowResultReq) (res *flowDto.ListFlowExecutionTreeRes, err error) {
|
||||||
|
return sessionService.SessionService.ResultList(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *session) ResultDelete(ctx context.Context, req *sessionDto.DeleteWorkflowResultReq) (res *beans.ResponseEmpty, err error) {
|
||||||
|
err = sessionService.SessionService.ResultDelete(ctx, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package skill
|
||||||
|
|
||||||
|
import (
|
||||||
|
skillDto "ai-agent/workflow/model/dto/skill"
|
||||||
|
skillService "ai-agent/workflow/service/skill"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/beans"
|
||||||
|
)
|
||||||
|
|
||||||
|
type skillTemplate struct{}
|
||||||
|
|
||||||
|
var SkillTemplate = new(skillTemplate)
|
||||||
|
|
||||||
|
func (c *skillTemplate) Create(ctx context.Context, req *skillDto.CreateSkillTemplateReq) (res *skillDto.CreateSkillTemplateRes, err error) {
|
||||||
|
return skillService.SkillTemplateService.Create(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *skillTemplate) Delete(ctx context.Context, req *skillDto.DeleteSkillTemplateReq) (res *beans.ResponseEmpty, err error) {
|
||||||
|
err = skillService.SkillTemplateService.Delete(ctx, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *skillTemplate) List(ctx context.Context, req *skillDto.ListSkillTemplateReq) (res *skillDto.ListSkillTemplateRes, err error) {
|
||||||
|
return skillService.SkillTemplateService.List(ctx, req)
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package skill
|
||||||
|
|
||||||
|
import (
|
||||||
|
skillDto "ai-agent/workflow/model/dto/skill"
|
||||||
|
skillService "ai-agent/workflow/service/skill"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/beans"
|
||||||
|
)
|
||||||
|
|
||||||
|
type skillUser struct{}
|
||||||
|
|
||||||
|
var SkillUser = new(skillUser)
|
||||||
|
|
||||||
|
func (c *skillUser) Create(ctx context.Context, req *skillDto.CreateSkillUserReq) (res *skillDto.CreateSkillUserRes, err error) {
|
||||||
|
return skillService.SkillUserService.Create(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *skillUser) Update(ctx context.Context, req *skillDto.UpdateSkillUserReq) (res *beans.ResponseEmpty, err error) {
|
||||||
|
err = skillService.SkillUserService.Update(ctx, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *skillUser) Delete(ctx context.Context, req *skillDto.DeleteSkillUserReq) (res *beans.ResponseEmpty, err error) {
|
||||||
|
err = skillService.SkillUserService.Delete(ctx, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *skillUser) Get(ctx context.Context, req *skillDto.GetSkillUserReq) (res *skillDto.SkillUserVO, err error) {
|
||||||
|
return skillService.SkillUserService.Get(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *skillUser) GetUserOrTemplate(ctx context.Context, req *skillDto.GetSkillReq) (res *skillDto.SkillUserVO, err error) {
|
||||||
|
return skillService.SkillUserService.GetUserOrTemplate(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *skillUser) List(ctx context.Context, req *skillDto.ListSkillReq) (res *skillDto.ListSkillUserRes, err error) {
|
||||||
|
return skillService.SkillUserService.List(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *skillUser) ListUser(ctx context.Context, req *skillDto.ListSkillUserReq) (res *skillDto.ListSkillUserRes, err error) {
|
||||||
|
return skillService.SkillUserService.ListUser(ctx, req)
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package tool
|
||||||
|
|
||||||
|
import (
|
||||||
|
toolDto "ai-agent/workflow/model/dto/tool"
|
||||||
|
toolService "ai-agent/workflow/service/tool"
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type tool struct{}
|
||||||
|
|
||||||
|
var Tool = new(tool)
|
||||||
|
|
||||||
|
func (c *tool) List(ctx context.Context, req *toolDto.ToolListReq) (res *toolDto.ToolListRes, err error) {
|
||||||
|
return toolService.ToolService.List(ctx, req)
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package dao
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ai-agent/workflow/consts/public"
|
||||||
|
"ai-agent/workflow/model/dto"
|
||||||
|
"ai-agent/workflow/model/entity"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/db/gfdb"
|
||||||
|
"github.com/gogf/gf/v2/util/gconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
var CreationInfoDao = &creationInfoDao{}
|
||||||
|
|
||||||
|
type creationInfoDao struct{}
|
||||||
|
|
||||||
|
func (d *creationInfoDao) List(ctx context.Context, req *dto.ListCreationInfoReq, fields ...string) (res []*entity.CreationInfo, total int, err error) {
|
||||||
|
model := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameCreationInfo).Fields(fields).OmitEmpty()
|
||||||
|
model.Where(entity.CreationInfoCol.Creator, req.Creator)
|
||||||
|
model.OrderDesc(entity.CreationInfoCol.CreatedAt)
|
||||||
|
if req.Page != nil {
|
||||||
|
model.Page(int(req.Page.PageNum), int(req.Page.PageSize))
|
||||||
|
}
|
||||||
|
r, total, err := model.AllAndCount(false)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = r.Structs(&res)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert 插入
|
||||||
|
func (d *creationInfoDao) Insert(ctx context.Context, req *dto.Create) (id int64, err error) {
|
||||||
|
e := &entity.CreationInfo{}
|
||||||
|
if err = gconv.Struct(req, e); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameCreationInfo).Insert(e)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return r.LastInsertId()
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package file
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ai-agent/workflow/consts/public"
|
||||||
|
fileDto "ai-agent/workflow/model/dto/file"
|
||||||
|
"ai-agent/workflow/model/entity"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/db/gfdb"
|
||||||
|
"github.com/gogf/gf/v2/util/gconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
var FileTempDao = &fileTempDao{}
|
||||||
|
|
||||||
|
type fileTempDao struct{}
|
||||||
|
|
||||||
|
func (d *fileTempDao) Insert(ctx context.Context, req *fileDto.CreateFileTempReq) (id int64, err error) {
|
||||||
|
fileTemp := new(entity.FileTemp)
|
||||||
|
err = gconv.Struct(req, &fileTemp)
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameFileTemp).Insert(&fileTemp)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return r.LastInsertId()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *fileTempDao) BatchInsert(ctx context.Context, req []*fileDto.CreateFileTempReq) (rows int64, err error) {
|
||||||
|
var res []*entity.FileTemp
|
||||||
|
if err = gconv.Structs(req, &res); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameFileTemp).Data(res).Save()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return r.RowsAffected()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *fileTempDao) Delete(ctx context.Context, req *fileDto.DeleteFileTempReq) (rows int64, err error) {
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameFileTemp).Where(entity.FileTempCol.Id, req.Id).Delete()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return r.RowsAffected()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *fileTempDao) List(ctx context.Context, req *fileDto.ListFileTempReq, fields ...string) (res []*entity.FileTemp, total int, err error) {
|
||||||
|
model := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameFileTemp).NoTenantId(ctx).Fields(fields).OmitEmpty()
|
||||||
|
model.OrderDesc(entity.FileTempCol.CreatedAt)
|
||||||
|
if req.Page != nil {
|
||||||
|
model.Page(int(req.Page.PageNum), int(req.Page.PageSize))
|
||||||
|
}
|
||||||
|
r, total, err := model.AllAndCount(false)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = r.Structs(&res)
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
package flow
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ai-agent/workflow/consts/public"
|
||||||
|
"ai-agent/workflow/model/entity"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/db/gfdb"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
FlowAsyncStateInflight = 0 // 任务已提交,结果未取(执行中/结果已发布未取/失败)
|
||||||
|
FlowAsyncStateDone = 1 // 成功,结果已缓存
|
||||||
|
FlowAsyncStateFailed = 2 // 确定失败
|
||||||
|
FlowAsyncSegSentinel = -1 // 非段异步调用的段序号哨兵值
|
||||||
|
)
|
||||||
|
|
||||||
|
var FlowAsyncTaskDao = &flowAsyncTaskDao{}
|
||||||
|
|
||||||
|
type flowAsyncTaskDao struct{}
|
||||||
|
|
||||||
|
// 缓存唯一键是 (node_group_id, node_id, segment_index):node_group_id 是"逻辑运行(attempt)"标识,
|
||||||
|
// 全新/换参重跑换新组 = 换新键;续跑/恢复复用 exec 记录的组 = 读到同组活行。
|
||||||
|
// 删除只有软删且只作用于"终态组"(成功尾部 / 换参重跑废弃的旧组),此类组此后永不再写,
|
||||||
|
// 软删行不会被同键重存(组永不复活)——与 flow_segment_result 同一约定。
|
||||||
|
// execution_id 仅作溯源保留,不参与唯一键。
|
||||||
|
|
||||||
|
// Get 查询唯一键 (node_group_id, node_id, segment_index) 的记录
|
||||||
|
func (d *flowAsyncTaskDao) Get(ctx context.Context, nodeGroupId string, nodeId string, segIdx int) (res *entity.FlowAsyncTask, err error) {
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameFlowAsyncTask).
|
||||||
|
Where(entity.FlowAsyncTaskCol.NodeGroupId, nodeGroupId).
|
||||||
|
Where(entity.FlowAsyncTaskCol.NodeId, nodeId).
|
||||||
|
Where(entity.FlowAsyncTaskCol.SegmentIndex, segIdx).
|
||||||
|
One()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if r.IsEmpty() {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
err = r.Struct(&res)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upsert 提交时写入/更新 in-flight 行:唯一键冲突则更新 model_id/task_id/msg_topic/state(保留已完成结果不动)。
|
||||||
|
// 与 flow_segment_result 相同走 OnConflict().Save();两点相对初稿的调整:
|
||||||
|
// 1. Result 列是 JSONB,空串会被 PG 拒绝(invalid input syntax for type json),
|
||||||
|
// 提交时本无结果,统一写 '{}' 占位(与表列默认值一致)。
|
||||||
|
// 2. 必须用 OnDuplicate 限定冲突更新列——GoFrame Save 默认把 Data 里所有列写进
|
||||||
|
// ON CONFLICT DO UPDATE SET,若不限定会把已缓存的结果覆盖成 '{}',与"保留已完成结果"矛盾。
|
||||||
|
//
|
||||||
|
// 提交/重提统一走本函数重置同组活行(state→in-flight + 新 task_id/msg_topic):不再"删行重建",
|
||||||
|
// 活行从未被软删,OnConflict 更新即可复位,无墓碑冲突、无物理删除。
|
||||||
|
func (d *flowAsyncTaskDao) Upsert(ctx context.Context, nodeGroupId string, execId int64, nodeId string, segIdx int, modelId, taskId int64, msgTopic string) error {
|
||||||
|
rec := &entity.FlowAsyncTask{
|
||||||
|
NodeGroupId: nodeGroupId,
|
||||||
|
ExecutionId: execId,
|
||||||
|
NodeId: nodeId,
|
||||||
|
SegmentIndex: segIdx,
|
||||||
|
ModelId: modelId,
|
||||||
|
TaskId: taskId,
|
||||||
|
MsgTopic: msgTopic,
|
||||||
|
State: FlowAsyncStateInflight,
|
||||||
|
Result: "{}",
|
||||||
|
}
|
||||||
|
_, err := gfdb.DB(ctx, public.DbNameBlackDeacon).
|
||||||
|
Model(ctx, public.TableNameFlowAsyncTask).
|
||||||
|
Data(rec).
|
||||||
|
OnConflict(entity.FlowAsyncTaskCol.NodeGroupId, entity.FlowAsyncTaskCol.NodeId, entity.FlowAsyncTaskCol.SegmentIndex).
|
||||||
|
OnDuplicate(entity.FlowAsyncTaskCol.ModelId, entity.FlowAsyncTaskCol.TaskId, entity.FlowAsyncTaskCol.MsgTopic, entity.FlowAsyncTaskCol.State).
|
||||||
|
Save()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateByKey 按唯一键更新 state/result(OmitNil 丢弃 nil 字段,map 值非 nil 全写入;
|
||||||
|
// state=0 也能落库)。Result 列是 JSONB,空串无法写入,统一落 '{}' 表示无结果。
|
||||||
|
func (d *flowAsyncTaskDao) UpdateByKey(ctx context.Context, nodeGroupId string, nodeId string, segIdx int, state int, result string) error {
|
||||||
|
resultVal := result
|
||||||
|
if resultVal == "" {
|
||||||
|
resultVal = "{}"
|
||||||
|
}
|
||||||
|
_, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameFlowAsyncTask).
|
||||||
|
Where(entity.FlowAsyncTaskCol.NodeGroupId, nodeGroupId).
|
||||||
|
Where(entity.FlowAsyncTaskCol.NodeId, nodeId).
|
||||||
|
Where(entity.FlowAsyncTaskCol.SegmentIndex, segIdx).
|
||||||
|
Data(map[string]any{
|
||||||
|
entity.FlowAsyncTaskCol.State: state,
|
||||||
|
entity.FlowAsyncTaskCol.Result: resultVal,
|
||||||
|
}).
|
||||||
|
Update()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteByGroup 软删指定逻辑运行(组)的异步任务缓存(gfdb Model.Delete 在 deletedAt 配置下退化为软删)。
|
||||||
|
// 仅允许对"终态组"调用:① 工作流执行成功后(BuildExecution 尾部);② 同一条 exec 换参重跑(forceNewRun)
|
||||||
|
// 废弃的旧组(execute 重置成功后回收)。终态组此后永不再被读写 → 软删行不复活、不占同键写入冲突。
|
||||||
|
// 失败/取消/重试耗尽一律不删(保留组行供同参数续跑复用)。绝无物理删除。
|
||||||
|
func (d *flowAsyncTaskDao) DeleteByGroup(ctx context.Context, nodeGroupId string) error {
|
||||||
|
_, err := gfdb.DB(ctx, public.DbNameBlackDeacon).
|
||||||
|
Model(ctx, public.TableNameFlowAsyncTask).
|
||||||
|
Where(entity.FlowAsyncTaskCol.NodeGroupId, nodeGroupId).
|
||||||
|
Delete()
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package flow
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ai-agent/workflow/consts/public"
|
||||||
|
"ai-agent/workflow/model/entity"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/db/gfdb"
|
||||||
|
)
|
||||||
|
|
||||||
|
var FlowCheckpointDao = &flowCheckpointDao{}
|
||||||
|
|
||||||
|
type flowCheckpointDao struct{}
|
||||||
|
|
||||||
|
// Upsert 插入或更新checkpoint数据(按 checkpoint_id 冲突则更新)
|
||||||
|
func (d *flowCheckpointDao) Upsert(ctx context.Context, checkpointId string, data string) error {
|
||||||
|
record := &entity.FlowCheckpoint{
|
||||||
|
CheckpointId: checkpointId,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
// Save 在 PostgreSQL 中自动执行 INSERT ON CONFLICT DO UPDATE
|
||||||
|
_, err := gfdb.DB(ctx, public.DbNameBlackDeacon).
|
||||||
|
Model(ctx, public.TableNameFlowCheckpoint).
|
||||||
|
Save(record)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *flowCheckpointDao) SaveOrUpdate(ctx context.Context, checkpointId string, data string) (err error) {
|
||||||
|
res := &entity.FlowCheckpoint{
|
||||||
|
CheckpointId: checkpointId,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
_, err = gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameFlowCheckpoint).Data(res).OnConflict(entity.FlowCheckpointCol.CheckpointId).Save()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get 根据 checkpoint_id 获取 checkpoint 数据
|
||||||
|
func (d *flowCheckpointDao) Get(ctx context.Context, checkpointId string) (res *entity.FlowCheckpoint, err error) {
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).
|
||||||
|
Model(ctx, public.TableNameFlowCheckpoint).
|
||||||
|
Where(entity.FlowCheckpointCol.CheckpointId, checkpointId).
|
||||||
|
One()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if r == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
err = r.Struct(&res)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete 根据 checkpoint_id 删除checkpoint数据
|
||||||
|
func (d *flowCheckpointDao) Delete(ctx context.Context, checkpointId string) error {
|
||||||
|
_, err := gfdb.DB(ctx, public.DbNameBlackDeacon).
|
||||||
|
Model(ctx, public.TableNameFlowCheckpoint).
|
||||||
|
Where(entity.FlowCheckpointCol.CheckpointId, checkpointId).
|
||||||
|
Delete()
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package flow
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ai-agent/workflow/consts/public"
|
||||||
|
flowDto "ai-agent/workflow/model/dto/flow"
|
||||||
|
"ai-agent/workflow/model/entity"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/db/gfdb"
|
||||||
|
"github.com/gogf/gf/v2/util/gconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
var FlowExecutionDao = &flowExecutionDao{}
|
||||||
|
|
||||||
|
type flowExecutionDao struct{}
|
||||||
|
|
||||||
|
// Insert 创建执行记录
|
||||||
|
func (d *flowExecutionDao) Insert(ctx context.Context, req *flowDto.CreateFlowExecutionReq) (id int64, err error) {
|
||||||
|
var flowExecution = new(entity.FlowExecution)
|
||||||
|
err = gconv.Struct(req, &flowExecution)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameFlowExecution).Insert(flowExecution)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return r.LastInsertId()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *flowExecutionDao) Update(ctx context.Context, req *flowDto.UpdateFlowExecutionReq) (rows int64, err error) {
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameFlowExecution).OmitEmpty().Data(&req).Where(entity.FlowExecutionCol.Id, req.Id).Update()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return r.RowsAffected()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *flowExecutionDao) Get(ctx context.Context, req *flowDto.GetFlowExecutionReq, fields ...string) (res *entity.FlowExecution, err error) {
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameFlowExecution).OmitEmpty().
|
||||||
|
Where(entity.FlowExecutionCol.Id, req.Id).
|
||||||
|
Where(entity.FlowExecutionCol.SessionId, req.SessionId).
|
||||||
|
Fields(fields).One()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = r.Struct(&res)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *flowExecutionDao) List(ctx context.Context, req *flowDto.ListFlowExecutionReq, fields ...string) (res []*entity.FlowExecution, total int, err error) {
|
||||||
|
model := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameFlowExecution).Fields(fields).OmitEmpty()
|
||||||
|
model.Where(entity.FlowExecutionCol.Creator, req.Creator)
|
||||||
|
model.OrderDesc(entity.FlowExecutionCol.CreatedAt)
|
||||||
|
if req.Page != nil {
|
||||||
|
model.Page(int(req.Page.PageNum), int(req.Page.PageSize))
|
||||||
|
}
|
||||||
|
r, total, err := model.AllAndCount(false)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = r.Structs(&res)
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package flow
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ai-agent/workflow/consts/public"
|
||||||
|
"ai-agent/workflow/model/entity"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/db/gfdb"
|
||||||
|
)
|
||||||
|
|
||||||
|
var FlowSegmentResultDao = &flowSegmentResultDao{}
|
||||||
|
|
||||||
|
type flowSegmentResultDao struct{}
|
||||||
|
|
||||||
|
// 缓存唯一键是 (node_group_id, node_id, segment_index):node_group_id 是"逻辑运行(attempt)"标识,
|
||||||
|
// 全新/换参重跑换新组 = 换新键;续跑/恢复复用 exec 记录的组 = 读到同组活行。
|
||||||
|
// 删除只有软删且只作用于"终态组"(成功尾部 / 换参重跑废弃的旧组),此类组此后永不再写 →
|
||||||
|
// 软删行永不被同键重存(不复活)。实体嵌入 SQLBaseDO(含 deleted_at),gfdb Model.Delete() 即软删,
|
||||||
|
// 不再需要 raw Exec 物理删除。
|
||||||
|
|
||||||
|
// Save 段成功后落库:唯一键 (node_group_id, node_id, segment_index),冲突则更新视频引用
|
||||||
|
func (d *flowSegmentResultDao) Save(ctx context.Context, nodeGroupId string, execId int64, nodeId string, segmentIndex int, videoKey, videoURL string) error {
|
||||||
|
rec := &entity.FlowSegmentResult{
|
||||||
|
NodeGroupId: nodeGroupId,
|
||||||
|
ExecutionId: execId,
|
||||||
|
NodeId: nodeId,
|
||||||
|
SegmentIndex: segmentIndex,
|
||||||
|
VideoKey: videoKey,
|
||||||
|
VideoURL: videoURL,
|
||||||
|
}
|
||||||
|
_, err := gfdb.DB(ctx, public.DbNameBlackDeacon).
|
||||||
|
Model(ctx, public.TableNameFlowSegmentResult).
|
||||||
|
Data(rec).
|
||||||
|
OnConflict(entity.FlowSegmentResultCol.NodeGroupId, entity.FlowSegmentResultCol.NodeId, entity.FlowSegmentResultCol.SegmentIndex).
|
||||||
|
Save()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListByNode 返回该节点已成功段(段序号 → 视频引用);仅读当前逻辑运行(组)的活行
|
||||||
|
func (d *flowSegmentResultDao) ListByNode(ctx context.Context, nodeGroupId string, nodeId string) (map[int]entity.SegmentRef, error) {
|
||||||
|
var list []*entity.FlowSegmentResult
|
||||||
|
err := gfdb.DB(ctx, public.DbNameBlackDeacon).
|
||||||
|
Model(ctx, public.TableNameFlowSegmentResult).
|
||||||
|
Where(entity.FlowSegmentResultCol.NodeGroupId, nodeGroupId).
|
||||||
|
Where(entity.FlowSegmentResultCol.NodeId, nodeId).
|
||||||
|
Scan(&list)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
m := make(map[int]entity.SegmentRef, len(list))
|
||||||
|
for _, r := range list {
|
||||||
|
m[r.SegmentIndex] = entity.SegmentRef{Key: r.VideoKey, URL: r.VideoURL}
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteByGroup 软删指定逻辑运行(组)的段结果(gfdb Model.Delete 在 deletedAt 配置下退化为软删)。
|
||||||
|
// 仅允许对"终态组"调用:① 工作流执行成功后(BuildExecution 尾部);② 同一条 exec 换参重跑(forceNewRun)
|
||||||
|
// 废弃的旧组(execute 重置成功后回收)。终态组此后永不再被读写 → 软删行不复活。
|
||||||
|
// 失败/取消不删(保留组行供 reExecute 复用已成功段)。绝无物理删除。
|
||||||
|
func (d *flowSegmentResultDao) DeleteByGroup(ctx context.Context, nodeGroupId string) error {
|
||||||
|
_, err := gfdb.DB(ctx, public.DbNameBlackDeacon).
|
||||||
|
Model(ctx, public.TableNameFlowSegmentResult).
|
||||||
|
Where(entity.FlowSegmentResultCol.NodeGroupId, nodeGroupId).
|
||||||
|
Delete()
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package flow
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ai-agent/workflow/consts/public"
|
||||||
|
flowDto "ai-agent/workflow/model/dto/flow"
|
||||||
|
"ai-agent/workflow/model/entity"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/db/gfdb"
|
||||||
|
"github.com/gogf/gf/v2/util/gconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
var FlowTemplateDao = &flowTemplateDao{}
|
||||||
|
|
||||||
|
type flowTemplateDao struct{}
|
||||||
|
|
||||||
|
func (d *flowTemplateDao) Insert(ctx context.Context, req *flowDto.CreateFlowTemplateReq) (id int64, err error) {
|
||||||
|
var e = new(entity.FlowTemplate)
|
||||||
|
err = gconv.Struct(req, &e)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameFlowTemplate).Insert(e)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return r.LastInsertId()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *flowTemplateDao) Update(ctx context.Context, req *flowDto.UpdateFlowTemplateReq) (rows int64, err error) {
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameFlowTemplate).OmitEmpty().
|
||||||
|
Where(entity.FlowTemplateCol.Id, req.Id).
|
||||||
|
Data(req).
|
||||||
|
Update()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return r.RowsAffected()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *flowTemplateDao) Delete(ctx context.Context, req *flowDto.DeleteFlowTemplateReq) (rows int64, err error) {
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameFlowTemplate).Where(entity.FlowTemplateCol.Id, req.Id).Delete()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return r.RowsAffected()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *flowTemplateDao) Get(ctx context.Context, req *flowDto.GetFlowTemplateReq, fields ...string) (res *entity.FlowTemplate, err error) {
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameFlowTemplate).NoTenantId(ctx).Where(entity.FlowTemplateCol.Id, req.Id).Fields(fields).One()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = r.Struct(&res)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *flowTemplateDao) List(ctx context.Context, req *flowDto.ListFlowTemplateReq, fields ...string) (res []*entity.FlowTemplate, total int, err error) {
|
||||||
|
model := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameFlowTemplate).NoTenantId(ctx).Fields(fields).OmitEmpty()
|
||||||
|
model.OrderDesc(entity.FlowTemplateCol.CreatedAt)
|
||||||
|
if req.Page != nil {
|
||||||
|
model.Page(int(req.Page.PageNum), int(req.Page.PageSize))
|
||||||
|
}
|
||||||
|
r, total, err := model.AllAndCount(false)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = r.Structs(&res)
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package flow
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ai-agent/workflow/consts/public"
|
||||||
|
flowDto "ai-agent/workflow/model/dto/flow"
|
||||||
|
"ai-agent/workflow/model/entity"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/db/gfdb"
|
||||||
|
"github.com/gogf/gf/v2/util/gconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
var FlowUserDao = &flowUserDao{}
|
||||||
|
|
||||||
|
type flowUserDao struct{}
|
||||||
|
|
||||||
|
func (d *flowUserDao) Insert(ctx context.Context, req *flowDto.CreateFlowUserReq) (id int64, err error) {
|
||||||
|
var e = new(entity.FlowUser)
|
||||||
|
err = gconv.Struct(req, &e)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameFlowUser).Insert(e)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return r.LastInsertId()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *flowUserDao) Update(ctx context.Context, req *flowDto.UpdateFlowUserReq) (rows int64, err error) {
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameFlowUser).OmitEmpty().
|
||||||
|
Where(entity.FlowUserCol.Id, req.Id).
|
||||||
|
Data(req).
|
||||||
|
Update()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return r.RowsAffected()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *flowUserDao) Delete(ctx context.Context, req *flowDto.DeleteFlowUserReq) (rows int64, err error) {
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameFlowUser).Where(entity.FlowUserCol.Id, req.Id).Delete()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return r.RowsAffected()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *flowUserDao) Get(ctx context.Context, req *flowDto.GetFlowUserReq, fields ...string) (res *entity.FlowUser, err error) {
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameFlowUser).NoTenantId(ctx).Where(entity.FlowUserCol.Id, req.Id).Fields(fields).One()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = r.Struct(&res)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *flowUserDao) List(ctx context.Context, req *flowDto.ListFlowUserReq, fields ...string) (res []*entity.FlowUser, total int, err error) {
|
||||||
|
model := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameFlowUser).NoTenantId(ctx).Fields(fields).OmitEmpty()
|
||||||
|
model.Where(entity.FlowUserCol.Creator, req.Creator)
|
||||||
|
model.OrderDesc(entity.FlowUserCol.CreatedAt)
|
||||||
|
if req.Page != nil {
|
||||||
|
model.Page(int(req.Page.PageNum), int(req.Page.PageSize))
|
||||||
|
}
|
||||||
|
r, total, err := model.AllAndCount(false)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = r.Structs(&res)
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package node
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ai-agent/workflow/consts/public"
|
||||||
|
nodeDto "ai-agent/workflow/model/dto/node"
|
||||||
|
"ai-agent/workflow/model/entity"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/db/gfdb"
|
||||||
|
"github.com/gogf/gf/v2/database/gdb"
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
"github.com/gogf/gf/v2/util/gconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
var NodeExecutionDao = &nodeExecutionDao{}
|
||||||
|
|
||||||
|
type nodeExecutionDao struct{}
|
||||||
|
|
||||||
|
// Insert 插入节点执行记录
|
||||||
|
func (d *nodeExecutionDao) Insert(ctx context.Context, req *nodeDto.CreateNodeExecutionReq) (id int64, err error) {
|
||||||
|
nodeExecution := new(entity.NodeExecution)
|
||||||
|
err = gconv.Struct(req, &nodeExecution)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameNodeExecution).Insert(&nodeExecution)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return r.LastInsertId()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update 更新节点执行记录
|
||||||
|
func (d *nodeExecutionDao) Update(ctx context.Context, req *nodeDto.UpdateNodeExecutionReq) (rows int64, err error) {
|
||||||
|
model := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameNodeExecution).OmitEmpty()
|
||||||
|
if !g.IsEmpty(req.CompletionTokens) {
|
||||||
|
model.Data(entity.NodeExecutionCol.CompletionTokens, &gdb.Counter{
|
||||||
|
Field: entity.NodeExecutionCol.CompletionTokens,
|
||||||
|
Value: gconv.Float64(req.CompletionTokens),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if !g.IsEmpty(req.PromptTokens) {
|
||||||
|
model.Data(entity.NodeExecutionCol.PromptTokens, &gdb.Counter{
|
||||||
|
Field: entity.NodeExecutionCol.PromptTokens,
|
||||||
|
Value: gconv.Float64(req.PromptTokens),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if !g.IsEmpty(req.TotalTokens) {
|
||||||
|
model.Data(entity.NodeExecutionCol.TotalTokens, &gdb.Counter{
|
||||||
|
Field: entity.NodeExecutionCol.TotalTokens,
|
||||||
|
Value: gconv.Float64(req.TotalTokens),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
r, err := model.Data(&req).Where(entity.NodeExecutionCol.Id, req.Id).Update()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return r.RowsAffected()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete 删除节点执行记录
|
||||||
|
func (d *nodeExecutionDao) Delete(ctx context.Context, req *nodeDto.DeleteNodeExecutionReq) (rows int64, err error) {
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameNodeExecution).Where(entity.NodeExecutionCol.Id, req.Id).Delete()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return r.RowsAffected()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get 根据ID查询节点执行记录
|
||||||
|
func (d *nodeExecutionDao) Get(ctx context.Context, req *nodeDto.GetNodeExecutionReq, fields ...string) (res *entity.NodeExecution, err error) {
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameNodeExecution).NoTenantId(ctx).OmitEmpty().
|
||||||
|
Where(entity.NodeExecutionCol.Id, req.Id).
|
||||||
|
Fields(fields).One()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if r.IsEmpty() {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
err = r.Struct(&res)
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListByFlowExecutionId 查询指定流程执行下的所有节点执行记录
|
||||||
|
func (d *nodeExecutionDao) ListByFlowExecutionId(ctx context.Context, req *nodeDto.ListNodeExecutionByFlowReq, fields ...string) (res []*entity.NodeExecution, total int, err error) {
|
||||||
|
model := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameNodeExecution).NoTenantId(ctx).Fields(fields).OmitEmpty()
|
||||||
|
model.Where(entity.NodeExecutionCol.FlowExecutionId, req.FlowExecutionId)
|
||||||
|
model.Where(entity.NodeExecutionCol.NodeGroupId, req.NodeGroupId)
|
||||||
|
if req.CreatedAtFrom != nil {
|
||||||
|
// 结算按订单收敛:只聚合订单创建后产生的节点记录(重跑开新单,created_at 各自独立,
|
||||||
|
// 避免把已终局运行(已结算扣费)的用量计入本次订单)
|
||||||
|
model.WhereGTE(entity.NodeExecutionCol.CreatedAt, *req.CreatedAtFrom)
|
||||||
|
}
|
||||||
|
model.Where(entity.NodeExecutionCol.Status, req.Status)
|
||||||
|
model.Where(entity.NodeExecutionCol.NodeId, req.NodeId)
|
||||||
|
model.OrderAsc(entity.NodeExecutionCol.CreatedAt)
|
||||||
|
if req.Page != nil {
|
||||||
|
model.Page(int(req.Page.PageNum), int(req.Page.PageSize))
|
||||||
|
}
|
||||||
|
r, total, err := model.AllAndCount(false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
err = r.Structs(&res)
|
||||||
|
return res, total, err
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package node
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ai-agent/workflow/consts/public"
|
||||||
|
nodeDto "ai-agent/workflow/model/dto/node"
|
||||||
|
"ai-agent/workflow/model/entity"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/db/gfdb"
|
||||||
|
"github.com/gogf/gf/v2/util/gconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
var NodePromptDao = &nodePromptDao{}
|
||||||
|
|
||||||
|
type nodePromptDao struct{}
|
||||||
|
|
||||||
|
// Insert 插入节点提示词
|
||||||
|
func (d *nodePromptDao) Insert(ctx context.Context, req *nodeDto.CreateNodePromptReq) (id int64, err error) {
|
||||||
|
nodePrompt := new(entity.NodePrompt)
|
||||||
|
err = gconv.Struct(req, &nodePrompt)
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameNodePrompt).Insert(&nodePrompt)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return r.LastInsertId()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update 更新节点提示词
|
||||||
|
func (d *nodePromptDao) Update(ctx context.Context, req *nodeDto.UpdateNodePromptReq) (rows int64, err error) {
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameNodePrompt).OmitEmpty().Data(&req).Where(entity.NodePromptCol.Id, req.Id).Update()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return r.RowsAffected()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete 删除节点提示词
|
||||||
|
func (d *nodePromptDao) Delete(ctx context.Context, req *nodeDto.DeleteNodePromptReq) (rows int64, err error) {
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameNodePrompt).Where(entity.NodePromptCol.Id, req.Id).Delete()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return r.RowsAffected()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get 根据ID查询节点提示词
|
||||||
|
func (d *nodePromptDao) Get(ctx context.Context, req *nodeDto.GetNodePromptReq, fields ...string) (res *entity.NodePrompt, err error) {
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameNodePrompt).NoTenantId(ctx).OmitEmpty().
|
||||||
|
Where(entity.NodePromptCol.Id, req.Id).
|
||||||
|
Where(entity.NodePromptCol.Prompt, req.Prompt).
|
||||||
|
Where(entity.NodePromptCol.Creator, req.Creator).
|
||||||
|
Fields(fields).One()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if r.IsEmpty() {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
err = r.Struct(&res)
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListByOnlyCreator 查询仅当前创建人自己创建的提示词
|
||||||
|
func (d *nodePromptDao) ListByOnlyCreator(ctx context.Context, req *nodeDto.ListMyNodePromptReq, fields ...string) (res []*entity.NodePrompt, total int, err error) {
|
||||||
|
model := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameNodePrompt).NoTenantId(ctx).Fields(fields).OmitEmpty()
|
||||||
|
model.Where(entity.NodePromptCol.Creator, req.Creator)
|
||||||
|
model.Where(entity.NodePromptCol.NodeType, req.NodeType)
|
||||||
|
model.OrderDesc(entity.NodePromptCol.CreatedAt)
|
||||||
|
if req.Page != nil {
|
||||||
|
model.Page(int(req.Page.PageNum), int(req.Page.PageSize))
|
||||||
|
}
|
||||||
|
r, total, err := model.AllAndCount(false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
err = r.Structs(&res)
|
||||||
|
return res, total, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListByCreator 查询当前创建人的所有提示词(包含系统和用户)
|
||||||
|
func (d *nodePromptDao) ListByCreator(ctx context.Context, req *nodeDto.ListNodePromptReq, fields ...string) (res []*entity.NodePrompt, total int, err error) {
|
||||||
|
// 完整 SQL
|
||||||
|
sql := ` SELECT * FROM black_deacon_node_prompt WHERE (creator=? OR source_type=1) AND node_type=? AND "deleted_at" IS NULL ORDER BY created_at DESC `
|
||||||
|
queryParams := []interface{}{req.Creator, req.NodeType}
|
||||||
|
if req.Page != nil {
|
||||||
|
sql += " LIMIT ?,?"
|
||||||
|
queryParams = append(queryParams, req.Page.PageNum, req.Page.PageSize)
|
||||||
|
}
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).GetAll(ctx, sql, queryParams...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
err = r.Structs(&res)
|
||||||
|
return res, total, err
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
package pull
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ai-agent/workflow/consts/public"
|
||||||
|
pullDto "ai-agent/workflow/model/dto/pull"
|
||||||
|
"ai-agent/workflow/model/entity"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/db/gfdb"
|
||||||
|
"github.com/gogf/gf/v2/util/gconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ActivePullDao = &activePullDao{}
|
||||||
|
|
||||||
|
type activePullDao struct{}
|
||||||
|
|
||||||
|
// Insert 创建执行记录
|
||||||
|
func (d *activePullDao) Insert(ctx context.Context, req *pullDto.CreateActivePullReq) (id int64, err error) {
|
||||||
|
var activePull = new(entity.ActivePull)
|
||||||
|
err = gconv.Struct(req, &activePull)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameActivePull).Insert(activePull)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return r.LastInsertId()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *activePullDao) Update(ctx context.Context, req *pullDto.UpdateActivePullReq) (rows int64, err error) {
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameActivePull).OmitEmpty().Data(&req).Where(entity.ActivePullCol.Id, req.Id).Update()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return r.RowsAffected()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *activePullDao) Delete(ctx context.Context, req *pullDto.DeleteActivePullReq) (rows int64, err error) {
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameActivePull).Where(entity.ActivePullCol.Id, req.Id).Delete()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return r.RowsAffected()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *activePullDao) List(ctx context.Context, req *pullDto.ListActivePullReq, fields ...string) (res []*entity.ActivePull, total int, err error) {
|
||||||
|
model := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameActivePull).Fields(fields).OmitEmpty()
|
||||||
|
model.OrderDesc(entity.ActivePullCol.CreatedAt)
|
||||||
|
if req.Page != nil {
|
||||||
|
model.Page(int(req.Page.PageNum), int(req.Page.PageSize))
|
||||||
|
}
|
||||||
|
r, total, err := model.AllAndCount(false)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = r.Structs(&res)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *activePullDao) ListNative(ctx context.Context, req *pullDto.ListActivePullReq, fields ...string) (res []*entity.ActivePull, total int, err error) {
|
||||||
|
db := gfdb.DB(ctx, public.DbNameBlackDeacon)
|
||||||
|
|
||||||
|
// Select fields
|
||||||
|
selectFields := "*"
|
||||||
|
if len(fields) > 0 {
|
||||||
|
selectFields = strings.Join(fields, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build count query first for total
|
||||||
|
countSql := fmt.Sprintf("SELECT COUNT(*) FROM %s WHERE deleted_at is null", "black_deacon_"+public.TableNameActivePull)
|
||||||
|
countResult, err := db.GetAll(ctx, countSql)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if len(countResult) > 0 {
|
||||||
|
total = countResult[0]["COUNT(*)"].Int()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build data query with native SQL
|
||||||
|
sql := fmt.Sprintf("SELECT %s FROM %s WHERE deleted_at is null ORDER BY created_at DESC", selectFields, "black_deacon_"+public.TableNameActivePull)
|
||||||
|
if req.Page != nil && req.Page.PageNum > 0 && req.Page.PageSize > 0 {
|
||||||
|
offset := (req.Page.PageNum - 1) * req.Page.PageSize
|
||||||
|
sql += fmt.Sprintf(" LIMIT %d OFFSET %d", req.Page.PageSize, offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute query with GetAll
|
||||||
|
result, err := db.GetAll(ctx, sql)
|
||||||
|
if err != nil {
|
||||||
|
return nil, total, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan to entity slice
|
||||||
|
var models []*entity.ActivePull
|
||||||
|
if err = result.Structs(&models); err != nil {
|
||||||
|
return nil, total, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return models, total, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package session
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ai-agent/workflow/consts/public"
|
||||||
|
sessionDto "ai-agent/workflow/model/dto/session"
|
||||||
|
"ai-agent/workflow/model/entity"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/beans"
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/db/gfdb"
|
||||||
|
"github.com/gogf/gf/v2/util/gconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ExecChatDao = &execChatDao{}
|
||||||
|
|
||||||
|
type execChatDao struct{}
|
||||||
|
|
||||||
|
func (d *execChatDao) Insert(ctx context.Context, req *sessionDto.CreateExecChatReq) (id int64, err error) {
|
||||||
|
var s = new(entity.ExecChat)
|
||||||
|
if err = gconv.Struct(req, &s); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecChat).Insert(s)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return r.LastInsertId()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *execChatDao) Update(ctx context.Context, req *sessionDto.UpdateExecChatReq) (rows int64, err error) {
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecChat).OmitEmpty().Data(&req).Where(entity.ExecChatCol.Id, req.Id).Update()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return r.RowsAffected()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearError 清空对话执行记录的报错信息(重新执行成功后调用,OmitEmpty 的 Update 会跳过空串,需显式写空)
|
||||||
|
func (d *execChatDao) ClearError(ctx context.Context, id int64) (rows int64, err error) {
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecChat).
|
||||||
|
Where(entity.ExecChatCol.Id, id).
|
||||||
|
Data(map[string]any{
|
||||||
|
entity.ExecChatCol.ErrorMessage: "",
|
||||||
|
entity.ExecChatCol.Error: "",
|
||||||
|
}).
|
||||||
|
Update()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return r.RowsAffected()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *execChatDao) Delete(ctx context.Context, req *sessionDto.DeleteExecChatReq) (rows int64, err error) {
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecChat).Where(entity.ExecChatCol.Id, req.Id).Delete()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return r.RowsAffected()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *execChatDao) List(ctx context.Context, creator string, page *beans.Page) (res []*entity.ExecChat, total int, err error) {
|
||||||
|
m := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecChat).
|
||||||
|
Where(entity.ExecChatCol.Creator, creator)
|
||||||
|
m.OrderDesc(entity.ExecChatCol.CreatedAt)
|
||||||
|
if page != nil {
|
||||||
|
m.Page(int(page.PageNum), int(page.PageSize))
|
||||||
|
}
|
||||||
|
r, total, err := m.AllAndCount(false)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = r.Structs(&res)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListBySession 查询会话下普通对话执行记录(按创建时间倒序)
|
||||||
|
func (d *execChatDao) ListBySession(ctx context.Context, sessionId string) (res []*entity.ExecChat, err error) {
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecChat).
|
||||||
|
Where(entity.ExecChatCol.SessionId, sessionId).
|
||||||
|
OrderDesc(entity.ExecChatCol.CreatedAt).
|
||||||
|
All()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = r.Structs(&res)
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
package session
|
||||||
|
|
||||||
|
import (
|
||||||
|
flow "ai-agent/workflow/consts/flow"
|
||||||
|
"ai-agent/workflow/consts/public"
|
||||||
|
sessionDto "ai-agent/workflow/model/dto/session"
|
||||||
|
"ai-agent/workflow/model/entity"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/beans"
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/db/gfdb"
|
||||||
|
"github.com/gogf/gf/v2/util/gconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ExecWorkflowDao = &execWorkflowDao{}
|
||||||
|
|
||||||
|
type execWorkflowDao struct{}
|
||||||
|
|
||||||
|
func (d *execWorkflowDao) Insert(ctx context.Context, req *sessionDto.CreateWorkflowReq) (id int64, err error) {
|
||||||
|
var s = new(entity.ExecWorkflow)
|
||||||
|
if err = gconv.Struct(req, &s); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecWorkflow).Insert(s)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return r.LastInsertId()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *execWorkflowDao) Delete(ctx context.Context, req *sessionDto.DeleteExecWorkflowReq) (rows int64, err error) {
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecWorkflow).Where(entity.ExecWorkflowCol.Id, req.Id).Delete()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return r.RowsAffected()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *execWorkflowDao) Update(ctx context.Context, req *sessionDto.UpdateWorkflowReq) (rows int64, err error) {
|
||||||
|
if req.Id <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecWorkflow).OmitEmpty().Data(&req).Where(entity.ExecWorkflowCol.Id, req.Id).Update()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return r.RowsAffected()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateMap 按列更新执行记录(map 更新不走 OmitEmpty,可显式写 0 值)。
|
||||||
|
// 供 recordWorkflow 终态一次原子落库(status/error_message/error/retryable/retry_count 同语句),
|
||||||
|
// 避免"先 UpdateRetry 再 Update"两步写部分生效导致 retryable 与状态不一致
|
||||||
|
func (d *execWorkflowDao) UpdateMap(ctx context.Context, id int64, data map[string]any) error {
|
||||||
|
if id <= 0 || len(data) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecWorkflow).
|
||||||
|
Where(entity.ExecWorkflowCol.Id, id).
|
||||||
|
Data(data).
|
||||||
|
Update()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearError 清空执行记录的报错信息(重新执行成功后调用,OmitEmpty 的 Update 会跳过空串,需显式写空)
|
||||||
|
func (d *execWorkflowDao) ClearError(ctx context.Context, id int64) (rows int64, err error) {
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecWorkflow).
|
||||||
|
Where(entity.ExecWorkflowCol.Id, id).
|
||||||
|
Data(map[string]any{
|
||||||
|
entity.ExecWorkflowCol.ErrorMessage: "",
|
||||||
|
entity.ExecWorkflowCol.Error: "",
|
||||||
|
}).
|
||||||
|
Update()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return r.RowsAffected()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *execWorkflowDao) GetById(ctx context.Context, id int64) (res *entity.ExecWorkflow, err error) {
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecWorkflow).
|
||||||
|
Where(entity.ExecWorkflowCol.Id, id).
|
||||||
|
One()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = r.Struct(&res)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *execWorkflowDao) List(ctx context.Context, creator string, page *beans.Page) (res []*entity.ExecWorkflow, total int, err error) {
|
||||||
|
m := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecWorkflow).
|
||||||
|
Where(entity.ExecWorkflowCol.Creator, creator)
|
||||||
|
m.OrderDesc(entity.ExecWorkflowCol.CreatedAt)
|
||||||
|
if page != nil {
|
||||||
|
m.Page(int(page.PageNum), int(page.PageSize))
|
||||||
|
}
|
||||||
|
r, total, err := m.AllAndCount(false)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = r.Structs(&res)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLatestBySessionAndFlow 查询会话+工作流下最近一次执行记录(按创建时间倒序,无记录返回 nil)
|
||||||
|
func (d *execWorkflowDao) GetLatestBySessionAndFlow(ctx context.Context, sessionId string, flowId int64) (res *entity.ExecWorkflow, err error) {
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecWorkflow).
|
||||||
|
Where(entity.ExecWorkflowCol.SessionId, sessionId).
|
||||||
|
Where(entity.ExecWorkflowCol.FlowId, flowId).
|
||||||
|
OrderDesc(entity.ExecWorkflowCol.CreatedAt).
|
||||||
|
Limit(1).
|
||||||
|
One()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.IsEmpty() {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
err = r.Struct(&res)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListBySession 查询会话下工作流执行记录(按创建时间倒序)
|
||||||
|
func (d *execWorkflowDao) ListBySession(ctx context.Context, sessionId string) (res []*entity.ExecWorkflow, err error) {
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecWorkflow).
|
||||||
|
Where(entity.ExecWorkflowCol.SessionId, sessionId).
|
||||||
|
OrderDesc(entity.ExecWorkflowCol.CreatedAt).
|
||||||
|
All()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = r.Structs(&res)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetRunning 置为运行中并刷新心跳(execute 复用失败记录 / reExecute 共用;
|
||||||
|
// 用 map 更新避免 OmitEmpty 省略 0 值,同时写 status、last_heartbeat、node_group_id)。
|
||||||
|
// 条件更新防双跑:仅当记录当前状态仍属于 prevStatuses 之一时才重置(DB 行锁原子判定),
|
||||||
|
// 其余场景(已被恢复例程/并发触发抢先重置)返回 reset=false,调用方应放弃执行并返回 errExecAlreadyRunning,
|
||||||
|
// 由持有方收敛状态,避免同一 exec 被两条路径并发 BuildExecution。
|
||||||
|
func (d *execWorkflowDao) ResetRunning(ctx context.Context, id int64, nodeGroupId string, prevStatuses ...int8) (reset bool, err error) {
|
||||||
|
m := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecWorkflow).
|
||||||
|
Where(entity.ExecWorkflowCol.Id, id)
|
||||||
|
if len(prevStatuses) > 0 {
|
||||||
|
statuses := make([]interface{}, 0, len(prevStatuses))
|
||||||
|
for _, s := range prevStatuses {
|
||||||
|
statuses = append(statuses, s)
|
||||||
|
}
|
||||||
|
m = m.WhereIn(entity.ExecWorkflowCol.Status, statuses)
|
||||||
|
}
|
||||||
|
r, err := m.Data(map[string]any{
|
||||||
|
entity.ExecWorkflowCol.Status: gconv.Int8(*flow.FlowExecutionStatusRunning.Code()),
|
||||||
|
entity.ExecWorkflowCol.LastHeartbeat: time.Now().UnixMilli(),
|
||||||
|
entity.ExecWorkflowCol.NodeGroupId: nodeGroupId,
|
||||||
|
}).Update()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
rows, err := r.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return rows > 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetRunningIfRecoverable 恢复例程专用:条件重置为运行中,仅当记录仍处于可恢复状态时
|
||||||
|
// (status=3 可重试失败,或 status=1 心跳陈旧的僵尸运行中)才重置(DB 行锁原子判定防双跑)。
|
||||||
|
// staleBeforeMs:status=1 时的心跳陈旧阈值(毫秒),与 ListRecoverable/isRecoverable 判定一致。
|
||||||
|
// 返回 reset=false 表示状态已被其它路径(用户 reExecute/execute 或其它节点恢复)抢先重置,
|
||||||
|
// 本恢复例程应放弃续跑、不落终态,状态由持有方收敛。
|
||||||
|
func (d *execWorkflowDao) ResetRunningIfRecoverable(ctx context.Context, id int64, nodeGroupId string, staleBeforeMs int64) (reset bool, err error) {
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecWorkflow).
|
||||||
|
Where(entity.ExecWorkflowCol.Id, id).
|
||||||
|
Where(fmt.Sprintf("(%s = ? OR (%s = ? AND %s < ?))",
|
||||||
|
entity.ExecWorkflowCol.Status,
|
||||||
|
entity.ExecWorkflowCol.Status,
|
||||||
|
entity.ExecWorkflowCol.LastHeartbeat),
|
||||||
|
gconv.Int8(*flow.FlowExecutionStatusFailed.Code()),
|
||||||
|
gconv.Int8(*flow.FlowExecutionStatusRunning.Code()),
|
||||||
|
staleBeforeMs).
|
||||||
|
Data(map[string]any{
|
||||||
|
entity.ExecWorkflowCol.Status: gconv.Int8(*flow.FlowExecutionStatusRunning.Code()),
|
||||||
|
entity.ExecWorkflowCol.LastHeartbeat: time.Now().UnixMilli(),
|
||||||
|
entity.ExecWorkflowCol.NodeGroupId: nodeGroupId,
|
||||||
|
}).
|
||||||
|
Update()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
rows, err := r.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return rows > 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TouchHeartbeat 更新执行心跳(毫秒时间戳),供后台心跳 goroutine 每 30s 调用一次
|
||||||
|
func (d *execWorkflowDao) TouchHeartbeat(ctx context.Context, id int64) error {
|
||||||
|
_, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecWorkflow).
|
||||||
|
Where(entity.ExecWorkflowCol.Id, id).
|
||||||
|
Data(map[string]any{entity.ExecWorkflowCol.LastHeartbeat: time.Now().UnixMilli()}).
|
||||||
|
Update()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateRetry 更新重试标记与已重试次数(map 更新,retryable=0 也需写入)
|
||||||
|
func (d *execWorkflowDao) UpdateRetry(ctx context.Context, id int64, retryable int, retryCount int) error {
|
||||||
|
_, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecWorkflow).
|
||||||
|
Where(entity.ExecWorkflowCol.Id, id).
|
||||||
|
Data(map[string]any{
|
||||||
|
entity.ExecWorkflowCol.Retryable: retryable,
|
||||||
|
entity.ExecWorkflowCol.RetryCount: retryCount,
|
||||||
|
}).
|
||||||
|
Update()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByIdNoTenant 跨租户按 id 读取执行记录(不追加 tenant_id 过滤)。
|
||||||
|
// 供恢复例程抢锁后、尚未知晓租户时重读 exec 使用;用户路径请继续用租户隔离的 GetById。
|
||||||
|
func (d *execWorkflowDao) GetByIdNoTenant(ctx context.Context, id int64) (res *entity.ExecWorkflow, err error) {
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecWorkflow).
|
||||||
|
NoTenantId(ctx).
|
||||||
|
Where(entity.ExecWorkflowCol.Id, id).
|
||||||
|
One()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = r.Struct(&res)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListRecoverable 返回可恢复执行:僵尸运行中(status=1 且心跳陈旧)或可重试失败(status=3 且 retryable=1 且未耗尽)。
|
||||||
|
// 调用方是跨租户的恢复扫描(无 HTTP 用户),显式 NoTenantId 走系统级扫描,避免僵尸执行因租户过滤漏检。
|
||||||
|
// ctx 必须携带 OTel span(NoTenantId 依赖 traceID 作为 gcache 标记键),恢复扫描由 scanAndRecover 保证。
|
||||||
|
func (d *execWorkflowDao) ListRecoverable(ctx context.Context, now int64, staleBefore int64, maxRetry int) (res []*entity.ExecWorkflow, err error) {
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecWorkflow).
|
||||||
|
NoTenantId(ctx).
|
||||||
|
Where(fmt.Sprintf("(%s = ? AND %s < ?) OR (%s = ? AND %s = 1 AND %s < ?)",
|
||||||
|
entity.ExecWorkflowCol.Status, entity.ExecWorkflowCol.LastHeartbeat,
|
||||||
|
entity.ExecWorkflowCol.Status, entity.ExecWorkflowCol.Retryable, entity.ExecWorkflowCol.RetryCount),
|
||||||
|
gconv.Int8(*flow.FlowExecutionStatusRunning.Code()), staleBefore,
|
||||||
|
gconv.Int8(*flow.FlowExecutionStatusFailed.Code()), maxRetry).
|
||||||
|
All()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 用 All+Structs 而非 Scan:Scan 生成的列清单会丢嵌入 SQLBaseDO 的 id 等基础列,恢复例程需要 id
|
||||||
|
err = r.Structs(&res)
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
package session
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ai-agent/workflow/consts/public"
|
||||||
|
sessionDto "ai-agent/workflow/model/dto/session"
|
||||||
|
"ai-agent/workflow/model/entity"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/beans"
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/db/gfdb"
|
||||||
|
"github.com/gogf/gf/v2/util/gconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ExecWorkflowResultDao = &execWorkflowResultDao{}
|
||||||
|
|
||||||
|
type execWorkflowResultDao struct{}
|
||||||
|
|
||||||
|
func (d *execWorkflowResultDao) Insert(ctx context.Context, req *sessionDto.CreateWorkflowResultReq) (id int64, err error) {
|
||||||
|
var s = new(entity.ExecWorkflowResult)
|
||||||
|
if err = gconv.Struct(req, &s); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecWorkflowResult).Insert(s)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return r.LastInsertId()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *execWorkflowResultDao) BatchInsert(ctx context.Context, req []*sessionDto.CreateWorkflowResultReq) (rows int64, err error) {
|
||||||
|
var res []*entity.ExecWorkflowResult
|
||||||
|
if err = gconv.Structs(req, &res); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecWorkflowResult).Data(res).Save()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return r.RowsAffected()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *execWorkflowResultDao) Delete(ctx context.Context, req *sessionDto.DeleteWorkflowResultReq) (rows int64, err error) {
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecWorkflowResult).Where(entity.ExecWorkflowResultCol.Id, req.Id).Delete()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return r.RowsAffected()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *execWorkflowResultDao) List(ctx context.Context, creator string, page *beans.Page) (res []*entity.ExecWorkflowResult, total int, err error) {
|
||||||
|
m := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecWorkflowResult).
|
||||||
|
Where(entity.ExecWorkflowResultCol.Creator, creator)
|
||||||
|
m.OrderDesc(entity.ExecWorkflowResultCol.CreatedAt)
|
||||||
|
if page != nil {
|
||||||
|
m.Page(int(page.PageNum), int(page.PageSize))
|
||||||
|
}
|
||||||
|
r, total, err := m.AllAndCount(false)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = r.Structs(&res)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListBySession 查询会话下工作流结果(按创建时间倒序)
|
||||||
|
func (d *execWorkflowResultDao) ListBySession(ctx context.Context, sessionId string) (res []*entity.ExecWorkflowResult, err error) {
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecWorkflowResult).
|
||||||
|
Where(entity.ExecWorkflowResultCol.SessionId, sessionId).
|
||||||
|
OrderDesc(entity.ExecWorkflowResultCol.CreatedAt).
|
||||||
|
All()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = r.Structs(&res)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListByExecId 查询指定工作流执行记录下的结果文件路径
|
||||||
|
func (d *execWorkflowResultDao) ListByExecId(ctx context.Context, execId int64) (res []*entity.ExecWorkflowResult, err error) {
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecWorkflowResult).
|
||||||
|
Where(entity.ExecWorkflowResultCol.ExecId, execId).
|
||||||
|
All()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = r.Structs(&res)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListDates 按创建人查询去重后的创建日期(倒序,支持分页;page 为 nil 返回全部日期)
|
||||||
|
func (d *execWorkflowResultDao) ListDates(ctx context.Context, creator string, page *beans.Page) (dates []string, err error) {
|
||||||
|
fieldAlias := "create_date"
|
||||||
|
m := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecWorkflowResult).
|
||||||
|
Fields("DATE("+entity.ExecWorkflowResultCol.CreatedAt+") AS "+fieldAlias).
|
||||||
|
Where(entity.ExecWorkflowResultCol.Creator, creator).
|
||||||
|
Group("DATE(" + entity.ExecWorkflowResultCol.CreatedAt + ")").
|
||||||
|
OrderDesc(fieldAlias)
|
||||||
|
|
||||||
|
if page != nil {
|
||||||
|
m.Page(int(page.PageNum), int(page.PageSize))
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := m.All()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rec := range r {
|
||||||
|
dates = append(dates, rec[fieldAlias].String())
|
||||||
|
}
|
||||||
|
return dates, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListByDates 按创建人查询指定创建日期(DATE(created_at) 命中)内的结果记录,按创建时间倒序
|
||||||
|
func (d *execWorkflowResultDao) ListByDates(ctx context.Context, creator string, dates []string) (res []*entity.ExecWorkflowResult, err error) {
|
||||||
|
if len(dates) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecWorkflowResult).
|
||||||
|
Where(entity.ExecWorkflowResultCol.Creator, creator).
|
||||||
|
WhereIn("DATE(created_at)", dates).
|
||||||
|
OrderDesc(entity.ExecWorkflowResultCol.CreatedAt).
|
||||||
|
All()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = r.Structs(&res)
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package session
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ai-agent/workflow/consts/public"
|
||||||
|
sessionDto "ai-agent/workflow/model/dto/session"
|
||||||
|
"ai-agent/workflow/model/entity"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/beans"
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/db/gfdb"
|
||||||
|
"github.com/gogf/gf/v2/util/gconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
var SessionDao = &sessionDao{}
|
||||||
|
|
||||||
|
type sessionDao struct{}
|
||||||
|
|
||||||
|
func (d *sessionDao) Insert(ctx context.Context, req *sessionDto.CreateSessionReq) (id int64, err error) {
|
||||||
|
var s = new(entity.Session)
|
||||||
|
if err = gconv.Struct(req, &s); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameSession).Insert(s)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return r.LastInsertId()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *sessionDao) GetById(ctx context.Context, id string) (res *entity.Session, err error) {
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameSession).
|
||||||
|
Where(entity.SessionCol.SessionId, id).
|
||||||
|
One()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = r.Struct(&res)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *sessionDao) Delete(ctx context.Context, req *sessionDto.DeleteSessionReq) (rows int64, err error) {
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameSession).Where(entity.SessionCol.SessionId, req.SessionId).Delete()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return r.RowsAffected()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *sessionDao) List(ctx context.Context, creator string, page *beans.Page) (res []*entity.Session, total int, err error) {
|
||||||
|
m := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameSession).
|
||||||
|
Where(entity.SessionCol.Creator, creator)
|
||||||
|
m.OrderDesc(entity.SessionCol.CreatedAt)
|
||||||
|
if page != nil {
|
||||||
|
m.Page(int(page.PageNum), int(page.PageSize))
|
||||||
|
}
|
||||||
|
r, total, err := m.AllAndCount(false)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = r.Structs(&res)
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package skill
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ai-agent/workflow/consts/public"
|
||||||
|
skillDto "ai-agent/workflow/model/dto/skill"
|
||||||
|
"ai-agent/workflow/model/entity"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/db/gfdb"
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
"github.com/gogf/gf/v2/util/gconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
var SkillTemplateDao = &skillTemplateDao{}
|
||||||
|
|
||||||
|
type skillTemplateDao struct{}
|
||||||
|
|
||||||
|
func (d *skillTemplateDao) Insert(ctx context.Context, req *skillDto.CreateSkillTemplateReq) (id int64, err error) {
|
||||||
|
skillTemplate := new(entity.SkillTemplate)
|
||||||
|
err = gconv.Struct(req, &skillTemplate)
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameSkillTemplate).Insert(&skillTemplate)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return r.LastInsertId()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *skillTemplateDao) Update(ctx context.Context, req *skillDto.UpdateSkillTemplateReq) (rows int64, err error) {
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameSkillTemplate).OmitEmpty().Data(&req).Where(entity.SkillTemplateCol.Id, req.Id).Update()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return r.RowsAffected()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *skillTemplateDao) Delete(ctx context.Context, req *skillDto.DeleteSkillTemplateReq) (rows int64, err error) {
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameSkillTemplate).Where(entity.SkillTemplateCol.Id, req.Id).Delete()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return r.RowsAffected()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *skillTemplateDao) Count(ctx context.Context, req *skillDto.GetSkillTemplateReq) (count int, err error) {
|
||||||
|
count, err = gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameSkillTemplate).NoTenantId(ctx).OmitEmpty().
|
||||||
|
WhereNot(entity.SkillTemplateCol.Id, req.NotInId).
|
||||||
|
Where(entity.SkillTemplateCol.Name, req.Name).
|
||||||
|
Where(entity.SkillTemplateCol.Id, req.Id).Count()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *skillTemplateDao) Get(ctx context.Context, req *skillDto.GetSkillTemplateReq, fields ...string) (res *entity.SkillTemplate, err error) {
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameSkillTemplate).NoTenantId(ctx).OmitEmpty().
|
||||||
|
Where(entity.SkillTemplateCol.Id, req.Id).
|
||||||
|
Where(entity.SkillTemplateCol.Name, req.Name).
|
||||||
|
Fields(fields).One()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = r.Struct(&res)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *skillTemplateDao) List(ctx context.Context, req *skillDto.ListSkillTemplateReq, fields ...string) (res []*entity.SkillTemplate, total int, err error) {
|
||||||
|
model := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameSkillTemplate).NoTenantId(ctx).Fields(fields).OmitEmpty()
|
||||||
|
if !g.IsEmpty(req.Keyword) {
|
||||||
|
model.WhereLike(entity.SkillTemplateCol.Name, "%"+req.Keyword+"%")
|
||||||
|
}
|
||||||
|
model.OrderDesc(entity.SkillTemplateCol.CreatedAt)
|
||||||
|
if req.Page != nil {
|
||||||
|
model.Page(int(req.Page.PageNum), int(req.Page.PageSize))
|
||||||
|
}
|
||||||
|
r, total, err := model.AllAndCount(false)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = r.Structs(&res)
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package skill
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ai-agent/workflow/consts/public"
|
||||||
|
skillDto "ai-agent/workflow/model/dto/skill"
|
||||||
|
"ai-agent/workflow/model/entity"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gitea.redpowerfuture.com/red-future/common/db/gfdb"
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
"github.com/gogf/gf/v2/util/gconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
var SkillUserDao = &skillUserDao{}
|
||||||
|
|
||||||
|
type skillUserDao struct{}
|
||||||
|
|
||||||
|
func (d *skillUserDao) Insert(ctx context.Context, req *skillDto.CreateSkillUserReq) (id int64, err error) {
|
||||||
|
skillUser := new(entity.SkillUser)
|
||||||
|
err = gconv.Struct(req, &skillUser)
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameSkillUser).Insert(&skillUser)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return r.LastInsertId()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *skillUserDao) Update(ctx context.Context, req *skillDto.UpdateSkillUserReq) (rows int64, err error) {
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameSkillUser).OmitEmpty().Data(&req).Where(entity.SkillUserCol.Id, req.Id).Update()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return r.RowsAffected()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *skillUserDao) Delete(ctx context.Context, req *skillDto.DeleteSkillUserReq) (rows int64, err error) {
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameSkillUser).Where(entity.SkillUserCol.Id, req.Id).Delete()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return r.RowsAffected()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *skillUserDao) Count(ctx context.Context, req *skillDto.GetSkillUserReq) (count int, err error) {
|
||||||
|
count, err = gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameSkillUser).NoTenantId(ctx).OmitEmpty().
|
||||||
|
Where(entity.SkillUserCol.Name, req.Name).
|
||||||
|
Where(entity.SkillUserCol.Creator, req.Creator).
|
||||||
|
WhereNot(entity.SkillUserCol.Id, req.NotInId).
|
||||||
|
Where(entity.SkillUserCol.Id, req.Id).Count()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *skillUserDao) Get(ctx context.Context, req *skillDto.GetSkillUserReq, fields ...string) (res *entity.SkillUser, err error) {
|
||||||
|
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameSkillUser).NoTenantId(ctx).OmitEmpty().
|
||||||
|
Where(entity.SkillUserCol.Id, req.Id).
|
||||||
|
Where(entity.SkillUserCol.Name, req.Name).
|
||||||
|
Where(entity.SkillUserCol.Creator, req.Creator).
|
||||||
|
Fields(fields).One()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = r.Struct(&res)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *skillUserDao) List(ctx context.Context, req *skillDto.ListSkillUserReq, fields ...string) (res []*entity.SkillUser, total int, err error) {
|
||||||
|
model := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameSkillUser).NoTenantId(ctx).Fields(fields).OmitEmpty()
|
||||||
|
model.Where(entity.SkillUserCol.Creator, req.Creator)
|
||||||
|
if !g.IsEmpty(req.Keyword) {
|
||||||
|
model.WhereLike(entity.SkillUserCol.Name, "%"+req.Keyword+"%")
|
||||||
|
}
|
||||||
|
model.OrderDesc(entity.SkillUserCol.CreatedAt)
|
||||||
|
if req.Page != nil {
|
||||||
|
model.Page(int(req.Page.PageNum), int(req.Page.PageSize))
|
||||||
|
}
|
||||||
|
r, total, err := model.AllAndCount(false)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = r.Structs(&res)
|
||||||
|
return
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user