Compare commits
14 Commits
master
...
feature/fi
Author | SHA1 | Date |
---|---|---|
|
addcf2d24d | |
|
8a2411016c | |
|
068563b914 | |
|
303dd39cb3 | |
|
c91e5519e9 | |
|
99c397aabf | |
|
a43c19eb48 | |
|
11241af84c | |
|
904b67a608 | |
|
949f80d417 | |
|
ecaf2635b5 | |
|
2bb054940a | |
|
abb52eb1d3 | |
|
b5e78cc244 |
|
@ -0,0 +1,23 @@
|
|||
GOHOSTOS:=$(shell go env GOHOSTOS)
|
||||
GOPATH:=$(shell go env GOPATH)
|
||||
VERSION=$(shell git describe --tags --always)
|
||||
PROJECTNAME=yumchina
|
||||
|
||||
ifeq ($(GOHOSTOS), windows)
|
||||
#the `find.exe` is different from `find` in bash/shell.
|
||||
#to see https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/find.
|
||||
#changed to use git-bash.exe to run find cli or other cli friendly, caused of every developer has a Git.
|
||||
#Git_Bash= $(subst cmd\,bin\bash.exe,$(dir $(shell where git)))
|
||||
Git_Bash=$(subst \,/,$(subst cmd\,bin\bash.exe,$(dir $(shell where git | grep cmd))))
|
||||
INTERNAL_PROTO_FILES=$(shell $(Git_Bash) -c "find internal -name *.proto")
|
||||
API_PROTO_FILES=$(shell $(Git_Bash) -c "find api -name *.proto")
|
||||
else
|
||||
INTERNAL_PROTO_FILES=$(shell find internal -name *.proto)
|
||||
API_PROTO_FILES=$(shell find api -name *.proto)
|
||||
endif
|
||||
|
||||
|
||||
.PHONY: wire
|
||||
# generate wire
|
||||
wire:
|
||||
cd ./cmd/server && wire
|
|
@ -1,87 +1,27 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/handlers"
|
||||
"context"
|
||||
"ai_scheduler/internal/config"
|
||||
|
||||
"flag"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
"fmt"
|
||||
|
||||
_ "ai_scheduler/docs"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gofiber/fiber/v2/log"
|
||||
)
|
||||
|
||||
// @title AI Scheduler API
|
||||
// @version 1.0
|
||||
// @description 智能路由调度系统API文档
|
||||
// @termsOfService http://swagger.io/terms/
|
||||
|
||||
// @contact.name API Support
|
||||
// @contact.url http://www.swagger.io/support
|
||||
// @contact.email support@swagger.io
|
||||
|
||||
// @license.name Apache 2.0
|
||||
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
|
||||
// @host localhost:8080
|
||||
// @BasePath /
|
||||
func main() {
|
||||
// 解析命令行参数
|
||||
configPath := flag.String("config", "config.yaml", "配置文件路径")
|
||||
port := flag.String("port", "8080", "服务端口")
|
||||
|
||||
configPath := flag.String("config", "./config/config.yaml", "Path to configuration file")
|
||||
flag.Parse()
|
||||
|
||||
// 初始化应用程序
|
||||
app, err := InitializeApp(*configPath)
|
||||
bc, err := config.LoadConfig(*configPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to initialize app: %v", err)
|
||||
log.Fatalf("加载配置失败: %v", err)
|
||||
}
|
||||
|
||||
// 设置Gin模式为发布模式
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
|
||||
// 设置路由
|
||||
router := handlers.SetupRoutes(app.RouterService)
|
||||
|
||||
// 创建HTTP服务器
|
||||
server := &http.Server{
|
||||
Addr: ":" + *port,
|
||||
Handler: router,
|
||||
ReadTimeout: 30 * time.Second,
|
||||
WriteTimeout: 30 * time.Second,
|
||||
app, cleanup, err := InitializeApp(bc, log.DefaultLogger())
|
||||
if err != nil {
|
||||
log.Fatalf("项目初始化失败: %v", err)
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
// 启动服务器
|
||||
go func() {
|
||||
log.Printf("Starting server on port %s", *port)
|
||||
log.Printf("Swagger UI: http://localhost:%s/swagger/index.html", *port)
|
||||
log.Printf("Health check: http://localhost:%s/health", *port)
|
||||
log.Printf("Chat API: http://localhost:%s/api/v1/chat", *port)
|
||||
|
||||
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatalf("Failed to start server: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待中断信号
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
|
||||
log.Println("Shutting down server...")
|
||||
|
||||
// 优雅关闭服务器
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := server.Shutdown(ctx); err != nil {
|
||||
log.Printf("Server forced to shutdown: %v", err)
|
||||
} else {
|
||||
log.Println("Server exited gracefully")
|
||||
}
|
||||
log.Fatal(app.HttpServer.Listen(fmt.Sprintf(":%d", bc.Server.Port)))
|
||||
}
|
||||
|
|
|
@ -4,63 +4,29 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/biz"
|
||||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/data/impl"
|
||||
"ai_scheduler/internal/pkg"
|
||||
"ai_scheduler/internal/server"
|
||||
"ai_scheduler/internal/services"
|
||||
"ai_scheduler/internal/tools"
|
||||
"ai_scheduler/pkg/ollama"
|
||||
"ai_scheduler/pkg/types"
|
||||
"ai_scheduler/utils"
|
||||
|
||||
"github.com/gofiber/fiber/v2/log"
|
||||
"github.com/google/wire"
|
||||
)
|
||||
|
||||
// InitializeApp 初始化应用程序
|
||||
func InitializeApp(configPath string) (*App, error) {
|
||||
wire.Build(
|
||||
// 配置
|
||||
config.LoadConfig,
|
||||
func InitializeApp(*config.Config, log.AllLogger) (*server.Servers, func(), error) {
|
||||
panic(wire.Build(
|
||||
server.ProviderSetServer,
|
||||
tools.ProviderSetTools,
|
||||
pkg.ProviderSetClient,
|
||||
services.ProviderSetServices,
|
||||
biz.ProviderSetBiz,
|
||||
impl.ProviderImpl,
|
||||
utils.ProviderUtils,
|
||||
))
|
||||
|
||||
// Ollama客户端
|
||||
provideOllamaClient,
|
||||
|
||||
// 工具管理器
|
||||
provideToolsConfig,
|
||||
tools.NewManager,
|
||||
|
||||
// 路由服务
|
||||
provideRouterService,
|
||||
|
||||
// 应用程序
|
||||
NewApp,
|
||||
)
|
||||
return &App{}, nil
|
||||
}
|
||||
|
||||
// provideOllamaClient 提供Ollama客户端
|
||||
func provideOllamaClient(cfg *config.Config) types.AIClient {
|
||||
client, _ := ollama.NewClient(&cfg.Ollama)
|
||||
return client
|
||||
}
|
||||
|
||||
// provideToolsConfig 提供工具配置
|
||||
func provideToolsConfig(cfg *config.Config) *config.ToolsConfig {
|
||||
return &cfg.Tools
|
||||
}
|
||||
|
||||
// provideRouterService 提供路由服务
|
||||
func provideRouterService(aiClient types.AIClient, toolManager *tools.Manager) types.RouterService {
|
||||
return services.NewRouterService(aiClient, toolManager)
|
||||
}
|
||||
|
||||
// App 应用程序结构
|
||||
type App struct {
|
||||
Config *config.Config
|
||||
RouterService types.RouterService
|
||||
}
|
||||
|
||||
// NewApp 创建应用程序
|
||||
func NewApp(cfg *config.Config, routerService types.RouterService) *App {
|
||||
return &App{
|
||||
Config: cfg,
|
||||
RouterService: routerService,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
# 服务器配置
|
||||
server:
|
||||
port: 8090
|
||||
host: "0.0.0.0"
|
||||
|
||||
ollama:
|
||||
base_url: "http://localhost:11434"
|
||||
model: "qwen3:8b"
|
||||
timeout: "120s"
|
||||
level: "info"
|
||||
format: "json"
|
||||
|
||||
sys:
|
||||
session_len: 3
|
||||
|
||||
redis:
|
||||
host: 47.97.27.195:6379
|
||||
type: node
|
||||
pass: lansexiongdi@666
|
||||
key: report-api-test
|
||||
pollSize: 5 #连接池大小,不配置,或配置为0表示不启用连接池
|
||||
minIdleConns: 2 #最小空闲连接数
|
||||
maxIdleTime: 30 #每个连接最大空闲时间,如果超过了这个时间会被关闭
|
||||
tls: 30
|
||||
db:
|
||||
db:
|
||||
driver: mysql
|
||||
source: root:SD###sdf323r343@tcp(121.199.38.107:3306)/sys_ai?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai
|
|
@ -0,0 +1,19 @@
|
|||
#!/bin/bash
|
||||
|
||||
# 使用方法:
|
||||
# ./genModel.sh usercenter user
|
||||
# ./genModel.sh usercenter user_auth
|
||||
# 再将./genModel下的文件剪切到对应服务的model目录里面,记得改package
|
||||
|
||||
|
||||
#生成的表名
|
||||
tables=$1
|
||||
#表生成的genmodel目录
|
||||
modeldir=./internal/data/model
|
||||
|
||||
# 数据库配置
|
||||
prefix=ai_
|
||||
|
||||
|
||||
|
||||
gentool --dsn "root:SD###sdf323r343@tcp(121.199.38.107:3306)/sys_ai?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai" -outPath ${modeldir} -onlyModel -modelPkgName "model" -tables ${prefix}${tables}
|
74
go.mod
74
go.mod
|
@ -5,68 +5,64 @@ go 1.24.0
|
|||
toolchain go1.24.7
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
gitea.cdlsxd.cn/self-tools/l_request v1.0.8
|
||||
github.com/emirpasic/gods v1.18.1
|
||||
github.com/go-kratos/kratos/v2 v2.9.1
|
||||
github.com/gofiber/fiber/v2 v2.52.9
|
||||
github.com/gofiber/websocket/v2 v2.2.1
|
||||
github.com/google/wire v0.7.0
|
||||
github.com/ollama/ollama v0.11.10
|
||||
github.com/ollama/ollama v0.11.11
|
||||
github.com/redis/go-redis/v9 v9.14.0
|
||||
github.com/spf13/viper v1.17.0
|
||||
github.com/swaggo/files v1.0.1
|
||||
github.com/swaggo/gin-swagger v1.6.1
|
||||
github.com/swaggo/swag v1.16.6
|
||||
github.com/tmc/langchaingo v0.1.13
|
||||
google.golang.org/grpc v1.64.0
|
||||
google.golang.org/protobuf v1.34.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gorm.io/driver/mysql v1.6.0
|
||||
gorm.io/gorm v1.31.0
|
||||
xorm.io/builder v0.3.13
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||
github.com/bytedance/sonic v1.11.6 // indirect
|
||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/dlclark/regexp2 v1.11.4 // indirect
|
||||
github.com/fasthttp/websocket v1.5.3 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
github.com/go-openapi/jsonreference v0.19.6 // indirect
|
||||
github.com/go-openapi/spec v0.20.4 // indirect
|
||||
github.com/go-openapi/swag v0.19.15 // 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.20.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mailru/easyjson v0.7.6 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // 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.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/pkoukk/tiktoken-go v0.1.6 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.3.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.10.0 // indirect
|
||||
github.com/spf13/cast v1.5.1 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/stretchr/testify v1.11.1 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.51.0 // indirect
|
||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
golang.org/x/arch v0.8.0 // indirect
|
||||
golang.org/x/crypto v0.36.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa // indirect
|
||||
golang.org/x/mod v0.23.0 // indirect
|
||||
golang.org/x/net v0.38.0 // indirect
|
||||
golang.org/x/sync v0.12.0 // indirect
|
||||
golang.org/x/sys v0.31.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
golang.org/x/tools v0.30.0 // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
189
go.sum
189
go.sum
|
@ -36,76 +36,63 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX
|
|||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
gitea.cdlsxd.cn/self-tools/l_request v1.0.8 h1:FaKRql9mCVcSoaGqPeBOAruZ52slzRngQ6VRTYKNSsA=
|
||||
gitea.cdlsxd.cn/self-tools/l_request v1.0.8/go.mod h1:Qf4hVXm2Eu5vOvwXk8D7U0q/aekMCkZ4Fg9wnRKlasQ=
|
||||
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s=
|
||||
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
|
||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||
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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
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/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
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/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/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
|
||||
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||
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/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fasthttp/websocket v1.5.3 h1:TPpQuLwJYfd4LJPXvHDYPMFWbLjsT91n3GpWtCQtdek=
|
||||
github.com/fasthttp/websocket v1.5.3/go.mod h1:46gg/UBmTU1kUaTcwQXpUxtRwG2PvIZYeA8oL6vF3Fs=
|
||||
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
|
||||
github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
|
||||
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
|
||||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
|
||||
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
|
||||
github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
|
||||
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
|
||||
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
|
||||
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||
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.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
||||
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/go-kratos/kratos/v2 v2.9.1 h1:EGif6/S/aK/RCR5clIbyhioTNyoSrii3FC118jG40Z0=
|
||||
github.com/go-kratos/kratos/v2 v2.9.1/go.mod h1:a1MQLjMhIh7R0kcJS9SzJYR43BRI7EPzzN0J1Ksu2bA=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/gofiber/fiber/v2 v2.52.9 h1:YjKl5DOiyP3j0mO61u3NTmK7or8GzzWzCFzkboyP5cw=
|
||||
github.com/gofiber/fiber/v2 v2.52.9/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
||||
github.com/gofiber/websocket/v2 v2.2.1 h1:C9cjxvloojayOp9AovmpQrk8VqvVnT8Oao3+IUygH7w=
|
||||
github.com/gofiber/websocket/v2 v2.2.1/go.mod h1:Ao/+nyNnX5u/hIFPuHl28a+NIkrqK7PRimyKaj4JxVU=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
|
@ -144,7 +131,6 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||
github.com/google/go-cmp v0.5.4/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/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
|
@ -160,6 +146,8 @@ github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLe
|
|||
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
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/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4=
|
||||
github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
|
@ -171,17 +159,15 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
|||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
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/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
|
@ -190,42 +176,42 @@ 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/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
|
||||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
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/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 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/ollama/ollama v0.11.10 h1:J9zaoTPwIXOrYXCRAqI7rV4cJ+FOMuQc/vBqQ5GIdWg=
|
||||
github.com/ollama/ollama v0.11.10/go.mod h1:9+1//yWPsDE2u+l1a5mpaKrYw4VdnSsRU3ioq5BvMms=
|
||||
github.com/ollama/ollama v0.11.11 h1:mErMiUGclp47rCDbSUmBiY2L76EpT0uIYRZVBO6qg/k=
|
||||
github.com/ollama/ollama v0.11.11/go.mod h1:9+1//yWPsDE2u+l1a5mpaKrYw4VdnSsRU3ioq5BvMms=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
|
||||
github.com/pkoukk/tiktoken-go v0.1.6 h1:JF0TlJzhTbrI30wCvFuiw6FzP2+/bR+FIxUdgEAcUsw=
|
||||
github.com/pkoukk/tiktoken-go v0.1.6/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=
|
||||
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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/redis/go-redis/v9 v9.14.0 h1:u4tNCjXOyzfgeLN+vAZaW1xUooqWDqVEsZN0U01jfAE=
|
||||
github.com/redis/go-redis/v9 v9.14.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/sagikazarmark/locafero v0.3.0 h1:zT7VEGWC2DTflmccN/5T1etyKvxSxpHsjb9cJvm4SvQ=
|
||||
github.com/sagikazarmark/locafero v0.3.0/go.mod h1:w+v7UsPNFwzF1cHuOajOOzoq4U7v/ig1mpRjqV+Bu1U=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY=
|
||||
|
@ -243,31 +229,27 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/
|
|||
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.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.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/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
|
||||
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
|
||||
github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY=
|
||||
github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw=
|
||||
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
|
||||
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
|
||||
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/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/tmc/langchaingo v0.1.13 h1:rcpMWBIi2y3B90XxfE4Ao8dhCQPVDMaNPnN5cGB1CaA=
|
||||
github.com/tmc/langchaingo v0.1.13/go.mod h1:vpQ5NOIhpzxDfTZK9B6tf2GM/MoaHewPWM5KXXGh7hg=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
|
||||
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
|
||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
|
@ -278,16 +260,12 @@ go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
|||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
||||
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
|
@ -326,9 +304,6 @@ 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.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
|
||||
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
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-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
|
@ -360,10 +335,7 @@ golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwY
|
|||
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
|
@ -385,7 +357,6 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
|
|||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
@ -420,20 +391,15 @@ golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
|
||||
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
@ -444,7 +410,6 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
|
@ -497,9 +462,6 @@ golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4f
|
|||
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
|
||||
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
|
||||
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=
|
||||
|
@ -566,6 +528,8 @@ google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6D
|
|||
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 h1:1GBuWVLM/KMVUv1t1En5Gs+gFZCNd360GGb4sSxtrhU=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
|
@ -582,6 +546,8 @@ google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM
|
|||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
|
||||
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY=
|
||||
google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg=
|
||||
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=
|
||||
|
@ -596,8 +562,8 @@ google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFW
|
|||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
|
@ -605,9 +571,12 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/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=
|
||||
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
|
||||
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
|
||||
gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY=
|
||||
gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
@ -615,8 +584,10 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
|
|||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
|
||||
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
|
||||
xorm.io/builder v0.3.13 h1:a3jmiVVL19psGeXx8GIurTp7p0IIgqeDmwhcR6BAOAo=
|
||||
xorm.io/builder v0.3.13/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
package biz
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/data/impl"
|
||||
"ai_scheduler/internal/data/model"
|
||||
"ai_scheduler/internal/entitys"
|
||||
"context"
|
||||
)
|
||||
|
||||
type ChatHistoryBiz struct {
|
||||
chatRepo *impl.ChatImpl
|
||||
}
|
||||
|
||||
func NewChatHistoryBiz(chatRepo *impl.ChatImpl) *ChatHistoryBiz {
|
||||
s := &ChatHistoryBiz{
|
||||
chatRepo: chatRepo,
|
||||
}
|
||||
go s.AsyncProcess(context.Background())
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *ChatHistoryBiz) create(ctx context.Context, sessionID, role, content string) error {
|
||||
chat := model.AiChatHi{
|
||||
SessionID: sessionID,
|
||||
Role: role,
|
||||
Content: content,
|
||||
}
|
||||
|
||||
return s.chatRepo.Create(&chat)
|
||||
}
|
||||
|
||||
// 添加会话历史
|
||||
func (s *ChatHistoryBiz) Create(ctx context.Context, chat entitys.ChatHistory) error {
|
||||
return s.create(ctx, chat.SessionID, chat.Role.String(), chat.Content)
|
||||
}
|
||||
|
||||
// 异步添加会话历史
|
||||
func (s *ChatHistoryBiz) AsyncCreate(ctx context.Context, chat entitys.ChatHistory) {
|
||||
s.chatRepo.AsyncCreate(ctx, model.AiChatHi{
|
||||
SessionID: chat.SessionID,
|
||||
Role: chat.Role.String(),
|
||||
Content: chat.Content,
|
||||
})
|
||||
}
|
||||
|
||||
// 异步处理会话历史
|
||||
func (s *ChatHistoryBiz) AsyncProcess(ctx context.Context) {
|
||||
s.chatRepo.AsyncProcess(ctx)
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package biz
|
||||
|
||||
import "github.com/google/wire"
|
||||
|
||||
var ProviderSetBiz = wire.NewSet(NewAiRouterBiz, NewSessionBiz, NewChatHistoryBiz)
|
|
@ -0,0 +1,371 @@
|
|||
package biz
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/config"
|
||||
errors "ai_scheduler/internal/data/error"
|
||||
"ai_scheduler/internal/data/impl"
|
||||
"ai_scheduler/internal/data/model"
|
||||
"ai_scheduler/internal/entitys"
|
||||
"ai_scheduler/internal/pkg"
|
||||
"ai_scheduler/internal/pkg/utils_ollama"
|
||||
"ai_scheduler/internal/tools"
|
||||
"ai_scheduler/tmpl/dataTemp"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log"
|
||||
|
||||
"github.com/gofiber/websocket/v2"
|
||||
"github.com/tmc/langchaingo/llms"
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// AiRouterService 智能路由服务
|
||||
type AiRouterService struct {
|
||||
//aiClient entitys.AIClient
|
||||
toolManager *tools.Manager
|
||||
sessionImpl *impl.SessionImpl
|
||||
sysImpl *impl.SysImpl
|
||||
taskImpl *impl.TaskImpl
|
||||
hisImpl *impl.ChatImpl
|
||||
conf *config.Config
|
||||
utilAgent *utils_ollama.UtilOllama
|
||||
}
|
||||
|
||||
// NewRouterService 创建路由服务
|
||||
func NewAiRouterBiz(
|
||||
//aiClient entitys.AIClient,
|
||||
toolManager *tools.Manager,
|
||||
sessionImpl *impl.SessionImpl,
|
||||
sysImpl *impl.SysImpl,
|
||||
taskImpl *impl.TaskImpl,
|
||||
hisImpl *impl.ChatImpl,
|
||||
conf *config.Config,
|
||||
utilAgent *utils_ollama.UtilOllama,
|
||||
) entitys.RouterService {
|
||||
return &AiRouterService{
|
||||
//aiClient: aiClient,
|
||||
toolManager: toolManager,
|
||||
sessionImpl: sessionImpl,
|
||||
conf: conf,
|
||||
sysImpl: sysImpl,
|
||||
hisImpl: hisImpl,
|
||||
taskImpl: taskImpl,
|
||||
utilAgent: utilAgent,
|
||||
}
|
||||
}
|
||||
|
||||
// Route 执行智能路由
|
||||
func (r *AiRouterService) Route(ctx context.Context, req *entitys.ChatRequest) (*entitys.ChatResponse, error) {
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Route 执行智能路由
|
||||
func (r *AiRouterService) RouteWithSocket(c *websocket.Conn, req *entitys.ChatSockRequest) error {
|
||||
|
||||
session := c.Headers("X-Session", "")
|
||||
if len(session) == 0 {
|
||||
return errors.SessionNotFound
|
||||
}
|
||||
auth := c.Headers("X-Authorization", "")
|
||||
if len(auth) == 0 {
|
||||
return errors.AuthNotFound
|
||||
}
|
||||
key := c.Headers("X-App-Key", "")
|
||||
if len(key) == 0 {
|
||||
return errors.KeyNotFound
|
||||
}
|
||||
|
||||
sysInfo, err := r.getSysInfo(key)
|
||||
if err != nil {
|
||||
return errors.SysNotFound
|
||||
}
|
||||
|
||||
history, err := r.getSessionChatHis(session)
|
||||
if err != nil {
|
||||
return errors.SystemError
|
||||
}
|
||||
|
||||
task, err := r.getTasks(sysInfo.SysID)
|
||||
if err != nil {
|
||||
return errors.SystemError
|
||||
}
|
||||
|
||||
toolDefinitions := r.registerTools(task)
|
||||
//prompt := r.getPrompt(sysInfo, history, req.Text)
|
||||
|
||||
//意图预测
|
||||
//msg, err := r.utilAgent.Llm.Call(context.TODO(), pkg.JsonStringIgonErr(prompt),
|
||||
// llms.WithTools(toolDefinitions),
|
||||
// //llms.WithToolChoice(llms.FunctionCallBehaviorAuto),
|
||||
// llms.WithJSONMode(),
|
||||
//)
|
||||
prompt := r.getPromptLLM(sysInfo, history, req.Text)
|
||||
msg, err := r.utilAgent.Llm.GenerateContent(context.TODO(), prompt,
|
||||
llms.WithTools(toolDefinitions),
|
||||
llms.WithToolChoice("tool_name"),
|
||||
llms.WithJSONMode(),
|
||||
)
|
||||
if err != nil {
|
||||
return errors.SystemError
|
||||
}
|
||||
|
||||
c.WriteMessage(1, []byte(msg.Choices[0].Content))
|
||||
// 构建消息
|
||||
//messages := []entitys.Message{
|
||||
// {
|
||||
// Role: "user",
|
||||
// Content: req.UserInput,
|
||||
// },
|
||||
//}
|
||||
//
|
||||
//// 第1次调用AI,获取用户意图
|
||||
//intentResponse, err := r.aiClient.Chat(ctx, messages, nil)
|
||||
//if err != nil {
|
||||
// return nil, fmt.Errorf("AI响应失败: %w", err)
|
||||
//}
|
||||
//
|
||||
//// 从AI响应中提取意图
|
||||
//intent := r.extractIntent(intentResponse)
|
||||
//if intent == "" {
|
||||
// return nil, fmt.Errorf("未识别到用户意图")
|
||||
//}
|
||||
//
|
||||
//switch intent {
|
||||
//case "order_diagnosis":
|
||||
// // 订单诊断意图
|
||||
// return r.handleOrderDiagnosis(ctx, req, messages)
|
||||
//case "knowledge_qa":
|
||||
// // 知识问答意图
|
||||
// return r.handleKnowledgeQA(ctx, req, messages)
|
||||
//default:
|
||||
// // 未知意图
|
||||
// return nil, fmt.Errorf("意图识别失败,请明确您的需求呢,我可以为您")
|
||||
//}
|
||||
//
|
||||
//// 获取工具定义
|
||||
//toolDefinitions := r.toolManager.GetToolDefinitions(constants.Caller(req.Caller))
|
||||
//
|
||||
//// 第2次调用AI,获取是否需要使用工具
|
||||
//response, err := r.aiClient.Chat(ctx, messages, toolDefinitions)
|
||||
//if err != nil {
|
||||
// return nil, fmt.Errorf("failed to chat with AI: %w", err)
|
||||
//}
|
||||
//
|
||||
//// 如果没有工具调用,直接返回
|
||||
//if len(response.ToolCalls) == 0 {
|
||||
// return response, nil
|
||||
//}
|
||||
//
|
||||
//// 执行工具调用
|
||||
//toolResults, err := r.toolManager.ExecuteToolCalls(ctx, response.ToolCalls)
|
||||
//if err != nil {
|
||||
// return nil, fmt.Errorf("failed to execute tools: %w", err)
|
||||
//}
|
||||
//
|
||||
//// 构建包含工具结果的消息
|
||||
//messages = append(messages, entitys.Message{
|
||||
// Role: "assistant",
|
||||
// Content: response.Message,
|
||||
//})
|
||||
//
|
||||
//// 添加工具调用结果
|
||||
//for _, toolResult := range toolResults {
|
||||
// toolResultStr, _ := json.Marshal(toolResult.Result)
|
||||
// messages = append(messages, entitys.Message{
|
||||
// Role: "tool",
|
||||
// Content: fmt.Sprintf("Tool %s result: %s", toolResult.Function.Name, string(toolResultStr)),
|
||||
// })
|
||||
//}
|
||||
//
|
||||
//// 第二次调用AI,生成最终回复
|
||||
//finalResponse, err := r.aiClient.Chat(ctx, messages, nil)
|
||||
//if err != nil {
|
||||
// return nil, fmt.Errorf("failed to generate final response: %w", err)
|
||||
//}
|
||||
//
|
||||
//// 合并工具调用信息到最终响应
|
||||
//finalResponse.ToolCalls = toolResults
|
||||
//
|
||||
//log.Printf("Router processed request: %s, used %d tools", req.UserInput, len(toolResults))
|
||||
|
||||
//return finalResponse, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *AiRouterService) getSessionChatHis(sessionId string) (his []model.AiChatHi, err error) {
|
||||
|
||||
cond := builder.NewCond()
|
||||
cond = cond.And(builder.Eq{"session_id": sessionId})
|
||||
|
||||
_, err = r.hisImpl.GetListToStruct(&cond, &dataTemp.ReqPageBo{Limit: r.conf.Sys.SessionLen}, &his, "his_id asc")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (r *AiRouterService) getSysInfo(appKey string) (sysInfo model.AiSy, err error) {
|
||||
cond := builder.NewCond()
|
||||
cond = cond.And(builder.Eq{"app_key": appKey})
|
||||
cond = cond.And(builder.IsNull{"delete_at"})
|
||||
cond = cond.And(builder.Eq{"status": 1})
|
||||
err = r.sysImpl.GetOneBySearchToStrut(&cond, &sysInfo)
|
||||
return
|
||||
}
|
||||
|
||||
func (r *AiRouterService) getTasks(sysId int32) (tasks []model.AiTask, err error) {
|
||||
|
||||
cond := builder.NewCond()
|
||||
cond = cond.And(builder.Eq{"sys_id": sysId})
|
||||
cond = cond.And(builder.IsNull{"delete_at"})
|
||||
cond = cond.And(builder.Eq{"status": 1})
|
||||
_, err = r.taskImpl.GetListToStruct(&cond, nil, &tasks, "")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (r *AiRouterService) registerTools(tasks []model.AiTask) []llms.Tool {
|
||||
taskPrompt := make([]llms.Tool, 0)
|
||||
for _, task := range tasks {
|
||||
var taskConfig entitys.TaskConfig
|
||||
err := json.Unmarshal([]byte(task.Config), &taskConfig)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
taskPrompt = append(taskPrompt, llms.Tool{
|
||||
Type: "function",
|
||||
Function: &llms.FunctionDefinition{
|
||||
Name: task.Index,
|
||||
Description: task.Desc,
|
||||
Parameters: taskConfig.Param,
|
||||
},
|
||||
})
|
||||
|
||||
}
|
||||
return taskPrompt
|
||||
}
|
||||
|
||||
func (r *AiRouterService) getPrompt(sysInfo model.AiSy, history []model.AiChatHi, reqInput string) []entitys.Message {
|
||||
var (
|
||||
prompt = make([]entitys.Message, 0)
|
||||
)
|
||||
prompt = append(prompt, entitys.Message{
|
||||
Role: "system",
|
||||
Content: r.buildSystemPrompt(sysInfo.SysPrompt),
|
||||
}, entitys.Message{
|
||||
Role: "assistant",
|
||||
Content: pkg.JsonStringIgonErr(r.buildAssistant(history)),
|
||||
}, entitys.Message{
|
||||
Role: "user",
|
||||
Content: reqInput,
|
||||
})
|
||||
return prompt
|
||||
}
|
||||
|
||||
func (r *AiRouterService) getPromptLLM(sysInfo model.AiSy, history []model.AiChatHi, reqInput string) []llms.MessageContent {
|
||||
var (
|
||||
prompt = make([]llms.MessageContent, 0)
|
||||
)
|
||||
prompt = append(prompt, llms.MessageContent{
|
||||
Role: llms.ChatMessageTypeSystem,
|
||||
Parts: []llms.ContentPart{
|
||||
llms.TextPart(r.buildSystemPrompt(sysInfo.SysPrompt)),
|
||||
},
|
||||
}, llms.MessageContent{
|
||||
Role: llms.ChatMessageTypeTool,
|
||||
Parts: []llms.ContentPart{
|
||||
llms.TextPart(pkg.JsonStringIgonErr(r.buildAssistant(history))),
|
||||
},
|
||||
}, llms.MessageContent{
|
||||
Role: llms.ChatMessageTypeHuman,
|
||||
Parts: []llms.ContentPart{
|
||||
llms.TextPart(reqInput),
|
||||
},
|
||||
})
|
||||
return prompt
|
||||
}
|
||||
|
||||
// buildSystemPrompt 构建系统提示词
|
||||
func (r *AiRouterService) buildSystemPrompt(prompt string) string {
|
||||
if len(prompt) == 0 {
|
||||
prompt = "[system] 你是一个智能路由系统,核心职责是 **精准解析用户意图并路由至对应任务模块**\n[rule]\n1.返回以下格式的JSON:{ \"index\": \"工具索引index\", \"confidence\": 0.0-1.0,\"reasoning\": \"判断理由\"}\n2.严格返回字符串格式,禁用markdown格式返回\n3.只返回json字符串,不包含任何其他解释性文字\n4.当用户意图非常不清晰时使用,尝试进行追问具体希望查询内容"
|
||||
}
|
||||
|
||||
return prompt
|
||||
}
|
||||
|
||||
func (r *AiRouterService) buildAssistant(his []model.AiChatHi) (chatHis entitys.ChatHis) {
|
||||
for _, item := range his {
|
||||
if len(chatHis.SessionId) == 0 {
|
||||
chatHis.SessionId = item.SessionID
|
||||
}
|
||||
chatHis.Messages = append(chatHis.Messages, entitys.HisMessage{
|
||||
Role: item.Role,
|
||||
Content: item.Content,
|
||||
Timestamp: item.CreateAt.Format("2006-01-02 15:04:05"),
|
||||
})
|
||||
}
|
||||
chatHis.Context = entitys.HisContext{
|
||||
UserLanguage: "zh-CN",
|
||||
SystemMode: "technical_support",
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// extractIntent 从AI响应中提取意图
|
||||
func (r *AiRouterService) extractIntent(response *entitys.ChatResponse) string {
|
||||
if response == nil || response.Message == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 尝试解析JSON
|
||||
var intent struct {
|
||||
Intent string `json:"intent"`
|
||||
Confidence string `json:"confidence"`
|
||||
Reasoning string `json:"reasoning"`
|
||||
}
|
||||
err := json.Unmarshal([]byte(response.Message), &intent)
|
||||
if err != nil {
|
||||
log.Printf("Failed to parse intent JSON: %v", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
return intent.Intent
|
||||
}
|
||||
|
||||
// handleOrderDiagnosis 处理订单诊断意图
|
||||
func (r *AiRouterService) handleOrderDiagnosis(ctx context.Context, req *entitys.ChatRequest, messages []entitys.Message) (*entitys.ChatResponse, error) {
|
||||
// 调用订单详情工具
|
||||
//orderDetailTool, ok := r.toolManager.GetTool("zltxOrderDetail")
|
||||
//if orderDetailTool == nil || !ok {
|
||||
// return nil, fmt.Errorf("order detail tool not found")
|
||||
//}
|
||||
//orderDetailTool.Execute(ctx, json.RawMessage{})
|
||||
//
|
||||
//// 获取相关工具定义
|
||||
//toolDefinitions := r.toolManager.GetToolDefinitions(constants.Caller(req.Caller))
|
||||
//
|
||||
//// 调用AI,获取是否需要使用工具
|
||||
//response, err := r.aiClient.Chat(ctx, messages, toolDefinitions)
|
||||
//if err != nil {
|
||||
// return nil, fmt.Errorf("failed to chat with AI: %w", err)
|
||||
//}
|
||||
//
|
||||
//// 如果没有工具调用,直接返回
|
||||
//if len(response.ToolCalls) == 0 {
|
||||
// return response, nil
|
||||
//}
|
||||
//
|
||||
//// 执行工具调用
|
||||
//toolResults, err := r.toolManager.ExecuteToolCalls(ctx, response.ToolCalls)
|
||||
//if err != nil {
|
||||
// return nil, fmt.Errorf("failed to execute tools: %w", err)
|
||||
//}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// handleKnowledgeQA 处理知识问答意图
|
||||
func (r *AiRouterService) handleKnowledgeQA(ctx context.Context, req *entitys.ChatRequest, messages []entitys.Message) (*entitys.ChatResponse, error) {
|
||||
|
||||
return nil, nil
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
package biz
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/constants"
|
||||
"ai_scheduler/internal/data/impl"
|
||||
"ai_scheduler/internal/data/model"
|
||||
"ai_scheduler/internal/entitys"
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/gofiber/fiber/v2/utils"
|
||||
"time"
|
||||
|
||||
"ai_scheduler/internal/config"
|
||||
)
|
||||
|
||||
type SessionBiz struct {
|
||||
sessionRepo *impl.SessionImpl
|
||||
sysRepo *impl.SysImpl
|
||||
chatRepo *impl.ChatImpl
|
||||
|
||||
conf *config.Config
|
||||
}
|
||||
|
||||
func NewSessionBiz(conf *config.Config, sessionImpl *impl.SessionImpl, sysImpl *impl.SysImpl) *SessionBiz {
|
||||
return &SessionBiz{
|
||||
sessionRepo: sessionImpl,
|
||||
sysRepo: sysImpl,
|
||||
conf: conf,
|
||||
}
|
||||
}
|
||||
|
||||
// InitSession 初始化会话 ,当天存在则返回会话,如果不存在则创建一个
|
||||
func (s *SessionBiz) SessionInit(ctx context.Context, req *entitys.SessionInitRequest) (result *entitys.SessionInitResponse, err error) {
|
||||
|
||||
// 获取系统配置
|
||||
sysConfig, has, err := s.sysRepo.FindOne(s.sysRepo.WithSysId(req.SysId))
|
||||
if err != nil {
|
||||
return
|
||||
} else if !has {
|
||||
err = fmt.Errorf("sys not found")
|
||||
return
|
||||
}
|
||||
|
||||
result = &entitys.SessionInitResponse{
|
||||
Chat: make([]entitys.ChatHistory, 0),
|
||||
}
|
||||
|
||||
// 获取 当天的session
|
||||
t := time.Now().Truncate(24 * time.Hour)
|
||||
session, has, err := s.sessionRepo.FindOne(
|
||||
s.sessionRepo.WithUserId(req.UserId), // 条件:用户ID
|
||||
s.sessionRepo.WithStartTime(t), // 条件:会话开始时间
|
||||
s.sysRepo.WithSysId(sysConfig.SysID), // 条件:系统ID
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
} else if !has {
|
||||
// 不存在,创建一个
|
||||
session = model.AiSession{
|
||||
SysID: sysConfig.SysID,
|
||||
SessionID: utils.UUID(),
|
||||
UserID: req.UserId,
|
||||
}
|
||||
err = s.sessionRepo.Create(&session)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
chat := entitys.ChatHistory{
|
||||
SessionID: session.SessionID,
|
||||
Role: constants.RoleSystem,
|
||||
Content: sysConfig.Prologue,
|
||||
}
|
||||
result.Chat = append(result.Chat, chat)
|
||||
|
||||
// 开场白写入会话历史
|
||||
s.chatRepo.AsyncCreate(ctx, model.AiChatHi{
|
||||
SessionID: chat.SessionID,
|
||||
Role: chat.Role.String(),
|
||||
Content: chat.Content,
|
||||
})
|
||||
|
||||
} else {
|
||||
// 存在,返回会话历史
|
||||
var chatList []model.AiChatHi
|
||||
chatList, err = s.chatRepo.FindAll(
|
||||
s.chatRepo.WithSessionId(session.SessionID), // 条件:会话ID
|
||||
s.chatRepo.OrderByDesc("create_at"), // 排序:按创建时间降序
|
||||
s.chatRepo.WithLimit(constants.ChatHistoryLimit), // 限制返回条数
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 转换为 entitys.ChatHistory 类型
|
||||
for _, chat := range chatList {
|
||||
result.Chat = append(result.Chat, entitys.ChatHistory{
|
||||
SessionID: chat.SessionID,
|
||||
Role: constants.Caller(chat.Role),
|
||||
Content: chat.Content,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// SessionList 会话列表
|
||||
func (s *SessionBiz) SessionList(ctx context.Context, req *entitys.SessionListRequest) (list []model.AiSession, err error) {
|
||||
|
||||
if req.Page <= 0 {
|
||||
req.Page = 1
|
||||
}
|
||||
if req.PageSize <= 0 {
|
||||
req.PageSize = 10
|
||||
}
|
||||
|
||||
list, err = s.sessionRepo.FindAll(
|
||||
s.sessionRepo.WithUserId(req.UserId), // 条件:用户ID
|
||||
s.sessionRepo.WithSysId(req.SysId), // 条件:系统ID
|
||||
s.sessionRepo.PaginateScope(req.Page, req.PageSize), // 分页
|
||||
s.sessionRepo.OrderByDesc("create_at"), // 排序:按创建时间降序
|
||||
)
|
||||
|
||||
return
|
||||
}
|
|
@ -11,13 +11,26 @@ import (
|
|||
type Config struct {
|
||||
Server ServerConfig `mapstructure:"server"`
|
||||
Ollama OllamaConfig `mapstructure:"ollama"`
|
||||
Sys SysConfig `mapstructure:"sys"`
|
||||
Tools ToolsConfig `mapstructure:"tools"`
|
||||
Logging LoggingConfig `mapstructure:"logging"`
|
||||
Redis Redis `mapstructure:"redis"`
|
||||
DB DB `mapstructure:"db"`
|
||||
// LLM *LLM `mapstructure:"llm"`
|
||||
}
|
||||
|
||||
type LLM struct {
|
||||
Model string `mapstructure:"model"`
|
||||
}
|
||||
|
||||
// SysConfig 系统配置
|
||||
type SysConfig struct {
|
||||
SessionLen int `mapstructure:"session_len"`
|
||||
}
|
||||
|
||||
// ServerConfig 服务器配置
|
||||
type ServerConfig struct {
|
||||
Port string `mapstructure:"port"`
|
||||
Port int `mapstructure:"port"`
|
||||
Host string `mapstructure:"host"`
|
||||
}
|
||||
|
||||
|
@ -28,6 +41,27 @@ type OllamaConfig struct {
|
|||
Timeout time.Duration `mapstructure:"timeout"`
|
||||
}
|
||||
|
||||
type Redis struct {
|
||||
Host string `mapstructure:"host"`
|
||||
Type string `mapstructure:"type"`
|
||||
Pass string `mapstructure:"pass"`
|
||||
Key string `mapstructure:"key"`
|
||||
Tls int32 `mapstructure:"tls"`
|
||||
Db int32 `mapstructure:"db"`
|
||||
MaxIdle int32 `mapstructure:"maxIdle"`
|
||||
PoolSize int32 `mapstructure:"poolSize"`
|
||||
MaxIdleTime int32 `mapstructure:"maxIdleTime"`
|
||||
}
|
||||
|
||||
type DB struct {
|
||||
Driver string `mapstructure:"driver"`
|
||||
Source string `mapstructure:"source"`
|
||||
MaxIdle int32 `mapstructure:"maxIdle"`
|
||||
MaxOpen int32 `mapstructure:"maxOpen"`
|
||||
MaxLifetime int32 `mapstructure:"maxLifetime"`
|
||||
IsDebug bool `mapstructure:"isDebug"`
|
||||
}
|
||||
|
||||
// ToolsConfig 工具配置
|
||||
type ToolsConfig struct {
|
||||
Weather ToolConfig `mapstructure:"weather"`
|
||||
|
@ -57,13 +91,13 @@ func LoadConfig(configPath string) (*Config, error) {
|
|||
viper.SetConfigType("yaml")
|
||||
|
||||
// 设置默认值
|
||||
viper.SetDefault("server.port", "8080")
|
||||
viper.SetDefault("server.host", "0.0.0.0")
|
||||
viper.SetDefault("ollama.base_url", "http://localhost:11434")
|
||||
viper.SetDefault("ollama.model", "llama2")
|
||||
viper.SetDefault("ollama.timeout", "30s")
|
||||
viper.SetDefault("logging.level", "info")
|
||||
viper.SetDefault("logging.format", "json")
|
||||
//viper.SetDefault("server.port", "8080")
|
||||
//viper.SetDefault("server.host", "0.0.0.0")
|
||||
//viper.SetDefault("ollama.base_url", "http://localhost:11434")
|
||||
//viper.SetDefault("ollama.model", "llama2")
|
||||
//viper.SetDefault("ollama.timeout", "30s")
|
||||
//viper.SetDefault("logging.level", "info")
|
||||
//viper.SetDefault("logging.format", "json")
|
||||
|
||||
// 读取配置文件
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
|
|
|
@ -5,6 +5,14 @@ type Caller string
|
|||
const (
|
||||
CallerZltx Caller = "zltx" // 直连天下
|
||||
CallerHyt Caller = "hyt" // 货易通
|
||||
|
||||
// 角色, 系统角色,用户角色
|
||||
RoleSystem Caller = "system" // 系统角色
|
||||
RoleUser Caller = "user" // 用户角色
|
||||
RoleAssistant Caller = "assistant" // 助手角色
|
||||
|
||||
// 分页默认条数
|
||||
ChatHistoryLimit = 10
|
||||
)
|
||||
|
||||
func (c Caller) String() string {
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
package constant
|
||||
|
||||
type ConnStatus int8
|
||||
|
||||
const (
|
||||
ConnStatusClosed ConnStatus = iota
|
||||
ConnStatusNormal
|
||||
ConnStatusIgnore
|
||||
)
|
||||
|
||||
type TaskType int32
|
||||
|
||||
const (
|
||||
TaskTypeApi ConnStatus = iota + 1
|
||||
TaskTypeKnowle
|
||||
)
|
|
@ -0,0 +1,3 @@
|
|||
package constant
|
||||
|
||||
const ()
|
|
@ -0,0 +1,45 @@
|
|||
package errorcode
|
||||
|
||||
var (
|
||||
Success = &BusinessErr{code: "0000", message: "成功"}
|
||||
ParamError = &BusinessErr{code: "0001", message: "参数错误"}
|
||||
NotFoundError = &BusinessErr{code: "0004", message: "请求地址未找到"}
|
||||
SystemError = &BusinessErr{code: "0005", message: "系统错误"}
|
||||
|
||||
SupplierNotFound = &BusinessErr{code: "0006", message: "供应商不存在"}
|
||||
SessionNotFound = &BusinessErr{code: "0007", message: "未找到会话信息"}
|
||||
AuthNotFound = &BusinessErr{code: "0008", message: "身份验证失败"}
|
||||
KeyNotFound = &BusinessErr{code: "0009", message: "身份验证失败"}
|
||||
SysNotFound = &BusinessErr{code: "0010", message: "未找到系统信息"}
|
||||
InvalidParam = &BusinessErr{code: InvalidParamCode, message: "无效参数"}
|
||||
)
|
||||
|
||||
const (
|
||||
InvalidParamCode = "0008"
|
||||
)
|
||||
|
||||
type BusinessErr struct {
|
||||
code string
|
||||
message string
|
||||
}
|
||||
|
||||
func (e *BusinessErr) Error() string {
|
||||
return e.message
|
||||
}
|
||||
func (e *BusinessErr) Code() string {
|
||||
return e.code
|
||||
}
|
||||
|
||||
func (e *BusinessErr) Is(target error) bool {
|
||||
_, ok := target.(*BusinessErr)
|
||||
return ok
|
||||
}
|
||||
|
||||
// CustomErr 自定义错误
|
||||
func NewBusinessErr(code string, message string) *BusinessErr {
|
||||
return &BusinessErr{code: code, message: message}
|
||||
}
|
||||
|
||||
func (e *BusinessErr) Wrap(err error) *BusinessErr {
|
||||
return NewBusinessErr(e.code, err.Error())
|
||||
}
|
|
@ -0,0 +1,216 @@
|
|||
package impl
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/data/model"
|
||||
"errors"
|
||||
"fmt"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoConditions = errors.New("不允许不带条件的操作")
|
||||
ErrInvalidPage = errors.New("page和pageSize必须为正整数")
|
||||
)
|
||||
|
||||
/*
|
||||
GORM 基础模型
|
||||
BaseModel 是一个泛型结构体,用于封装GORM数据库通用操作。
|
||||
支持的PO类型需实现具体的数据库模型(如ZxCreditOrder、ZxCreditLog等)
|
||||
*/
|
||||
|
||||
// 定义受支持的PO类型集合(可根据需要扩展), 只有包含表结构才能使用BaseModel,避免使用出现问题
|
||||
type PO interface {
|
||||
model.AiChatHi |
|
||||
model.AiSy | model.AiSession | model.AiTask
|
||||
}
|
||||
|
||||
type BaseModel[P PO] struct {
|
||||
Db *gorm.DB // 数据库连接
|
||||
}
|
||||
|
||||
func NewBaseModel[P PO](db *gorm.DB) BaseRepository[P] {
|
||||
return &BaseModel[P]{
|
||||
Db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// 查询条件函数类型,支持链式条件组装, 由外部调用者自定义组装
|
||||
type CondFunc = func(db *gorm.DB) *gorm.DB
|
||||
|
||||
// 定义通用数据库操作接口
|
||||
type BaseRepository[P PO] interface {
|
||||
FindAll(conditions ...CondFunc) ([]P, error) // 查询所有
|
||||
Paginate(page, pageSize int, conditions ...CondFunc) (*PaginationResult[P], error) // 分页查询
|
||||
FindOne(conditions ...CondFunc) (P, bool, error) // 查询单条记录,若未找到则返回 has=false, err=nil
|
||||
Create(m *P) error // 创建
|
||||
BatchCreate(m *[]P) (err error) // 批量创建
|
||||
Update(m *P, conditions ...CondFunc) (err error) // 更新
|
||||
Delete(conditions ...CondFunc) (err error) // 删除
|
||||
Count(conditions ...CondFunc) (count int64, err error) // 查询条数
|
||||
PaginateScope(page, pageSize int) CondFunc // 分页
|
||||
OrderByDesc(field string) CondFunc // 倒序排序
|
||||
WithId(id interface{}) CondFunc // 查询id
|
||||
WithStatus(status int) CondFunc // 查询status
|
||||
GetDb() *gorm.DB // 获取数据库连接
|
||||
WithLimit(limit int) CondFunc // 限制返回条数
|
||||
}
|
||||
|
||||
// PaginationResult 分页查询结果
|
||||
type PaginationResult[P any] struct {
|
||||
List []P `json:"list"` // 数据列表
|
||||
Total int64 `json:"total"` // 总记录数
|
||||
Page int `json:"page"` // 当前页码(从1开始)
|
||||
PageSize int `json:"page_size"` // 每页数量
|
||||
}
|
||||
|
||||
// 分页查询实现
|
||||
func (this *BaseModel[P]) Paginate(page, pageSize int, conditions ...CondFunc) (*PaginationResult[P], error) {
|
||||
if page < 1 || pageSize < 1 {
|
||||
return nil, ErrInvalidPage
|
||||
}
|
||||
|
||||
db := this.Db.Model(new(P)).Scopes(conditions...) // 自动绑定泛型类型对应的表
|
||||
|
||||
var (
|
||||
total int64
|
||||
list []P
|
||||
)
|
||||
|
||||
// 先统计总数
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, fmt.Errorf("统计总数失败: %w", err)
|
||||
}
|
||||
|
||||
// 再查询分页数据
|
||||
if err := db.Scopes(this.PaginateScope(page, pageSize)).Find(&list).Error; err != nil {
|
||||
return nil, fmt.Errorf("分页查询失败: %w", err)
|
||||
}
|
||||
|
||||
return &PaginationResult[P]{
|
||||
List: list,
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 查询所有
|
||||
func (this *BaseModel[P]) FindAll(conditions ...CondFunc) ([]P, error) {
|
||||
var list []P
|
||||
err := this.Db.Model(new(P)).
|
||||
Scopes(conditions...).
|
||||
Find(&list).Error
|
||||
return list, err
|
||||
}
|
||||
|
||||
// 查询单条记录(优先返回第一条)
|
||||
func (this *BaseModel[P]) FindOne(conditions ...CondFunc) (P, bool, error) {
|
||||
var (
|
||||
result P
|
||||
)
|
||||
|
||||
err := this.Db.Model(new(P)).
|
||||
Scopes(conditions...).
|
||||
First(&result).
|
||||
Error
|
||||
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return result, false, nil // 未找到记录
|
||||
}
|
||||
if err != nil {
|
||||
return result, false, fmt.Errorf("查询单条记录失败: %w", err)
|
||||
}
|
||||
|
||||
return result, true, err
|
||||
}
|
||||
|
||||
// 创建
|
||||
func (this *BaseModel[P]) Create(m *P) error {
|
||||
err := this.Db.Create(m).Error
|
||||
return err
|
||||
}
|
||||
|
||||
// 批量创建
|
||||
func (this *BaseModel[P]) BatchCreate(m *[]P) (err error) {
|
||||
//使用 CreateInBatches 方法分批插入用户,批次大小为 100
|
||||
err = this.Db.CreateInBatches(m, 100).Error
|
||||
return err
|
||||
}
|
||||
|
||||
// 按条件更新记录(必须带条件)
|
||||
func (this *BaseModel[P]) Update(m *P, conditions ...CondFunc) (err error) {
|
||||
// 没有更新条件
|
||||
if len(conditions) == 0 {
|
||||
return ErrNoConditions
|
||||
}
|
||||
|
||||
return this.Db.Model(new(P)).
|
||||
Scopes(conditions...).
|
||||
Updates(m).
|
||||
Error
|
||||
}
|
||||
|
||||
// 按条件删除记录(必须带条件)
|
||||
func (this *BaseModel[P]) Delete(conditions ...CondFunc) (err error) {
|
||||
// 没有更新条件
|
||||
if len(conditions) == 0 {
|
||||
return ErrNoConditions
|
||||
}
|
||||
|
||||
return this.Db.Model(new(P)).
|
||||
Scopes(conditions...).
|
||||
Delete(new(P)).
|
||||
Error
|
||||
}
|
||||
|
||||
// 统计符合条件的记录数
|
||||
func (this *BaseModel[P]) Count(conditions ...CondFunc) (count int64, err error) {
|
||||
err = this.Db.Model(new(P)).
|
||||
Scopes(conditions...).
|
||||
Count(&count).
|
||||
Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// 分页条件生成器
|
||||
func (this *BaseModel[P]) PaginateScope(page, pageSize int) CondFunc {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
if page < 1 || pageSize < 1 {
|
||||
return db // 由上层方法校验参数有效性
|
||||
}
|
||||
offset := (page - 1) * pageSize
|
||||
return db.Offset(offset).Limit(pageSize)
|
||||
}
|
||||
}
|
||||
|
||||
// 倒序排序条件生成器
|
||||
func (this *BaseModel[P]) OrderByDesc(field string) CondFunc {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
return db.Order(fmt.Sprintf("%s DESC", field))
|
||||
}
|
||||
}
|
||||
|
||||
// ID查询条件生成器
|
||||
func (this *BaseModel[P]) WithId(id interface{}) CondFunc {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("id = ?", id)
|
||||
}
|
||||
}
|
||||
|
||||
// 状态查询条件生成器
|
||||
func (this *BaseModel[P]) WithStatus(status int) CondFunc {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("status =?", status)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取数据链接
|
||||
func (this *BaseModel[P]) GetDb() *gorm.DB {
|
||||
return this.Db
|
||||
}
|
||||
|
||||
func (this *BaseModel[P]) WithLimit(limit int) CondFunc {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
return db.Limit(limit)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package impl
|
||||
|
||||
//import (
|
||||
// "ai_scheduler/internal/data/model"
|
||||
// "ai_scheduler/tmpl/dataTemp"
|
||||
// "ai_scheduler/utils"
|
||||
//)
|
||||
|
||||
//type ChatHisImpl struct {
|
||||
// dataTemp.DataTemp
|
||||
//}
|
||||
//
|
||||
//func NewChatHisImpl(db *utils.Db) *ChatHisImpl {
|
||||
// return &ChatHisImpl{*dataTemp.NewDataTemp(db, new(model.AiChatHi))}
|
||||
//}
|
|
@ -0,0 +1,57 @@
|
|||
package impl
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/data/model"
|
||||
"ai_scheduler/tmpl/dataTemp"
|
||||
"ai_scheduler/utils"
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2/log"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ChatImpl struct {
|
||||
dataTemp.DataTemp
|
||||
BaseRepository[model.AiChatHi]
|
||||
chatChannel chan model.AiChatHi
|
||||
}
|
||||
|
||||
func NewChatImpl(db *utils.Db) *ChatImpl {
|
||||
return &ChatImpl{
|
||||
DataTemp: *dataTemp.NewDataTemp(db, new(model.AiChatHi)),
|
||||
BaseRepository: NewBaseModel[model.AiChatHi](db.Client),
|
||||
chatChannel: make(chan model.AiChatHi, 100),
|
||||
}
|
||||
}
|
||||
|
||||
// WithSessionId 条件:会话ID
|
||||
func (impl *ChatImpl) WithSessionId(sessionId interface{}) CondFunc {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("session_id = ?", sessionId)
|
||||
}
|
||||
}
|
||||
|
||||
// 异步添加会话历史
|
||||
func (impl *ChatImpl) AsyncCreate(ctx context.Context, chat model.AiChatHi) {
|
||||
impl.chatChannel <- chat
|
||||
}
|
||||
|
||||
// 异步处理会话历史
|
||||
func (impl *ChatImpl) AsyncProcess(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
case chat := <-impl.chatChannel:
|
||||
log.Infof("ChatHistoryAsyncProcess chat: %v", chat)
|
||||
if err := impl.Create(&chat); err != nil {
|
||||
log.Errorf("ChatHistoryAsyncProcess err: %v", err)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
log.Infof("ChatHistoryAsyncProcess ctx done")
|
||||
return
|
||||
// 定时打印通道大小
|
||||
case <-time.After(time.Second * 5):
|
||||
//log.Infof("ChatHistoryAsyncProcess channel len: %d", len(impl.chatChannel))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package impl
|
||||
|
||||
import (
|
||||
"github.com/google/wire"
|
||||
)
|
||||
|
||||
var ProviderImpl = wire.NewSet(NewSessionImpl, NewSysImpl, NewTaskImpl, NewChatImpl)
|
|
@ -0,0 +1,48 @@
|
|||
package impl
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/data/model"
|
||||
"ai_scheduler/tmpl/dataTemp"
|
||||
"ai_scheduler/utils"
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SessionImpl struct {
|
||||
dataTemp.DataTemp
|
||||
BaseRepository[model.AiSession]
|
||||
}
|
||||
|
||||
func NewSessionImpl(db *utils.Db) *SessionImpl {
|
||||
return &SessionImpl{
|
||||
DataTemp: *dataTemp.NewDataTemp(db, new(model.AiSession)),
|
||||
BaseRepository: NewBaseModel[model.AiSession](db.Client),
|
||||
}
|
||||
}
|
||||
|
||||
// WithUserId 条件:用户ID
|
||||
func (impl *SessionImpl) WithUserId(userId interface{}) CondFunc {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("user_id = ?", userId)
|
||||
}
|
||||
}
|
||||
|
||||
// WithStartTime 条件:会话开始时间
|
||||
func (impl *SessionImpl) WithStartTime(startTime time.Time) CondFunc {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("create_at >= ?", startTime)
|
||||
}
|
||||
}
|
||||
|
||||
// WithSysId 系统id
|
||||
func (s *SessionImpl) WithSysId(sysId interface{}) CondFunc {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("sys_id = ?", sysId)
|
||||
}
|
||||
}
|
||||
|
||||
func (impl *SessionImpl) WithSessionId(sessionId interface{}) CondFunc {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("session_id = ?", sessionId)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package impl
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/data/model"
|
||||
"ai_scheduler/tmpl/dataTemp"
|
||||
"ai_scheduler/utils"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type SysImpl struct {
|
||||
dataTemp.DataTemp
|
||||
BaseModel[model.AiSy]
|
||||
}
|
||||
|
||||
func NewSysImpl(db *utils.Db) *SysImpl {
|
||||
return &SysImpl{
|
||||
DataTemp: *dataTemp.NewDataTemp(db, new(model.AiSy)),
|
||||
BaseModel: BaseModel[model.AiSy]{},
|
||||
}
|
||||
}
|
||||
|
||||
// WithSysId 系统id
|
||||
func (s *SysImpl) WithSysId(sysId interface{}) CondFunc {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("sys_id = ?", sysId)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package impl
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/data/model"
|
||||
"ai_scheduler/tmpl/dataTemp"
|
||||
"ai_scheduler/utils"
|
||||
)
|
||||
|
||||
type TaskImpl struct {
|
||||
dataTemp.DataTemp
|
||||
}
|
||||
|
||||
func NewTaskImpl(db *utils.Db) *TaskImpl {
|
||||
return &TaskImpl{*dataTemp.NewDataTemp(db, new(model.AiTask))}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const TableNameAiChatHi = "ai_chat_his"
|
||||
|
||||
// AiChatHi mapped from table <ai_chat_his>
|
||||
type AiChatHi struct {
|
||||
HisID int64 `gorm:"column:his_id;primaryKey" json:"his_id"`
|
||||
SessionID string `gorm:"column:session_id;not null" json:"session_id"`
|
||||
Role string `gorm:"column:role;not null;comment:system系统输出,assistant助手输出,user用户输入" json:"role"` // system系统输出,assistant助手输出,user用户输入
|
||||
Content string `gorm:"column:content;not null" json:"content"`
|
||||
CreateAt *time.Time `gorm:"column:create_at;default:CURRENT_TIMESTAMP" json:"create_at"`
|
||||
}
|
||||
|
||||
// TableName AiChatHi's table name
|
||||
func (*AiChatHi) TableName() string {
|
||||
return TableNameAiChatHi
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const TableNameAiSession = "ai_session"
|
||||
|
||||
// AiSession mapped from table <ai_session>
|
||||
type AiSession struct {
|
||||
SessionID string `gorm:"column:session_id;primaryKey" json:"session_id"`
|
||||
SysID int32 `gorm:"column:sys_id;not null" json:"sys_id"`
|
||||
KnowlegeSessionID string `gorm:"column:knowlege_session_id;not null" json:"knowlege_session_id"`
|
||||
Title string `gorm:"column:title;not null" json:"title"`
|
||||
CreateAt *time.Time `gorm:"column:create_at;default:CURRENT_TIMESTAMP" json:"create_at"`
|
||||
UpdateAt *time.Time `gorm:"column:update_at;default:CURRENT_TIMESTAMP" json:"update_at"`
|
||||
Status int32 `gorm:"column:status;not null" json:"status"`
|
||||
DeleteAt *time.Time `gorm:"column:delete_at" json:"delete_at"`
|
||||
UserID string `gorm:"column:user_id;comment:用户id" json:"user_id"` // 用户id
|
||||
}
|
||||
|
||||
// TableName AiSession's table name
|
||||
func (*AiSession) TableName() string {
|
||||
return TableNameAiSession
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const TableNameAiSy = "ai_sys"
|
||||
|
||||
// AiSy mapped from table <ai_sys>
|
||||
type AiSy struct {
|
||||
SysID int32 `gorm:"column:sys_id;primaryKey;autoIncrement:true" json:"sys_id"`
|
||||
SysName string `gorm:"column:sys_name;not null" json:"sys_name"`
|
||||
AppKey string `gorm:"column:app_key;not null" json:"app_key"`
|
||||
KnowlegeTenantKey string `gorm:"column:knowlege_tenant_key;not null" json:"knowlege_tenant_key"`
|
||||
KnowlegeBaseID string `gorm:"column:knowlege_base_id;not null" json:"knowlege_base_id"`
|
||||
SysPrompt string `gorm:"column:sys_prompt" json:"sys_prompt"`
|
||||
CreateAt time.Time `gorm:"column:create_at;default:CURRENT_TIMESTAMP" json:"create_at"`
|
||||
UpdateAt time.Time `gorm:"column:update_at;default:CURRENT_TIMESTAMP" json:"update_at"`
|
||||
Status int32 `gorm:"column:status;not null" json:"status"`
|
||||
DeleteAt time.Time `gorm:"column:delete_at" json:"delete_at"`
|
||||
Prologue string `gorm:"column:prologue;comment:会话开场白" json:"prologue"` // 会话开场白
|
||||
}
|
||||
|
||||
// TableName AiSy's table name
|
||||
func (*AiSy) TableName() string {
|
||||
return TableNameAiSy
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const TableNameAiTask = "ai_task"
|
||||
|
||||
// AiTask mapped from table <ai_task>
|
||||
type AiTask struct {
|
||||
TaskID int32 `gorm:"column:task_id;primaryKey" json:"task_id"`
|
||||
SysID int32 `gorm:"column:sys_id;not null" json:"sys_id"`
|
||||
Name string `gorm:"column:name;not null" json:"name"`
|
||||
Index string `gorm:"column:index;not null" json:"index"`
|
||||
Desc string `gorm:"column:desc;not null" json:"desc"`
|
||||
Type int32 `gorm:"column:type;not null;comment:类型,1:api,2:知识库" json:"type"` // 类型,1:api,2:知识库
|
||||
Config string `gorm:"column:config" json:"config"`
|
||||
CreateAt time.Time `gorm:"column:create_at;default:CURRENT_TIMESTAMP" json:"create_at"`
|
||||
UpdateAt time.Time `gorm:"column:update_at;default:CURRENT_TIMESTAMP" json:"update_at"`
|
||||
Status int32 `gorm:"column:status;not null;default:1" json:"status"`
|
||||
DeleteAt time.Time `gorm:"column:delete_at" json:"delete_at"`
|
||||
}
|
||||
|
||||
// TableName AiTask's table name
|
||||
func (*AiTask) TableName() string {
|
||||
return TableNameAiTask
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package entitys
|
||||
|
||||
import "ai_scheduler/internal/constants"
|
||||
|
||||
type ChatHistory struct {
|
||||
SessionID string `json:"session_id"`
|
||||
Role constants.Caller `json:"role"`
|
||||
Content string `json:"content"`
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package entitys
|
||||
|
||||
type SessionInitRequest struct {
|
||||
SysId string `json:"sys_id"`
|
||||
UserId string `json:"user_id"`
|
||||
}
|
||||
|
||||
type SessionInitResponse struct {
|
||||
Chat []ChatHistory `json:"chat"`
|
||||
}
|
||||
|
||||
type SessionListRequest struct {
|
||||
SysId string `json:"sys_id"`
|
||||
UserId string `json:"user_id"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
|
@ -1,8 +1,11 @@
|
|||
package types
|
||||
package entitys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"gitea.cdlsxd.cn/self-tools/l_request"
|
||||
"github.com/gofiber/websocket/v2"
|
||||
)
|
||||
|
||||
// ChatRequest 聊天请求
|
||||
|
@ -12,12 +15,17 @@ type ChatRequest struct {
|
|||
SessionID string `json:"session_id"`
|
||||
ChatRequestMeta ChatRequestMeta `json:"meta,omitempty"`
|
||||
}
|
||||
|
||||
// ChatRequestMeta 聊天请求元数据
|
||||
type ChatRequestMeta struct {
|
||||
Authorization string `json:"authorization"`
|
||||
}
|
||||
|
||||
type ChatSockRequest struct {
|
||||
Text string `json:"text" binding:"required"`
|
||||
Img string `json:"img" binding:"required"`
|
||||
Caller string `json:"caller" binding:"required"`
|
||||
SessionID string `json:"session_id"`
|
||||
}
|
||||
|
||||
// ChatResponse 聊天响应
|
||||
type ChatResponse struct {
|
||||
Status string `json:"status"`
|
||||
|
@ -72,7 +80,33 @@ type Message struct {
|
|||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type FuncApi struct {
|
||||
Param interface{} `json:"param"`
|
||||
Request l_request.Request `json:"request"`
|
||||
}
|
||||
|
||||
type TaskConfig struct {
|
||||
Param interface{} `json:"param"`
|
||||
}
|
||||
|
||||
type ChatHis struct {
|
||||
SessionId string `json:"session_id"`
|
||||
Messages []HisMessage `json:"messages"`
|
||||
Context HisContext `json:"context"`
|
||||
}
|
||||
type HisMessage struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
}
|
||||
|
||||
type HisContext struct {
|
||||
UserLanguage string `json:"user_language"`
|
||||
SystemMode string `json:"system_mode"`
|
||||
}
|
||||
|
||||
// RouterService 路由服务接口
|
||||
type RouterService interface {
|
||||
Route(ctx context.Context, req *ChatRequest) (*ChatResponse, error)
|
||||
RouteWithSocket(c *websocket.Conn, req *ChatSockRequest) error
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
package gateway
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
ID string
|
||||
SendFunc func(data []byte) error
|
||||
}
|
||||
|
||||
type Gateway struct {
|
||||
mu sync.RWMutex
|
||||
clients map[string]*Client // clientID -> Client
|
||||
uidMap map[string][]string // uid -> []clientID
|
||||
}
|
||||
|
||||
func NewGateway() *Gateway {
|
||||
return &Gateway{
|
||||
clients: make(map[string]*Client),
|
||||
uidMap: make(map[string][]string),
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Gateway) AddClient(c *Client) {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
g.clients[c.ID] = c
|
||||
}
|
||||
|
||||
func (g *Gateway) RemoveClient(clientID string) {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
delete(g.clients, clientID)
|
||||
for uid, list := range g.uidMap {
|
||||
newList := []string{}
|
||||
for _, cid := range list {
|
||||
if cid != clientID {
|
||||
newList = append(newList, cid)
|
||||
}
|
||||
}
|
||||
g.uidMap[uid] = newList
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Gateway) SendToAll(msg []byte) {
|
||||
g.mu.RLock()
|
||||
defer g.mu.RUnlock()
|
||||
for _, c := range g.clients {
|
||||
_ = c.SendFunc(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Gateway) SendToClient(clientID string, msg []byte) error {
|
||||
g.mu.RLock()
|
||||
defer g.mu.RUnlock()
|
||||
if c, ok := g.clients[clientID]; ok {
|
||||
return c.SendFunc(msg)
|
||||
}
|
||||
return errors.New("client not found")
|
||||
}
|
||||
|
||||
func (g *Gateway) BindUid(clientID, uid string) error {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
if _, ok := g.clients[clientID]; !ok {
|
||||
return errors.New("client not found")
|
||||
}
|
||||
g.uidMap[uid] = append(g.uidMap[uid], clientID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Gateway) SendToUid(uid string, msg []byte) {
|
||||
g.mu.RLock()
|
||||
defer g.mu.RUnlock()
|
||||
if list, ok := g.uidMap[uid]; ok {
|
||||
for _, cid := range list {
|
||||
if c, ok := g.clients[cid]; ok {
|
||||
_ = c.SendFunc(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Gateway) ListClients() []string {
|
||||
g.mu.RLock()
|
||||
defer g.mu.RUnlock()
|
||||
ids := make([]string, 0, len(g.clients))
|
||||
for id := range g.clients {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func (g *Gateway) ListUids() map[string][]string {
|
||||
g.mu.RLock()
|
||||
defer g.mu.RUnlock()
|
||||
result := make(map[string][]string, len(g.uidMap))
|
||||
for uid, list := range g.uidMap {
|
||||
result[uid] = append([]string(nil), list...)
|
||||
}
|
||||
return result
|
||||
}
|
|
@ -1,90 +0,0 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"ai_scheduler/pkg/types"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ChatHandler 聊天处理器
|
||||
type ChatHandler struct {
|
||||
routerService types.RouterService
|
||||
}
|
||||
|
||||
// NewChatHandler 创建聊天处理器
|
||||
func NewChatHandler(routerService types.RouterService) *ChatHandler {
|
||||
return &ChatHandler{
|
||||
routerService: routerService,
|
||||
}
|
||||
}
|
||||
|
||||
// ChatRequest HTTP聊天请求
|
||||
type ChatRequest struct {
|
||||
UserInput string `json:"user_input" binding:"required" example:"考勤规则"`
|
||||
Caller string `json:"caller" binding:"required" example:"zltx"`
|
||||
SessionID string `json:"session_id" example:"default"`
|
||||
}
|
||||
|
||||
// ChatResponse HTTP聊天响应
|
||||
type ChatResponse struct {
|
||||
Status string `json:"status" example:"success"` // 处理状态
|
||||
Message string `json:"message" example:""` // 响应消息
|
||||
Data any `json:"data,omitempty"` // 响应数据
|
||||
TaskCode string `json:"task_code,omitempty"` // 任务代码
|
||||
}
|
||||
|
||||
// ToolCallResponse 工具调用响应
|
||||
type ToolCallResponse struct {
|
||||
ID string `json:"id" example:"call_1"`
|
||||
Type string `json:"type" example:"function"`
|
||||
Function FunctionCallResponse `json:"function"`
|
||||
Result interface{} `json:"result,omitempty"`
|
||||
}
|
||||
|
||||
// FunctionCallResponse 函数调用响应
|
||||
type FunctionCallResponse struct {
|
||||
Name string `json:"name" example:"get_weather"`
|
||||
Arguments interface{} `json:"arguments"`
|
||||
}
|
||||
|
||||
func (h *ChatHandler) Chat(c *gin.Context) {
|
||||
var req ChatRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, ChatResponse{
|
||||
Status: "error",
|
||||
Message: "请求参数错误",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 转换为服务层请求
|
||||
serviceReq := &types.ChatRequest{
|
||||
UserInput: req.UserInput,
|
||||
Caller: req.Caller,
|
||||
SessionID: req.SessionID,
|
||||
ChatRequestMeta: types.ChatRequestMeta{
|
||||
Authorization: c.GetHeader("Authorization"),
|
||||
},
|
||||
}
|
||||
|
||||
// 调用路由服务
|
||||
response, err := h.routerService.Route(c.Request.Context(), serviceReq)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, ChatResponse{
|
||||
Status: "error",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 转换响应格式
|
||||
httpResponse := &ChatResponse{
|
||||
Message: response.Message,
|
||||
Status: response.Status,
|
||||
Data: response.Data,
|
||||
TaskCode: response.TaskCode,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, httpResponse)
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"ai_scheduler/pkg/types"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
swaggerFiles "github.com/swaggo/files"
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
)
|
||||
|
||||
// SetupRoutes 设置路由
|
||||
func SetupRoutes(routerService types.RouterService) *gin.Engine {
|
||||
r := gin.Default()
|
||||
|
||||
// 添加CORS中间件
|
||||
r.Use(func(c *gin.Context) {
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
if c.Request.Method == "OPTIONS" {
|
||||
c.AbortWithStatus(204)
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
})
|
||||
|
||||
// 创建处理器
|
||||
chatHandler := NewChatHandler(routerService)
|
||||
|
||||
// API路由组
|
||||
v1 := r.Group("/api/v1")
|
||||
{
|
||||
v1.POST("/chat", chatHandler.Chat)
|
||||
}
|
||||
|
||||
// Swagger文档
|
||||
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
||||
|
||||
return r
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package pkg
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
func JsonStringIgonErr(data interface{}) string {
|
||||
dataByte, _ := json.Marshal(data)
|
||||
return string(dataByte)
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package pkg
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/pkg/utils_gorm"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Db struct {
|
||||
Client *gorm.DB
|
||||
}
|
||||
|
||||
func NewGormDb(c *config.Config) (*Db, func()) {
|
||||
transDBClient, mf := utils_gorm.DBConn(c.DB)
|
||||
//directDBClient, df := directDB(c, hLog)
|
||||
cleanup := func() {
|
||||
mf()
|
||||
//df()
|
||||
}
|
||||
return &Db{
|
||||
Client: transDBClient,
|
||||
//DirectDBClient: directDBClient,
|
||||
}, cleanup
|
||||
}
|
|
@ -0,0 +1,279 @@
|
|||
package mapstructure
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// typedDecodeHook takes a raw DecodeHookFunc (an interface{}) and turns
|
||||
// it into the proper DecodeHookFunc type, such as DecodeHookFuncType.
|
||||
func typedDecodeHook(h DecodeHookFunc) DecodeHookFunc {
|
||||
// Create variables here so we can reference them with the reflect pkg
|
||||
var f1 DecodeHookFuncType
|
||||
var f2 DecodeHookFuncKind
|
||||
var f3 DecodeHookFuncValue
|
||||
|
||||
// Fill in the variables into this interface and the rest is done
|
||||
// automatically using the reflect package.
|
||||
potential := []interface{}{f1, f2, f3}
|
||||
|
||||
v := reflect.ValueOf(h)
|
||||
vt := v.Type()
|
||||
for _, raw := range potential {
|
||||
pt := reflect.ValueOf(raw).Type()
|
||||
if vt.ConvertibleTo(pt) {
|
||||
return v.Convert(pt).Interface()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DecodeHookExec executes the given decode hook. This should be used
|
||||
// since it'll naturally degrade to the older backwards compatible DecodeHookFunc
|
||||
// that took reflect.Kind instead of reflect.Type.
|
||||
func DecodeHookExec(
|
||||
raw DecodeHookFunc,
|
||||
from reflect.Value, to reflect.Value) (interface{}, error) {
|
||||
|
||||
switch f := typedDecodeHook(raw).(type) {
|
||||
case DecodeHookFuncType:
|
||||
return f(from.Type(), to.Type(), from.Interface())
|
||||
case DecodeHookFuncKind:
|
||||
return f(from.Kind(), to.Kind(), from.Interface())
|
||||
case DecodeHookFuncValue:
|
||||
return f(from, to)
|
||||
default:
|
||||
return nil, errors.New("invalid decode hook signature")
|
||||
}
|
||||
}
|
||||
|
||||
// ComposeDecodeHookFunc creates a single DecodeHookFunc that
|
||||
// automatically composes multiple DecodeHookFuncs.
|
||||
//
|
||||
// The composed funcs are called in order, with the result of the
|
||||
// previous transformation.
|
||||
func ComposeDecodeHookFunc(fs ...DecodeHookFunc) DecodeHookFunc {
|
||||
return func(f reflect.Value, t reflect.Value) (interface{}, error) {
|
||||
var err error
|
||||
data := f.Interface()
|
||||
|
||||
newFrom := f
|
||||
for _, f1 := range fs {
|
||||
data, err = DecodeHookExec(f1, newFrom, t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newFrom = reflect.ValueOf(data)
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
}
|
||||
|
||||
// OrComposeDecodeHookFunc executes all input hook functions until one of them returns no error. In that case its value is returned.
|
||||
// If all hooks return an error, OrComposeDecodeHookFunc returns an error concatenating all error messages.
|
||||
func OrComposeDecodeHookFunc(ff ...DecodeHookFunc) DecodeHookFunc {
|
||||
return func(a, b reflect.Value) (interface{}, error) {
|
||||
var allErrs string
|
||||
var out interface{}
|
||||
var err error
|
||||
|
||||
for _, f := range ff {
|
||||
out, err = DecodeHookExec(f, a, b)
|
||||
if err != nil {
|
||||
allErrs += err.Error() + "\n"
|
||||
continue
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
return nil, errors.New(allErrs)
|
||||
}
|
||||
}
|
||||
|
||||
// StringToSliceHookFunc returns a DecodeHookFunc that converts
|
||||
// string to []string by splitting on the given sep.
|
||||
func StringToSliceHookFunc(sep string) DecodeHookFunc {
|
||||
return func(
|
||||
f reflect.Kind,
|
||||
t reflect.Kind,
|
||||
data interface{}) (interface{}, error) {
|
||||
if f != reflect.String || t != reflect.Slice {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
raw := data.(string)
|
||||
if raw == "" {
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
return strings.Split(raw, sep), nil
|
||||
}
|
||||
}
|
||||
|
||||
// StringToTimeDurationHookFunc returns a DecodeHookFunc that converts
|
||||
// strings to time.Duration.
|
||||
func StringToTimeDurationHookFunc() DecodeHookFunc {
|
||||
return func(
|
||||
f reflect.Type,
|
||||
t reflect.Type,
|
||||
data interface{}) (interface{}, error) {
|
||||
if f.Kind() != reflect.String {
|
||||
return data, nil
|
||||
}
|
||||
if t != reflect.TypeOf(time.Duration(5)) {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Convert it by parsing
|
||||
return time.ParseDuration(data.(string))
|
||||
}
|
||||
}
|
||||
|
||||
// StringToIPHookFunc returns a DecodeHookFunc that converts
|
||||
// strings to net.IP
|
||||
func StringToIPHookFunc() DecodeHookFunc {
|
||||
return func(
|
||||
f reflect.Type,
|
||||
t reflect.Type,
|
||||
data interface{}) (interface{}, error) {
|
||||
if f.Kind() != reflect.String {
|
||||
return data, nil
|
||||
}
|
||||
if t != reflect.TypeOf(net.IP{}) {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Convert it by parsing
|
||||
ip := net.ParseIP(data.(string))
|
||||
if ip == nil {
|
||||
return net.IP{}, fmt.Errorf("failed parsing ip %v", data)
|
||||
}
|
||||
|
||||
return ip, nil
|
||||
}
|
||||
}
|
||||
|
||||
// StringToIPNetHookFunc returns a DecodeHookFunc that converts
|
||||
// strings to net.IPNet
|
||||
func StringToIPNetHookFunc() DecodeHookFunc {
|
||||
return func(
|
||||
f reflect.Type,
|
||||
t reflect.Type,
|
||||
data interface{}) (interface{}, error) {
|
||||
if f.Kind() != reflect.String {
|
||||
return data, nil
|
||||
}
|
||||
if t != reflect.TypeOf(net.IPNet{}) {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Convert it by parsing
|
||||
_, net, err := net.ParseCIDR(data.(string))
|
||||
return net, err
|
||||
}
|
||||
}
|
||||
|
||||
// StringToTimeHookFunc returns a DecodeHookFunc that converts
|
||||
// strings to time.Time.
|
||||
func StringToTimeHookFunc(layout string) DecodeHookFunc {
|
||||
return func(
|
||||
f reflect.Type,
|
||||
t reflect.Type,
|
||||
data interface{}) (interface{}, error) {
|
||||
if f.Kind() != reflect.String {
|
||||
return data, nil
|
||||
}
|
||||
if t != reflect.TypeOf(time.Time{}) {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Convert it by parsing
|
||||
return time.Parse(layout, data.(string))
|
||||
}
|
||||
}
|
||||
|
||||
// WeaklyTypedHook is a DecodeHookFunc which adds support for weak typing to
|
||||
// the decoder.
|
||||
//
|
||||
// Note that this is significantly different from the WeaklyTypedInput option
|
||||
// of the DecoderConfig.
|
||||
func WeaklyTypedHook(
|
||||
f reflect.Kind,
|
||||
t reflect.Kind,
|
||||
data interface{}) (interface{}, error) {
|
||||
dataVal := reflect.ValueOf(data)
|
||||
switch t {
|
||||
case reflect.String:
|
||||
switch f {
|
||||
case reflect.Bool:
|
||||
if dataVal.Bool() {
|
||||
return "1", nil
|
||||
}
|
||||
return "0", nil
|
||||
case reflect.Float32:
|
||||
return strconv.FormatFloat(dataVal.Float(), 'f', -1, 64), nil
|
||||
case reflect.Int:
|
||||
return strconv.FormatInt(dataVal.Int(), 10), nil
|
||||
case reflect.Slice:
|
||||
dataType := dataVal.Type()
|
||||
elemKind := dataType.Elem().Kind()
|
||||
if elemKind == reflect.Uint8 {
|
||||
return string(dataVal.Interface().([]uint8)), nil
|
||||
}
|
||||
case reflect.Uint:
|
||||
return strconv.FormatUint(dataVal.Uint(), 10), nil
|
||||
}
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func RecursiveStructToMapHookFunc() DecodeHookFunc {
|
||||
return func(f reflect.Value, t reflect.Value) (interface{}, error) {
|
||||
if f.Kind() != reflect.Struct {
|
||||
return f.Interface(), nil
|
||||
}
|
||||
|
||||
var i interface{} = struct{}{}
|
||||
if t.Type() != reflect.TypeOf(&i).Elem() {
|
||||
return f.Interface(), nil
|
||||
}
|
||||
|
||||
m := make(map[string]interface{})
|
||||
t.Set(reflect.ValueOf(m))
|
||||
|
||||
return f.Interface(), nil
|
||||
}
|
||||
}
|
||||
|
||||
// TextUnmarshallerHookFunc returns a DecodeHookFunc that applies
|
||||
// strings to the UnmarshalText function, when the target type
|
||||
// implements the encoding.TextUnmarshaler interface
|
||||
func TextUnmarshallerHookFunc() DecodeHookFuncType {
|
||||
return func(
|
||||
f reflect.Type,
|
||||
t reflect.Type,
|
||||
data interface{}) (interface{}, error) {
|
||||
if f.Kind() != reflect.String {
|
||||
return data, nil
|
||||
}
|
||||
result := reflect.New(t).Interface()
|
||||
unmarshaller, ok := result.(encoding.TextUnmarshaler)
|
||||
if !ok {
|
||||
return data, nil
|
||||
}
|
||||
if err := unmarshaller.UnmarshalText([]byte(data.(string))); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
}
|
|
@ -0,0 +1,567 @@
|
|||
package mapstructure
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math/big"
|
||||
"net"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestComposeDecodeHookFunc(t *testing.T) {
|
||||
f1 := func(
|
||||
f reflect.Kind,
|
||||
t reflect.Kind,
|
||||
data interface{}) (interface{}, error) {
|
||||
return data.(string) + "foo", nil
|
||||
}
|
||||
|
||||
f2 := func(
|
||||
f reflect.Kind,
|
||||
t reflect.Kind,
|
||||
data interface{}) (interface{}, error) {
|
||||
return data.(string) + "bar", nil
|
||||
}
|
||||
|
||||
f := ComposeDecodeHookFunc(f1, f2)
|
||||
|
||||
result, err := DecodeHookExec(
|
||||
f, reflect.ValueOf(""), reflect.ValueOf([]byte("")))
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
if result.(string) != "foobar" {
|
||||
t.Fatalf("bad: %#v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestComposeDecodeHookFunc_err(t *testing.T) {
|
||||
f1 := func(reflect.Kind, reflect.Kind, interface{}) (interface{}, error) {
|
||||
return nil, errors.New("foo")
|
||||
}
|
||||
|
||||
f2 := func(reflect.Kind, reflect.Kind, interface{}) (interface{}, error) {
|
||||
panic("NOPE")
|
||||
}
|
||||
|
||||
f := ComposeDecodeHookFunc(f1, f2)
|
||||
|
||||
_, err := DecodeHookExec(
|
||||
f, reflect.ValueOf(""), reflect.ValueOf([]byte("")))
|
||||
if err.Error() != "foo" {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestComposeDecodeHookFunc_kinds(t *testing.T) {
|
||||
var f2From reflect.Kind
|
||||
|
||||
f1 := func(
|
||||
f reflect.Kind,
|
||||
t reflect.Kind,
|
||||
data interface{}) (interface{}, error) {
|
||||
return int(42), nil
|
||||
}
|
||||
|
||||
f2 := func(
|
||||
f reflect.Kind,
|
||||
t reflect.Kind,
|
||||
data interface{}) (interface{}, error) {
|
||||
f2From = f
|
||||
return data, nil
|
||||
}
|
||||
|
||||
f := ComposeDecodeHookFunc(f1, f2)
|
||||
|
||||
_, err := DecodeHookExec(
|
||||
f, reflect.ValueOf(""), reflect.ValueOf([]byte("")))
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
if f2From != reflect.Int {
|
||||
t.Fatalf("bad: %#v", f2From)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrComposeDecodeHookFunc(t *testing.T) {
|
||||
f1 := func(
|
||||
f reflect.Kind,
|
||||
t reflect.Kind,
|
||||
data interface{}) (interface{}, error) {
|
||||
return data.(string) + "foo", nil
|
||||
}
|
||||
|
||||
f2 := func(
|
||||
f reflect.Kind,
|
||||
t reflect.Kind,
|
||||
data interface{}) (interface{}, error) {
|
||||
return data.(string) + "bar", nil
|
||||
}
|
||||
|
||||
f := OrComposeDecodeHookFunc(f1, f2)
|
||||
|
||||
result, err := DecodeHookExec(
|
||||
f, reflect.ValueOf(""), reflect.ValueOf([]byte("")))
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
if result.(string) != "foo" {
|
||||
t.Fatalf("bad: %#v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrComposeDecodeHookFunc_correctValueIsLast(t *testing.T) {
|
||||
f1 := func(
|
||||
f reflect.Kind,
|
||||
t reflect.Kind,
|
||||
data interface{}) (interface{}, error) {
|
||||
return nil, errors.New("f1 error")
|
||||
}
|
||||
|
||||
f2 := func(
|
||||
f reflect.Kind,
|
||||
t reflect.Kind,
|
||||
data interface{}) (interface{}, error) {
|
||||
return nil, errors.New("f2 error")
|
||||
}
|
||||
|
||||
f3 := func(
|
||||
f reflect.Kind,
|
||||
t reflect.Kind,
|
||||
data interface{}) (interface{}, error) {
|
||||
return data.(string) + "bar", nil
|
||||
}
|
||||
|
||||
f := OrComposeDecodeHookFunc(f1, f2, f3)
|
||||
|
||||
result, err := DecodeHookExec(
|
||||
f, reflect.ValueOf(""), reflect.ValueOf([]byte("")))
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
if result.(string) != "bar" {
|
||||
t.Fatalf("bad: %#v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrComposeDecodeHookFunc_err(t *testing.T) {
|
||||
f1 := func(
|
||||
f reflect.Kind,
|
||||
t reflect.Kind,
|
||||
data interface{}) (interface{}, error) {
|
||||
return nil, errors.New("f1 error")
|
||||
}
|
||||
|
||||
f2 := func(
|
||||
f reflect.Kind,
|
||||
t reflect.Kind,
|
||||
data interface{}) (interface{}, error) {
|
||||
return nil, errors.New("f2 error")
|
||||
}
|
||||
|
||||
f := OrComposeDecodeHookFunc(f1, f2)
|
||||
|
||||
_, err := DecodeHookExec(
|
||||
f, reflect.ValueOf(""), reflect.ValueOf([]byte("")))
|
||||
if err == nil {
|
||||
t.Fatalf("bad: should return an error")
|
||||
}
|
||||
if err.Error() != "f1 error\nf2 error\n" {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestComposeDecodeHookFunc_safe_nofuncs(t *testing.T) {
|
||||
f := ComposeDecodeHookFunc()
|
||||
type myStruct2 struct {
|
||||
MyInt int
|
||||
}
|
||||
|
||||
type myStruct1 struct {
|
||||
Blah map[string]myStruct2
|
||||
}
|
||||
|
||||
src := &myStruct1{Blah: map[string]myStruct2{
|
||||
"test": {
|
||||
MyInt: 1,
|
||||
},
|
||||
}}
|
||||
|
||||
dst := &myStruct1{}
|
||||
dConf := &DecoderConfig{
|
||||
Result: dst,
|
||||
ErrorUnused: true,
|
||||
DecodeHook: f,
|
||||
}
|
||||
d, err := NewDecoder(dConf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = d.Decode(src)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringToSliceHookFunc(t *testing.T) {
|
||||
f := StringToSliceHookFunc(",")
|
||||
|
||||
strValue := reflect.ValueOf("42")
|
||||
sliceValue := reflect.ValueOf([]byte("42"))
|
||||
cases := []struct {
|
||||
f, t reflect.Value
|
||||
result interface{}
|
||||
err bool
|
||||
}{
|
||||
{sliceValue, sliceValue, []byte("42"), false},
|
||||
{strValue, strValue, "42", false},
|
||||
{
|
||||
reflect.ValueOf("foo,bar,baz"),
|
||||
sliceValue,
|
||||
[]string{"foo", "bar", "baz"},
|
||||
false,
|
||||
},
|
||||
{
|
||||
reflect.ValueOf(""),
|
||||
sliceValue,
|
||||
[]string{},
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range cases {
|
||||
actual, err := DecodeHookExec(f, tc.f, tc.t)
|
||||
if tc.err != (err != nil) {
|
||||
t.Fatalf("case %d: expected jderr %#v", i, tc.err)
|
||||
}
|
||||
if !reflect.DeepEqual(actual, tc.result) {
|
||||
t.Fatalf(
|
||||
"case %d: expected %#v, got %#v",
|
||||
i, tc.result, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringToTimeDurationHookFunc(t *testing.T) {
|
||||
f := StringToTimeDurationHookFunc()
|
||||
|
||||
timeValue := reflect.ValueOf(time.Duration(5))
|
||||
strValue := reflect.ValueOf("")
|
||||
cases := []struct {
|
||||
f, t reflect.Value
|
||||
result interface{}
|
||||
err bool
|
||||
}{
|
||||
{reflect.ValueOf("5s"), timeValue, 5 * time.Second, false},
|
||||
{reflect.ValueOf("5"), timeValue, time.Duration(0), true},
|
||||
{reflect.ValueOf("5"), strValue, "5", false},
|
||||
}
|
||||
|
||||
for i, tc := range cases {
|
||||
actual, err := DecodeHookExec(f, tc.f, tc.t)
|
||||
if tc.err != (err != nil) {
|
||||
t.Fatalf("case %d: expected jderr %#v", i, tc.err)
|
||||
}
|
||||
if !reflect.DeepEqual(actual, tc.result) {
|
||||
t.Fatalf(
|
||||
"case %d: expected %#v, got %#v",
|
||||
i, tc.result, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringToTimeHookFunc(t *testing.T) {
|
||||
strValue := reflect.ValueOf("5")
|
||||
timeValue := reflect.ValueOf(time.Time{})
|
||||
cases := []struct {
|
||||
f, t reflect.Value
|
||||
layout string
|
||||
result interface{}
|
||||
err bool
|
||||
}{
|
||||
{reflect.ValueOf("2006-01-02T15:04:05Z"), timeValue, time.RFC3339,
|
||||
time.Date(2006, 1, 2, 15, 4, 5, 0, time.UTC), false},
|
||||
{strValue, timeValue, time.RFC3339, time.Time{}, true},
|
||||
{strValue, strValue, time.RFC3339, "5", false},
|
||||
}
|
||||
|
||||
for i, tc := range cases {
|
||||
f := StringToTimeHookFunc(tc.layout)
|
||||
actual, err := DecodeHookExec(f, tc.f, tc.t)
|
||||
if tc.err != (err != nil) {
|
||||
t.Fatalf("case %d: expected jderr %#v", i, tc.err)
|
||||
}
|
||||
if !reflect.DeepEqual(actual, tc.result) {
|
||||
t.Fatalf(
|
||||
"case %d: expected %#v, got %#v",
|
||||
i, tc.result, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringToIPHookFunc(t *testing.T) {
|
||||
strValue := reflect.ValueOf("5")
|
||||
ipValue := reflect.ValueOf(net.IP{})
|
||||
cases := []struct {
|
||||
f, t reflect.Value
|
||||
result interface{}
|
||||
err bool
|
||||
}{
|
||||
{reflect.ValueOf("1.2.3.4"), ipValue,
|
||||
net.IPv4(0x01, 0x02, 0x03, 0x04), false},
|
||||
{strValue, ipValue, net.IP{}, true},
|
||||
{strValue, strValue, "5", false},
|
||||
}
|
||||
|
||||
for i, tc := range cases {
|
||||
f := StringToIPHookFunc()
|
||||
actual, err := DecodeHookExec(f, tc.f, tc.t)
|
||||
if tc.err != (err != nil) {
|
||||
t.Fatalf("case %d: expected jderr %#v", i, tc.err)
|
||||
}
|
||||
if !reflect.DeepEqual(actual, tc.result) {
|
||||
t.Fatalf(
|
||||
"case %d: expected %#v, got %#v",
|
||||
i, tc.result, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringToIPNetHookFunc(t *testing.T) {
|
||||
strValue := reflect.ValueOf("5")
|
||||
ipNetValue := reflect.ValueOf(net.IPNet{})
|
||||
var nilNet *net.IPNet = nil
|
||||
|
||||
cases := []struct {
|
||||
f, t reflect.Value
|
||||
result interface{}
|
||||
err bool
|
||||
}{
|
||||
{reflect.ValueOf("1.2.3.4/24"), ipNetValue,
|
||||
&net.IPNet{
|
||||
IP: net.IP{0x01, 0x02, 0x03, 0x00},
|
||||
Mask: net.IPv4Mask(0xff, 0xff, 0xff, 0x00),
|
||||
}, false},
|
||||
{strValue, ipNetValue, nilNet, true},
|
||||
{strValue, strValue, "5", false},
|
||||
}
|
||||
|
||||
for i, tc := range cases {
|
||||
f := StringToIPNetHookFunc()
|
||||
actual, err := DecodeHookExec(f, tc.f, tc.t)
|
||||
if tc.err != (err != nil) {
|
||||
t.Fatalf("case %d: expected jderr %#v", i, tc.err)
|
||||
}
|
||||
if !reflect.DeepEqual(actual, tc.result) {
|
||||
t.Fatalf(
|
||||
"case %d: expected %#v, got %#v",
|
||||
i, tc.result, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWeaklyTypedHook(t *testing.T) {
|
||||
var f DecodeHookFunc = WeaklyTypedHook
|
||||
|
||||
strValue := reflect.ValueOf("")
|
||||
cases := []struct {
|
||||
f, t reflect.Value
|
||||
result interface{}
|
||||
err bool
|
||||
}{
|
||||
// TO STRING
|
||||
{
|
||||
reflect.ValueOf(false),
|
||||
strValue,
|
||||
"0",
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
reflect.ValueOf(true),
|
||||
strValue,
|
||||
"1",
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
reflect.ValueOf(float32(7)),
|
||||
strValue,
|
||||
"7",
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
reflect.ValueOf(int(7)),
|
||||
strValue,
|
||||
"7",
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
reflect.ValueOf([]uint8("foo")),
|
||||
strValue,
|
||||
"foo",
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
reflect.ValueOf(uint(7)),
|
||||
strValue,
|
||||
"7",
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range cases {
|
||||
actual, err := DecodeHookExec(f, tc.f, tc.t)
|
||||
if tc.err != (err != nil) {
|
||||
t.Fatalf("case %d: expected jderr %#v", i, tc.err)
|
||||
}
|
||||
if !reflect.DeepEqual(actual, tc.result) {
|
||||
t.Fatalf(
|
||||
"case %d: expected %#v, got %#v",
|
||||
i, tc.result, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStructToMapHookFuncTabled(t *testing.T) {
|
||||
var f DecodeHookFunc = RecursiveStructToMapHookFunc()
|
||||
|
||||
type b struct {
|
||||
TestKey string
|
||||
}
|
||||
|
||||
type a struct {
|
||||
Sub b
|
||||
}
|
||||
|
||||
testStruct := a{
|
||||
Sub: b{
|
||||
TestKey: "testval",
|
||||
},
|
||||
}
|
||||
|
||||
testMap := map[string]interface{}{
|
||||
"Sub": map[string]interface{}{
|
||||
"TestKey": "testval",
|
||||
},
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
receiver interface{}
|
||||
input interface{}
|
||||
expected interface{}
|
||||
err bool
|
||||
}{
|
||||
{
|
||||
"map receiver",
|
||||
func() interface{} {
|
||||
var res map[string]interface{}
|
||||
return &res
|
||||
}(),
|
||||
testStruct,
|
||||
&testMap,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"interface receiver",
|
||||
func() interface{} {
|
||||
var res interface{}
|
||||
return &res
|
||||
}(),
|
||||
testStruct,
|
||||
func() interface{} {
|
||||
var exp interface{} = testMap
|
||||
return &exp
|
||||
}(),
|
||||
false,
|
||||
},
|
||||
{
|
||||
"slice receiver errors",
|
||||
func() interface{} {
|
||||
var res []string
|
||||
return &res
|
||||
}(),
|
||||
testStruct,
|
||||
new([]string),
|
||||
true,
|
||||
},
|
||||
{
|
||||
"slice to slice - no change",
|
||||
func() interface{} {
|
||||
var res []string
|
||||
return &res
|
||||
}(),
|
||||
[]string{"a", "b"},
|
||||
&[]string{"a", "b"},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"string to string - no change",
|
||||
func() interface{} {
|
||||
var res string
|
||||
return &res
|
||||
}(),
|
||||
"test",
|
||||
func() *string {
|
||||
s := "test"
|
||||
return &s
|
||||
}(),
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cfg := &DecoderConfig{
|
||||
DecodeHook: f,
|
||||
Result: tc.receiver,
|
||||
}
|
||||
|
||||
d, err := NewDecoder(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected jderr %#v", err)
|
||||
}
|
||||
|
||||
err = d.Decode(tc.input)
|
||||
if tc.err != (err != nil) {
|
||||
t.Fatalf("expected jderr %#v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(tc.expected, tc.receiver) {
|
||||
t.Fatalf("expected %#v, got %#v",
|
||||
tc.expected, tc.receiver)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestTextUnmarshallerHookFunc(t *testing.T) {
|
||||
cases := []struct {
|
||||
f, t reflect.Value
|
||||
result interface{}
|
||||
err bool
|
||||
}{
|
||||
{reflect.ValueOf("42"), reflect.ValueOf(big.Int{}), big.NewInt(42), false},
|
||||
{reflect.ValueOf("invalid"), reflect.ValueOf(big.Int{}), nil, true},
|
||||
{reflect.ValueOf("5"), reflect.ValueOf("5"), "5", false},
|
||||
}
|
||||
|
||||
for i, tc := range cases {
|
||||
f := TextUnmarshallerHookFunc()
|
||||
actual, err := DecodeHookExec(f, tc.f, tc.t)
|
||||
if tc.err != (err != nil) {
|
||||
t.Fatalf("case %d: expected jderr %#v", i, tc.err)
|
||||
}
|
||||
if !reflect.DeepEqual(actual, tc.result) {
|
||||
t.Fatalf(
|
||||
"case %d: expected %#v, got %#v",
|
||||
i, tc.result, actual)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package mapstructure
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Error implements the error interface and can represents multiple
|
||||
// errors that occur in the course of a single decode.
|
||||
type Error struct {
|
||||
Errors []string
|
||||
}
|
||||
|
||||
func (e *Error) Error() string {
|
||||
points := make([]string, len(e.Errors))
|
||||
for i, err := range e.Errors {
|
||||
points[i] = fmt.Sprintf("* %s", err)
|
||||
}
|
||||
|
||||
sort.Strings(points)
|
||||
return fmt.Sprintf(
|
||||
"%d error(s) decoding:\n\n%s",
|
||||
len(e.Errors), strings.Join(points, "\n"))
|
||||
}
|
||||
|
||||
// WrappedErrors implements the errwrap.Wrapper interface to make this
|
||||
// return value more useful with the errwrap and go-multierror libraries.
|
||||
func (e *Error) WrappedErrors() []error {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make([]error, len(e.Errors))
|
||||
for i, e := range e.Errors {
|
||||
result[i] = errors.New(e)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func appendErrors(errors []string, err error) []string {
|
||||
switch e := err.(type) {
|
||||
case *Error:
|
||||
return append(errors, e.Errors...)
|
||||
default:
|
||||
return append(errors, e.Error())
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,285 @@
|
|||
package mapstructure
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
Emails []string
|
||||
Extra map[string]string
|
||||
}
|
||||
|
||||
func Benchmark_Decode(b *testing.B) {
|
||||
input := map[string]interface{}{
|
||||
"name": "Mitchell",
|
||||
"age": 91,
|
||||
"emails": []string{"one", "two", "three"},
|
||||
"extra": map[string]string{
|
||||
"twitter": "mitchellh",
|
||||
},
|
||||
}
|
||||
|
||||
var result Person
|
||||
for i := 0; i < b.N; i++ {
|
||||
Decode(input, &result)
|
||||
}
|
||||
}
|
||||
|
||||
// decodeViaJSON takes the map data and passes it through encoding/json to convert it into the
|
||||
// given Go native structure pointed to by v. v must be a pointer to a struct.
|
||||
func decodeViaJSON(data interface{}, v interface{}) error {
|
||||
// Perform the task by simply marshalling the input into JSON,
|
||||
// then unmarshalling it into target native Go struct.
|
||||
b, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(b, v)
|
||||
}
|
||||
|
||||
func Benchmark_DecodeViaJSON(b *testing.B) {
|
||||
input := map[string]interface{}{
|
||||
"name": "Mitchell",
|
||||
"age": 91,
|
||||
"emails": []string{"one", "two", "three"},
|
||||
"extra": map[string]string{
|
||||
"twitter": "mitchellh",
|
||||
},
|
||||
}
|
||||
|
||||
var result Person
|
||||
for i := 0; i < b.N; i++ {
|
||||
decodeViaJSON(input, &result)
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_JSONUnmarshal(b *testing.B) {
|
||||
input := map[string]interface{}{
|
||||
"name": "Mitchell",
|
||||
"age": 91,
|
||||
"emails": []string{"one", "two", "three"},
|
||||
"extra": map[string]string{
|
||||
"twitter": "mitchellh",
|
||||
},
|
||||
}
|
||||
|
||||
inputB, err := json.Marshal(input)
|
||||
if err != nil {
|
||||
b.Fatal("Failed to marshal test input:", err)
|
||||
}
|
||||
|
||||
var result Person
|
||||
for i := 0; i < b.N; i++ {
|
||||
json.Unmarshal(inputB, &result)
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_DecodeBasic(b *testing.B) {
|
||||
input := map[string]interface{}{
|
||||
"vstring": "foo",
|
||||
"vint": 42,
|
||||
"Vuint": 42,
|
||||
"vbool": true,
|
||||
"Vfloat": 42.42,
|
||||
"vsilent": true,
|
||||
"vdata": 42,
|
||||
"vjsonInt": json.Number("1234"),
|
||||
"vjsonFloat": json.Number("1234.5"),
|
||||
"vjsonNumber": json.Number("1234.5"),
|
||||
}
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
var result Basic
|
||||
Decode(input, &result)
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_DecodeEmbedded(b *testing.B) {
|
||||
input := map[string]interface{}{
|
||||
"vstring": "foo",
|
||||
"Basic": map[string]interface{}{
|
||||
"vstring": "innerfoo",
|
||||
},
|
||||
"vunique": "bar",
|
||||
}
|
||||
|
||||
var result Embedded
|
||||
for i := 0; i < b.N; i++ {
|
||||
Decode(input, &result)
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_DecodeTypeConversion(b *testing.B) {
|
||||
input := map[string]interface{}{
|
||||
"IntToFloat": 42,
|
||||
"IntToUint": 42,
|
||||
"IntToBool": 1,
|
||||
"IntToString": 42,
|
||||
"UintToInt": 42,
|
||||
"UintToFloat": 42,
|
||||
"UintToBool": 42,
|
||||
"UintToString": 42,
|
||||
"BoolToInt": true,
|
||||
"BoolToUint": true,
|
||||
"BoolToFloat": true,
|
||||
"BoolToString": true,
|
||||
"FloatToInt": 42.42,
|
||||
"FloatToUint": 42.42,
|
||||
"FloatToBool": 42.42,
|
||||
"FloatToString": 42.42,
|
||||
"StringToInt": "42",
|
||||
"StringToUint": "42",
|
||||
"StringToBool": "1",
|
||||
"StringToFloat": "42.42",
|
||||
"SliceToMap": []interface{}{},
|
||||
"MapToSlice": map[string]interface{}{},
|
||||
}
|
||||
|
||||
var resultStrict TypeConversionResult
|
||||
for i := 0; i < b.N; i++ {
|
||||
Decode(input, &resultStrict)
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_DecodeMap(b *testing.B) {
|
||||
input := map[string]interface{}{
|
||||
"vfoo": "foo",
|
||||
"vother": map[interface{}]interface{}{
|
||||
"foo": "foo",
|
||||
"bar": "bar",
|
||||
},
|
||||
}
|
||||
|
||||
var result Map
|
||||
for i := 0; i < b.N; i++ {
|
||||
Decode(input, &result)
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_DecodeMapOfStruct(b *testing.B) {
|
||||
input := map[string]interface{}{
|
||||
"value": map[string]interface{}{
|
||||
"foo": map[string]string{"vstring": "one"},
|
||||
"bar": map[string]string{"vstring": "two"},
|
||||
},
|
||||
}
|
||||
|
||||
var result MapOfStruct
|
||||
for i := 0; i < b.N; i++ {
|
||||
Decode(input, &result)
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_DecodeSlice(b *testing.B) {
|
||||
input := map[string]interface{}{
|
||||
"vfoo": "foo",
|
||||
"vbar": []string{"foo", "bar", "baz"},
|
||||
}
|
||||
|
||||
var result Slice
|
||||
for i := 0; i < b.N; i++ {
|
||||
Decode(input, &result)
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_DecodeSliceOfStruct(b *testing.B) {
|
||||
input := map[string]interface{}{
|
||||
"value": []map[string]interface{}{
|
||||
{"vstring": "one"},
|
||||
{"vstring": "two"},
|
||||
},
|
||||
}
|
||||
|
||||
var result SliceOfStruct
|
||||
for i := 0; i < b.N; i++ {
|
||||
Decode(input, &result)
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_DecodeWeaklyTypedInput(b *testing.B) {
|
||||
// This input can come from anywhere, but typically comes from
|
||||
// something like decoding JSON, generated by a weakly typed language
|
||||
// such as PHP.
|
||||
input := map[string]interface{}{
|
||||
"name": 123, // number => string
|
||||
"age": "42", // string => number
|
||||
"emails": map[string]interface{}{}, // empty map => empty array
|
||||
}
|
||||
|
||||
var result Person
|
||||
config := &DecoderConfig{
|
||||
WeaklyTypedInput: true,
|
||||
Result: &result,
|
||||
}
|
||||
|
||||
decoder, err := NewDecoder(config)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
decoder.Decode(input)
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_DecodeMetadata(b *testing.B) {
|
||||
input := map[string]interface{}{
|
||||
"name": "Mitchell",
|
||||
"age": 91,
|
||||
"email": "foo@bar.com",
|
||||
}
|
||||
|
||||
var md Metadata
|
||||
var result Person
|
||||
config := &DecoderConfig{
|
||||
Metadata: &md,
|
||||
Result: &result,
|
||||
}
|
||||
|
||||
decoder, err := NewDecoder(config)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
decoder.Decode(input)
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_DecodeMetadataEmbedded(b *testing.B) {
|
||||
input := map[string]interface{}{
|
||||
"vstring": "foo",
|
||||
"vunique": "bar",
|
||||
}
|
||||
|
||||
var md Metadata
|
||||
var result EmbeddedSquash
|
||||
config := &DecoderConfig{
|
||||
Metadata: &md,
|
||||
Result: &result,
|
||||
}
|
||||
|
||||
decoder, err := NewDecoder(config)
|
||||
if err != nil {
|
||||
b.Fatalf("jderr: %s", err)
|
||||
}
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
decoder.Decode(input)
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_DecodeTagged(b *testing.B) {
|
||||
input := map[string]interface{}{
|
||||
"foo": "bar",
|
||||
"bar": "value",
|
||||
}
|
||||
|
||||
var result Tagged
|
||||
for i := 0; i < b.N; i++ {
|
||||
Decode(input, &result)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,627 @@
|
|||
package mapstructure
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GH-1, GH-10, GH-96
|
||||
func TestDecode_NilValue(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
in interface{}
|
||||
target interface{}
|
||||
out interface{}
|
||||
metaKeys []string
|
||||
metaUnused []string
|
||||
}{
|
||||
{
|
||||
"all nil",
|
||||
&map[string]interface{}{
|
||||
"vfoo": nil,
|
||||
"vother": nil,
|
||||
},
|
||||
&Map{Vfoo: "foo", Vother: map[string]string{"foo": "bar"}},
|
||||
&Map{Vfoo: "", Vother: nil},
|
||||
[]string{"Vfoo", "Vother"},
|
||||
[]string{},
|
||||
},
|
||||
{
|
||||
"partial nil",
|
||||
&map[string]interface{}{
|
||||
"vfoo": "baz",
|
||||
"vother": nil,
|
||||
},
|
||||
&Map{Vfoo: "foo", Vother: map[string]string{"foo": "bar"}},
|
||||
&Map{Vfoo: "baz", Vother: nil},
|
||||
[]string{"Vfoo", "Vother"},
|
||||
[]string{},
|
||||
},
|
||||
{
|
||||
"partial decode",
|
||||
&map[string]interface{}{
|
||||
"vother": nil,
|
||||
},
|
||||
&Map{Vfoo: "foo", Vother: map[string]string{"foo": "bar"}},
|
||||
&Map{Vfoo: "foo", Vother: nil},
|
||||
[]string{"Vother"},
|
||||
[]string{},
|
||||
},
|
||||
{
|
||||
"unused values",
|
||||
&map[string]interface{}{
|
||||
"vbar": "bar",
|
||||
"vfoo": nil,
|
||||
"vother": nil,
|
||||
},
|
||||
&Map{Vfoo: "foo", Vother: map[string]string{"foo": "bar"}},
|
||||
&Map{Vfoo: "", Vother: nil},
|
||||
[]string{"Vfoo", "Vother"},
|
||||
[]string{"vbar"},
|
||||
},
|
||||
{
|
||||
"map interface all nil",
|
||||
&map[interface{}]interface{}{
|
||||
"vfoo": nil,
|
||||
"vother": nil,
|
||||
},
|
||||
&Map{Vfoo: "foo", Vother: map[string]string{"foo": "bar"}},
|
||||
&Map{Vfoo: "", Vother: nil},
|
||||
[]string{"Vfoo", "Vother"},
|
||||
[]string{},
|
||||
},
|
||||
{
|
||||
"map interface partial nil",
|
||||
&map[interface{}]interface{}{
|
||||
"vfoo": "baz",
|
||||
"vother": nil,
|
||||
},
|
||||
&Map{Vfoo: "foo", Vother: map[string]string{"foo": "bar"}},
|
||||
&Map{Vfoo: "baz", Vother: nil},
|
||||
[]string{"Vfoo", "Vother"},
|
||||
[]string{},
|
||||
},
|
||||
{
|
||||
"map interface partial decode",
|
||||
&map[interface{}]interface{}{
|
||||
"vother": nil,
|
||||
},
|
||||
&Map{Vfoo: "foo", Vother: map[string]string{"foo": "bar"}},
|
||||
&Map{Vfoo: "foo", Vother: nil},
|
||||
[]string{"Vother"},
|
||||
[]string{},
|
||||
},
|
||||
{
|
||||
"map interface unused values",
|
||||
&map[interface{}]interface{}{
|
||||
"vbar": "bar",
|
||||
"vfoo": nil,
|
||||
"vother": nil,
|
||||
},
|
||||
&Map{Vfoo: "foo", Vother: map[string]string{"foo": "bar"}},
|
||||
&Map{Vfoo: "", Vother: nil},
|
||||
[]string{"Vfoo", "Vother"},
|
||||
[]string{"vbar"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
config := &DecoderConfig{
|
||||
Metadata: new(Metadata),
|
||||
Result: tc.target,
|
||||
ZeroFields: true,
|
||||
}
|
||||
|
||||
decoder, err := NewDecoder(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not error: %s", err)
|
||||
}
|
||||
|
||||
err = decoder.Decode(tc.in)
|
||||
if err != nil {
|
||||
t.Fatalf("should not error: %s", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(tc.out, tc.target) {
|
||||
t.Fatalf("%q: TestDecode_NilValue() expected: %#v, got: %#v", tc.name, tc.out, tc.target)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(tc.metaKeys, config.Metadata.Keys) {
|
||||
t.Fatalf("%q: Metadata.Keys mismatch expected: %#v, got: %#v", tc.name, tc.metaKeys, config.Metadata.Keys)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(tc.metaUnused, config.Metadata.Unused) {
|
||||
t.Fatalf("%q: Metadata.Unused mismatch expected: %#v, got: %#v", tc.name, tc.metaUnused, config.Metadata.Unused)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// #48
|
||||
func TestNestedTypePointerWithDefaults(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
input := map[string]interface{}{
|
||||
"vfoo": "foo",
|
||||
"vbar": map[string]interface{}{
|
||||
"vstring": "foo",
|
||||
"vint": 42,
|
||||
"vbool": true,
|
||||
},
|
||||
}
|
||||
|
||||
result := NestedPointer{
|
||||
Vbar: &Basic{
|
||||
Vuint: 42,
|
||||
},
|
||||
}
|
||||
err := Decode(input, &result)
|
||||
if err != nil {
|
||||
t.Fatalf("got an jderr: %s", err.Error())
|
||||
}
|
||||
|
||||
if result.Vfoo != "foo" {
|
||||
t.Errorf("vfoo value should be 'foo': %#v", result.Vfoo)
|
||||
}
|
||||
|
||||
if result.Vbar.Vstring != "foo" {
|
||||
t.Errorf("vstring value should be 'foo': %#v", result.Vbar.Vstring)
|
||||
}
|
||||
|
||||
if result.Vbar.Vint != 42 {
|
||||
t.Errorf("vint value should be 42: %#v", result.Vbar.Vint)
|
||||
}
|
||||
|
||||
if result.Vbar.Vbool != true {
|
||||
t.Errorf("vbool value should be true: %#v", result.Vbar.Vbool)
|
||||
}
|
||||
|
||||
if result.Vbar.Vextra != "" {
|
||||
t.Errorf("vextra value should be empty: %#v", result.Vbar.Vextra)
|
||||
}
|
||||
|
||||
// this is the error
|
||||
if result.Vbar.Vuint != 42 {
|
||||
t.Errorf("vuint value should be 42: %#v", result.Vbar.Vuint)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type NestedSlice struct {
|
||||
Vfoo string
|
||||
Vbars []Basic
|
||||
Vempty []Basic
|
||||
}
|
||||
|
||||
// #48
|
||||
func TestNestedTypeSliceWithDefaults(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
input := map[string]interface{}{
|
||||
"vfoo": "foo",
|
||||
"vbars": []map[string]interface{}{
|
||||
{"vstring": "foo", "vint": 42, "vbool": true},
|
||||
{"vint": 42, "vbool": true},
|
||||
},
|
||||
"vempty": []map[string]interface{}{
|
||||
{"vstring": "foo", "vint": 42, "vbool": true},
|
||||
{"vint": 42, "vbool": true},
|
||||
},
|
||||
}
|
||||
|
||||
result := NestedSlice{
|
||||
Vbars: []Basic{
|
||||
{Vuint: 42},
|
||||
{Vstring: "foo"},
|
||||
},
|
||||
}
|
||||
err := Decode(input, &result)
|
||||
if err != nil {
|
||||
t.Fatalf("got an jderr: %s", err.Error())
|
||||
}
|
||||
|
||||
if result.Vfoo != "foo" {
|
||||
t.Errorf("vfoo value should be 'foo': %#v", result.Vfoo)
|
||||
}
|
||||
|
||||
if result.Vbars[0].Vstring != "foo" {
|
||||
t.Errorf("vstring value should be 'foo': %#v", result.Vbars[0].Vstring)
|
||||
}
|
||||
// this is the error
|
||||
if result.Vbars[0].Vuint != 42 {
|
||||
t.Errorf("vuint value should be 42: %#v", result.Vbars[0].Vuint)
|
||||
}
|
||||
}
|
||||
|
||||
// #48 workaround
|
||||
func TestNestedTypeWithDefaults(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
input := map[string]interface{}{
|
||||
"vfoo": "foo",
|
||||
"vbar": map[string]interface{}{
|
||||
"vstring": "foo",
|
||||
"vint": 42,
|
||||
"vbool": true,
|
||||
},
|
||||
}
|
||||
|
||||
result := Nested{
|
||||
Vbar: Basic{
|
||||
Vuint: 42,
|
||||
},
|
||||
}
|
||||
err := Decode(input, &result)
|
||||
if err != nil {
|
||||
t.Fatalf("got an jderr: %s", err.Error())
|
||||
}
|
||||
|
||||
if result.Vfoo != "foo" {
|
||||
t.Errorf("vfoo value should be 'foo': %#v", result.Vfoo)
|
||||
}
|
||||
|
||||
if result.Vbar.Vstring != "foo" {
|
||||
t.Errorf("vstring value should be 'foo': %#v", result.Vbar.Vstring)
|
||||
}
|
||||
|
||||
if result.Vbar.Vint != 42 {
|
||||
t.Errorf("vint value should be 42: %#v", result.Vbar.Vint)
|
||||
}
|
||||
|
||||
if result.Vbar.Vbool != true {
|
||||
t.Errorf("vbool value should be true: %#v", result.Vbar.Vbool)
|
||||
}
|
||||
|
||||
if result.Vbar.Vextra != "" {
|
||||
t.Errorf("vextra value should be empty: %#v", result.Vbar.Vextra)
|
||||
}
|
||||
|
||||
// this is the error
|
||||
if result.Vbar.Vuint != 42 {
|
||||
t.Errorf("vuint value should be 42: %#v", result.Vbar.Vuint)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// #67 panic() on extending slices (decodeSlice with disabled ZeroValues)
|
||||
func TestDecodeSliceToEmptySliceWOZeroing(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type TestStruct struct {
|
||||
Vfoo []string
|
||||
}
|
||||
|
||||
decode := func(m interface{}, rawVal interface{}) error {
|
||||
config := &DecoderConfig{
|
||||
Metadata: nil,
|
||||
Result: rawVal,
|
||||
ZeroFields: false,
|
||||
}
|
||||
|
||||
decoder, err := NewDecoder(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return decoder.Decode(m)
|
||||
}
|
||||
|
||||
{
|
||||
input := map[string]interface{}{
|
||||
"vfoo": []string{"1"},
|
||||
}
|
||||
|
||||
result := &TestStruct{}
|
||||
|
||||
err := decode(input, &result)
|
||||
if err != nil {
|
||||
t.Fatalf("got an jderr: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
input := map[string]interface{}{
|
||||
"vfoo": []string{"1"},
|
||||
}
|
||||
|
||||
result := &TestStruct{
|
||||
Vfoo: []string{},
|
||||
}
|
||||
|
||||
err := decode(input, &result)
|
||||
if err != nil {
|
||||
t.Fatalf("got an jderr: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
input := map[string]interface{}{
|
||||
"vfoo": []string{"2", "3"},
|
||||
}
|
||||
|
||||
result := &TestStruct{
|
||||
Vfoo: []string{"1"},
|
||||
}
|
||||
|
||||
err := decode(input, &result)
|
||||
if err != nil {
|
||||
t.Fatalf("got an jderr: %s", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// #70
|
||||
func TestNextSquashMapstructure(t *testing.T) {
|
||||
data := &struct {
|
||||
Level1 struct {
|
||||
Level2 struct {
|
||||
Foo string
|
||||
} `mapstructure:",squash"`
|
||||
} `mapstructure:",squash"`
|
||||
}{}
|
||||
err := Decode(map[interface{}]interface{}{"foo": "baz"}, &data)
|
||||
if err != nil {
|
||||
t.Fatalf("should not error: %s", err)
|
||||
}
|
||||
if data.Level1.Level2.Foo != "baz" {
|
||||
t.Fatal("value should be baz")
|
||||
}
|
||||
}
|
||||
|
||||
type ImplementsInterfacePointerReceiver struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func (i *ImplementsInterfacePointerReceiver) DoStuff() {}
|
||||
|
||||
type ImplementsInterfaceValueReceiver string
|
||||
|
||||
func (i ImplementsInterfaceValueReceiver) DoStuff() {}
|
||||
|
||||
// GH-140 Type error when using DecodeHook to decode into interface
|
||||
func TestDecode_DecodeHookInterface(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type Interface interface {
|
||||
DoStuff()
|
||||
}
|
||||
type DecodeIntoInterface struct {
|
||||
Test Interface
|
||||
}
|
||||
|
||||
testData := map[string]string{"test": "test"}
|
||||
|
||||
stringToPointerInterfaceDecodeHook := func(from, to reflect.Type, data interface{}) (interface{}, error) {
|
||||
if from.Kind() != reflect.String {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
if to != reflect.TypeOf((*Interface)(nil)).Elem() {
|
||||
return data, nil
|
||||
}
|
||||
// Ensure interface is satisfied
|
||||
var impl Interface = &ImplementsInterfacePointerReceiver{data.(string)}
|
||||
return impl, nil
|
||||
}
|
||||
|
||||
stringToValueInterfaceDecodeHook := func(from, to reflect.Type, data interface{}) (interface{}, error) {
|
||||
if from.Kind() != reflect.String {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
if to != reflect.TypeOf((*Interface)(nil)).Elem() {
|
||||
return data, nil
|
||||
}
|
||||
// Ensure interface is satisfied
|
||||
var impl Interface = ImplementsInterfaceValueReceiver(data.(string))
|
||||
return impl, nil
|
||||
}
|
||||
|
||||
{
|
||||
decodeInto := new(DecodeIntoInterface)
|
||||
|
||||
decoder, _ := NewDecoder(&DecoderConfig{
|
||||
DecodeHook: stringToPointerInterfaceDecodeHook,
|
||||
Result: decodeInto,
|
||||
})
|
||||
|
||||
err := decoder.Decode(testData)
|
||||
if err != nil {
|
||||
t.Fatalf("Decode returned error: %s", err)
|
||||
}
|
||||
|
||||
expected := &ImplementsInterfacePointerReceiver{"test"}
|
||||
if !reflect.DeepEqual(decodeInto.Test, expected) {
|
||||
t.Fatalf("expected: %#v (%T), got: %#v (%T)", decodeInto.Test, decodeInto.Test, expected, expected)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
decodeInto := new(DecodeIntoInterface)
|
||||
|
||||
decoder, _ := NewDecoder(&DecoderConfig{
|
||||
DecodeHook: stringToValueInterfaceDecodeHook,
|
||||
Result: decodeInto,
|
||||
})
|
||||
|
||||
err := decoder.Decode(testData)
|
||||
if err != nil {
|
||||
t.Fatalf("Decode returned error: %s", err)
|
||||
}
|
||||
|
||||
expected := ImplementsInterfaceValueReceiver("test")
|
||||
if !reflect.DeepEqual(decodeInto.Test, expected) {
|
||||
t.Fatalf("expected: %#v (%T), got: %#v (%T)", decodeInto.Test, decodeInto.Test, expected, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// #103 Check for data type before trying to access its composants prevent a panic error
|
||||
// in decodeSlice
|
||||
func TestDecodeBadDataTypeInSlice(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
input := map[string]interface{}{
|
||||
"Toto": "titi",
|
||||
}
|
||||
result := []struct {
|
||||
Toto string
|
||||
}{}
|
||||
|
||||
if err := Decode(input, &result); err == nil {
|
||||
t.Error("An error was expected, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// #202 Ensure that intermediate maps in the struct -> struct decode process are settable
|
||||
// and not just the elements within them.
|
||||
func TestDecodeIntermediateMapsSettable(t *testing.T) {
|
||||
type Timestamp struct {
|
||||
Seconds int64
|
||||
Nanos int32
|
||||
}
|
||||
|
||||
type TsWrapper struct {
|
||||
Timestamp *Timestamp
|
||||
}
|
||||
|
||||
type TimeWrapper struct {
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
input := TimeWrapper{
|
||||
Timestamp: time.Unix(123456789, 987654),
|
||||
}
|
||||
|
||||
expected := TsWrapper{
|
||||
Timestamp: &Timestamp{
|
||||
Seconds: 123456789,
|
||||
Nanos: 987654,
|
||||
},
|
||||
}
|
||||
|
||||
timePtrType := reflect.TypeOf((*time.Time)(nil))
|
||||
mapStrInfType := reflect.TypeOf((map[string]interface{})(nil))
|
||||
|
||||
var actual TsWrapper
|
||||
decoder, err := NewDecoder(&DecoderConfig{
|
||||
Result: &actual,
|
||||
DecodeHook: func(from, to reflect.Type, data interface{}) (interface{}, error) {
|
||||
if from == timePtrType && to == mapStrInfType {
|
||||
ts := data.(*time.Time)
|
||||
nanos := ts.UnixNano()
|
||||
|
||||
seconds := nanos / 1000000000
|
||||
nanos = nanos % 1000000000
|
||||
|
||||
return &map[string]interface{}{
|
||||
"Seconds": seconds,
|
||||
"Nanos": int32(nanos),
|
||||
}, nil
|
||||
}
|
||||
return data, nil
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create decoder: %v", err)
|
||||
}
|
||||
|
||||
if err := decoder.Decode(&input); err != nil {
|
||||
t.Fatalf("failed to decode input: %v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(expected, actual) {
|
||||
t.Fatalf("expected: %#[1]v (%[1]T), got: %#[2]v (%[2]T)", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
// GH-206: decodeInt throws an error for an empty string
|
||||
func TestDecode_weakEmptyStringToInt(t *testing.T) {
|
||||
input := map[string]interface{}{
|
||||
"StringToInt": "",
|
||||
"StringToUint": "",
|
||||
"StringToBool": "",
|
||||
"StringToFloat": "",
|
||||
}
|
||||
|
||||
expectedResultWeak := TypeConversionResult{
|
||||
StringToInt: 0,
|
||||
StringToUint: 0,
|
||||
StringToBool: false,
|
||||
StringToFloat: 0,
|
||||
}
|
||||
|
||||
// Test weak type conversion
|
||||
var resultWeak TypeConversionResult
|
||||
err := WeakDecode(input, &resultWeak)
|
||||
if err != nil {
|
||||
t.Fatalf("got an jderr: %s", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(resultWeak, expectedResultWeak) {
|
||||
t.Errorf("expected \n%#v, got: \n%#v", expectedResultWeak, resultWeak)
|
||||
}
|
||||
}
|
||||
|
||||
// GH-228: Squash cause *time.Time set to zero
|
||||
func TestMapSquash(t *testing.T) {
|
||||
type AA struct {
|
||||
T *time.Time
|
||||
}
|
||||
type A struct {
|
||||
AA
|
||||
}
|
||||
|
||||
v := time.Now()
|
||||
in := &AA{
|
||||
T: &v,
|
||||
}
|
||||
out := &A{}
|
||||
d, err := NewDecoder(&DecoderConfig{
|
||||
Squash: true,
|
||||
Result: out,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("jderr: %s", err)
|
||||
}
|
||||
if err := d.Decode(in); err != nil {
|
||||
t.Fatalf("jderr: %s", err)
|
||||
}
|
||||
|
||||
// these failed
|
||||
if !v.Equal(*out.T) {
|
||||
t.Fatal("expected equal")
|
||||
}
|
||||
if out.T.IsZero() {
|
||||
t.Fatal("expected false")
|
||||
}
|
||||
}
|
||||
|
||||
// GH-238: Empty key name when decoding map from struct with only omitempty flag
|
||||
func TestMapOmitEmptyWithEmptyFieldnameInTag(t *testing.T) {
|
||||
type Struct struct {
|
||||
Username string `mapstructure:",omitempty"`
|
||||
Age int `mapstructure:",omitempty"`
|
||||
}
|
||||
|
||||
s := Struct{
|
||||
Username: "Joe",
|
||||
}
|
||||
var m map[string]interface{}
|
||||
|
||||
if err := Decode(s, &m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(m) != 1 {
|
||||
t.Fatalf("fail: %#v", m)
|
||||
}
|
||||
if m["Username"] != "Joe" {
|
||||
t.Fatalf("fail: %#v", m)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,256 @@
|
|||
package mapstructure
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func ExampleDecode() {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
Emails []string
|
||||
Extra map[string]string
|
||||
}
|
||||
|
||||
// This input can come from anywhere, but typically comes from
|
||||
// something like decoding JSON where we're not quite sure of the
|
||||
// struct initially.
|
||||
input := map[string]interface{}{
|
||||
"name": "Mitchell",
|
||||
"age": 91,
|
||||
"emails": []string{"one", "two", "three"},
|
||||
"extra": map[string]string{
|
||||
"twitter": "mitchellh",
|
||||
},
|
||||
}
|
||||
|
||||
var result Person
|
||||
err := Decode(input, &result)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Printf("%#v", result)
|
||||
// Output:
|
||||
// mapstructure.Person{Name:"Mitchell", Age:91, Emails:[]string{"one", "two", "three"}, Extra:map[string]string{"twitter":"mitchellh"}}
|
||||
}
|
||||
|
||||
func ExampleDecode_errors() {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
Emails []string
|
||||
Extra map[string]string
|
||||
}
|
||||
|
||||
// This input can come from anywhere, but typically comes from
|
||||
// something like decoding JSON where we're not quite sure of the
|
||||
// struct initially.
|
||||
input := map[string]interface{}{
|
||||
"name": 123,
|
||||
"age": "bad value",
|
||||
"emails": []int{1, 2, 3},
|
||||
}
|
||||
|
||||
var result Person
|
||||
err := Decode(input, &result)
|
||||
if err == nil {
|
||||
panic("should have an error")
|
||||
}
|
||||
|
||||
fmt.Println(err.Error())
|
||||
// Output:
|
||||
// 5 error(s) decoding:
|
||||
//
|
||||
// * 'Age' expected type 'int', got unconvertible type 'string', value: 'bad value'
|
||||
// * 'Emails[0]' expected type 'string', got unconvertible type 'int', value: '1'
|
||||
// * 'Emails[1]' expected type 'string', got unconvertible type 'int', value: '2'
|
||||
// * 'Emails[2]' expected type 'string', got unconvertible type 'int', value: '3'
|
||||
// * 'Name' expected type 'string', got unconvertible type 'int', value: '123'
|
||||
}
|
||||
|
||||
func ExampleDecode_metadata() {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
// This input can come from anywhere, but typically comes from
|
||||
// something like decoding JSON where we're not quite sure of the
|
||||
// struct initially.
|
||||
input := map[string]interface{}{
|
||||
"name": "Mitchell",
|
||||
"age": 91,
|
||||
"email": "foo@bar.com",
|
||||
}
|
||||
|
||||
// For metadata, we make a more advanced DecoderConfig so we can
|
||||
// more finely configure the decoder that is used. In this case, we
|
||||
// just tell the decoder we want to track metadata.
|
||||
var md Metadata
|
||||
var result Person
|
||||
config := &DecoderConfig{
|
||||
Metadata: &md,
|
||||
Result: &result,
|
||||
}
|
||||
|
||||
decoder, err := NewDecoder(config)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := decoder.Decode(input); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Printf("Unused keys: %#v", md.Unused)
|
||||
// Output:
|
||||
// Unused keys: []string{"email"}
|
||||
}
|
||||
|
||||
func ExampleDecode_weaklyTypedInput() {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
Emails []string
|
||||
}
|
||||
|
||||
// This input can come from anywhere, but typically comes from
|
||||
// something like decoding JSON, generated by a weakly typed language
|
||||
// such as PHP.
|
||||
input := map[string]interface{}{
|
||||
"name": 123, // number => string
|
||||
"age": "42", // string => number
|
||||
"emails": map[string]interface{}{}, // empty map => empty array
|
||||
}
|
||||
|
||||
var result Person
|
||||
config := &DecoderConfig{
|
||||
WeaklyTypedInput: true,
|
||||
Result: &result,
|
||||
}
|
||||
|
||||
decoder, err := NewDecoder(config)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = decoder.Decode(input)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Printf("%#v", result)
|
||||
// Output: mapstructure.Person{Name:"123", Age:42, Emails:[]string{}}
|
||||
}
|
||||
|
||||
func ExampleDecode_tags() {
|
||||
// Note that the mapstructure tags defined in the struct type
|
||||
// can indicate which fields the values are mapped to.
|
||||
type Person struct {
|
||||
Name string `mapstructure:"person_name"`
|
||||
Age int `mapstructure:"person_age"`
|
||||
}
|
||||
|
||||
input := map[string]interface{}{
|
||||
"person_name": "Mitchell",
|
||||
"person_age": 91,
|
||||
}
|
||||
|
||||
var result Person
|
||||
err := Decode(input, &result)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Printf("%#v", result)
|
||||
// Output:
|
||||
// mapstructure.Person{Name:"Mitchell", Age:91}
|
||||
}
|
||||
|
||||
func ExampleDecode_embeddedStruct() {
|
||||
// Squashing multiple embedded structs is allowed using the squash tag.
|
||||
// This is demonstrated by creating a composite struct of multiple types
|
||||
// and decoding into it. In this case, a person can carry with it both
|
||||
// a Family and a Location, as well as their own FirstName.
|
||||
type Family struct {
|
||||
LastName string
|
||||
}
|
||||
type Location struct {
|
||||
City string
|
||||
}
|
||||
type Person struct {
|
||||
Family `mapstructure:",squash"`
|
||||
Location `mapstructure:",squash"`
|
||||
FirstName string
|
||||
}
|
||||
|
||||
input := map[string]interface{}{
|
||||
"FirstName": "Mitchell",
|
||||
"LastName": "Hashimoto",
|
||||
"City": "San Francisco",
|
||||
}
|
||||
|
||||
var result Person
|
||||
err := Decode(input, &result)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Printf("%s %s, %s", result.FirstName, result.LastName, result.City)
|
||||
// Output:
|
||||
// Mitchell Hashimoto, San Francisco
|
||||
}
|
||||
|
||||
func ExampleDecode_remainingData() {
|
||||
// Note that the mapstructure tags defined in the struct type
|
||||
// can indicate which fields the values are mapped to.
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
Other map[string]interface{} `mapstructure:",remain"`
|
||||
}
|
||||
|
||||
input := map[string]interface{}{
|
||||
"name": "Mitchell",
|
||||
"age": 91,
|
||||
"email": "mitchell@example.com",
|
||||
}
|
||||
|
||||
var result Person
|
||||
err := Decode(input, &result)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Printf("%#v", result)
|
||||
// Output:
|
||||
// mapstructure.Person{Name:"Mitchell", Age:91, Other:map[string]interface {}{"email":"mitchell@example.com"}}
|
||||
}
|
||||
|
||||
func ExampleDecode_omitempty() {
|
||||
// Add omitempty annotation to avoid map keys for empty values
|
||||
type Family struct {
|
||||
LastName string
|
||||
}
|
||||
type Location struct {
|
||||
City string
|
||||
}
|
||||
type Person struct {
|
||||
*Family `mapstructure:",omitempty"`
|
||||
*Location `mapstructure:",omitempty"`
|
||||
Age int
|
||||
FirstName string
|
||||
}
|
||||
|
||||
result := &map[string]interface{}{}
|
||||
input := Person{FirstName: "Somebody"}
|
||||
err := Decode(input, &result)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Printf("%+v", result)
|
||||
// Output:
|
||||
// &map[Age:0 FirstName:Somebody]
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
package mapstructure
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDecode_Ptr(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type G struct {
|
||||
Id int
|
||||
Name string
|
||||
}
|
||||
|
||||
type X struct {
|
||||
Id int
|
||||
Name int
|
||||
}
|
||||
|
||||
type AG struct {
|
||||
List []*G
|
||||
}
|
||||
|
||||
type AX struct {
|
||||
List []*X
|
||||
}
|
||||
|
||||
g2 := &AG{
|
||||
List: []*G{
|
||||
{
|
||||
Id: 11,
|
||||
Name: "gg",
|
||||
},
|
||||
},
|
||||
}
|
||||
x2 := AX{}
|
||||
|
||||
// 报错但还是会转换成功,转换后值为目标类型的 0 值
|
||||
err := Decode(g2, &x2)
|
||||
|
||||
res := AX{
|
||||
List: []*X{
|
||||
{
|
||||
Id: 11,
|
||||
Name: 0, // 这个类型的 0 值
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
t.Errorf("Decode_Ptr jderr should not be 'nil': %#v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(res, x2) {
|
||||
t.Errorf("result should be %#v: got %#v", res, x2)
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,26 @@
|
|||
package mapstructure
|
||||
|
||||
import "time"
|
||||
|
||||
// DecodeWithTime 支持时间转字符串
|
||||
// 支持
|
||||
// 1. *Time.time 转 string/*string
|
||||
// 2. *Time.time 转 uint/uint32/uint64/int/int32/int64,支持带指针
|
||||
// 不能用 Time.time 转,它会在上层认为是一个结构体数据而直接转成map,再到hook方法
|
||||
func DecodeWithTime(input, output interface{}, layout string) error {
|
||||
if layout == "" {
|
||||
layout = time.DateTime
|
||||
}
|
||||
config := &DecoderConfig{
|
||||
Metadata: nil,
|
||||
Result: output,
|
||||
DecodeHook: ComposeDecodeHookFunc(TimeToStringHook(layout), TimeToUnixIntHook()),
|
||||
}
|
||||
|
||||
decoder, err := NewDecoder(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return decoder.Decode(input)
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
package mapstructure
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TimeToStringHook 时间转字符串
|
||||
// 支持 *Time.time 转 string/*string
|
||||
// 不能用 Time.time 转,它会在上层认为是一个结构体数据而直接转成map,再到hook方法
|
||||
func TimeToStringHook(layout string) DecodeHookFunc {
|
||||
return func(
|
||||
f reflect.Type,
|
||||
t reflect.Type,
|
||||
data interface{}) (interface{}, error) {
|
||||
// 判断目标类型是否为字符串
|
||||
var strType string
|
||||
var isStrPointer *bool // 要转换的目标类型是否为指针字符串
|
||||
if t == reflect.TypeOf(strType) {
|
||||
isStrPointer = new(bool)
|
||||
} else if t == reflect.TypeOf(&strType) {
|
||||
isStrPointer = new(bool)
|
||||
*isStrPointer = true
|
||||
}
|
||||
if isStrPointer == nil {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// 判断类型是否为时间
|
||||
timeType := time.Time{}
|
||||
if f != reflect.TypeOf(timeType) && f != reflect.TypeOf(&timeType) {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// 将时间转换为字符串
|
||||
var output string
|
||||
switch v := data.(type) {
|
||||
case *time.Time:
|
||||
output = v.Format(layout)
|
||||
case time.Time:
|
||||
output = v.Format(layout)
|
||||
default:
|
||||
return data, nil
|
||||
}
|
||||
|
||||
if *isStrPointer {
|
||||
return &output, nil
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
}
|
||||
|
||||
// TimeToUnixIntHook 时间转时间戳
|
||||
// 支持 *Time.time 转 uint/uint32/uint64/int/int32/int64,支持带指针
|
||||
// 不能用 Time.time 转,它会在上层认为是一个结构体数据而直接转成map,再到hook方法
|
||||
func TimeToUnixIntHook() DecodeHookFunc {
|
||||
return func(
|
||||
f reflect.Type,
|
||||
t reflect.Type,
|
||||
data interface{}) (interface{}, error) {
|
||||
|
||||
tkd := t.Kind()
|
||||
if tkd != reflect.Int && tkd != reflect.Int32 && tkd != reflect.Int64 &&
|
||||
tkd != reflect.Uint && tkd != reflect.Uint32 && tkd != reflect.Uint64 {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// 判断类型是否为时间
|
||||
timeType := time.Time{}
|
||||
if f != reflect.TypeOf(timeType) && f != reflect.TypeOf(&timeType) {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// 将时间转换为字符串
|
||||
var output int64
|
||||
switch v := data.(type) {
|
||||
case *time.Time:
|
||||
output = v.Unix()
|
||||
case time.Time:
|
||||
output = v.Unix()
|
||||
default:
|
||||
return data, nil
|
||||
}
|
||||
switch tkd {
|
||||
case reflect.Int:
|
||||
return int(output), nil
|
||||
case reflect.Int32:
|
||||
return int32(output), nil
|
||||
case reflect.Int64:
|
||||
return output, nil
|
||||
case reflect.Uint:
|
||||
return uint(output), nil
|
||||
case reflect.Uint32:
|
||||
return uint32(output), nil
|
||||
case reflect.Uint64:
|
||||
return uint64(output), nil
|
||||
default:
|
||||
return data, nil
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,274 @@
|
|||
package mapstructure
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Test_TimeToStringHook(t *testing.T) {
|
||||
type Input struct {
|
||||
Time time.Time
|
||||
Id int
|
||||
}
|
||||
|
||||
type InputTPointer struct {
|
||||
Time *time.Time
|
||||
Id int
|
||||
}
|
||||
|
||||
type Output struct {
|
||||
Time string
|
||||
Id int
|
||||
}
|
||||
|
||||
type OutputTPointer struct {
|
||||
Time *string
|
||||
Id int
|
||||
}
|
||||
now := time.Now()
|
||||
target := now.Format("2006-01-02 15:04:05")
|
||||
idValue := 1
|
||||
tests := []struct {
|
||||
input any
|
||||
output any
|
||||
name string
|
||||
layout string
|
||||
}{
|
||||
{
|
||||
name: "测试Time.time转string",
|
||||
layout: "2006-01-02 15:04:05",
|
||||
input: InputTPointer{
|
||||
Time: &now,
|
||||
Id: idValue,
|
||||
},
|
||||
output: Output{},
|
||||
},
|
||||
{
|
||||
name: "测试*Time.time转*string",
|
||||
layout: "2006-01-02 15:04:05",
|
||||
input: InputTPointer{
|
||||
Time: &now,
|
||||
Id: idValue,
|
||||
},
|
||||
output: OutputTPointer{},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
decoder, err := NewDecoder(&DecoderConfig{
|
||||
DecodeHook: TimeToStringHook(tt.layout),
|
||||
Result: &tt.output,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("NewDecoder() jderr = %v,want nil", err)
|
||||
}
|
||||
|
||||
if i, isOk := tt.input.(Input); isOk {
|
||||
err = decoder.Decode(i)
|
||||
}
|
||||
if i, isOk := tt.input.(InputTPointer); isOk {
|
||||
err = decoder.Decode(&i)
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("Decode jderr = %v,want nil", err)
|
||||
}
|
||||
//验证测试值
|
||||
if output, isOk := tt.output.(OutputTPointer); isOk {
|
||||
if *output.Time != target {
|
||||
t.Errorf("Decode output time = %v,want %v", *output.Time, target)
|
||||
}
|
||||
if output.Id != idValue {
|
||||
t.Errorf("Decode output id = %v,want %v", output.Id, idValue)
|
||||
}
|
||||
}
|
||||
if output, isOk := tt.output.(Output); isOk {
|
||||
if output.Time != target {
|
||||
t.Errorf("Decode output time = %v,want %v", output.Time, target)
|
||||
}
|
||||
if output.Id != idValue {
|
||||
t.Errorf("Decode output id = %v,want %v", output.Id, idValue)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_TimeToUnixIntHook(t *testing.T) {
|
||||
type InputTPointer struct {
|
||||
Time *time.Time
|
||||
Id int
|
||||
}
|
||||
|
||||
type Output[T int | *int | int32 | *int32 | int64 | *int64 | uint | *uint] struct {
|
||||
Time T
|
||||
Id int
|
||||
}
|
||||
|
||||
type test struct {
|
||||
input any
|
||||
output any
|
||||
name string
|
||||
layout string
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
target := now.Unix()
|
||||
idValue := 1
|
||||
tests := []test{
|
||||
{
|
||||
name: "测试Time.time转int",
|
||||
layout: "2006-01-02 15:04:05",
|
||||
input: InputTPointer{
|
||||
Time: &now,
|
||||
Id: idValue,
|
||||
},
|
||||
output: Output[int]{},
|
||||
},
|
||||
{
|
||||
name: "测试Time.time转*int",
|
||||
layout: "2006-01-02 15:04:05",
|
||||
input: InputTPointer{
|
||||
Time: &now,
|
||||
Id: idValue,
|
||||
},
|
||||
output: Output[*int]{},
|
||||
},
|
||||
{
|
||||
name: "测试Time.time转int32",
|
||||
layout: "2006-01-02 15:04:05",
|
||||
input: InputTPointer{
|
||||
Time: &now,
|
||||
Id: idValue,
|
||||
},
|
||||
output: Output[int32]{},
|
||||
},
|
||||
{
|
||||
name: "测试Time.time转*int32",
|
||||
layout: "2006-01-02 15:04:05",
|
||||
input: InputTPointer{
|
||||
Time: &now,
|
||||
Id: idValue,
|
||||
},
|
||||
output: Output[*int32]{},
|
||||
},
|
||||
{
|
||||
name: "测试Time.time转int64",
|
||||
layout: "2006-01-02 15:04:05",
|
||||
input: InputTPointer{
|
||||
Time: &now,
|
||||
Id: idValue,
|
||||
},
|
||||
output: Output[int64]{},
|
||||
},
|
||||
{
|
||||
name: "测试Time.time转*int64",
|
||||
layout: "2006-01-02 15:04:05",
|
||||
input: InputTPointer{
|
||||
Time: &now,
|
||||
Id: idValue,
|
||||
},
|
||||
output: Output[*int64]{},
|
||||
},
|
||||
{
|
||||
name: "测试Time.time转uint",
|
||||
layout: "2006-01-02 15:04:05",
|
||||
input: InputTPointer{
|
||||
Time: &now,
|
||||
Id: idValue,
|
||||
},
|
||||
output: Output[uint]{},
|
||||
},
|
||||
{
|
||||
name: "测试Time.time转*uint",
|
||||
layout: "2006-01-02 15:04:05",
|
||||
input: InputTPointer{
|
||||
Time: &now,
|
||||
Id: idValue,
|
||||
},
|
||||
output: Output[*uint]{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
decoder, err := NewDecoder(&DecoderConfig{
|
||||
DecodeHook: TimeToUnixIntHook(),
|
||||
Result: &tt.output,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("NewDecoder() jderr = %v,want nil", err)
|
||||
}
|
||||
|
||||
if i, isOk := tt.input.(InputTPointer); isOk {
|
||||
err = decoder.Decode(i)
|
||||
}
|
||||
if i, isOk := tt.input.(InputTPointer); isOk {
|
||||
err = decoder.Decode(&i)
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("Decode jderr = %v,want nil", err)
|
||||
}
|
||||
|
||||
//验证测试值
|
||||
switch v := tt.output.(type) {
|
||||
case Output[int]:
|
||||
if int64(v.Time) != target {
|
||||
t.Errorf("Decode output time = %v,want %v", v.Time, target)
|
||||
}
|
||||
if v.Id != idValue {
|
||||
t.Errorf("Decode output id = %v,want %v", v.Id, idValue)
|
||||
}
|
||||
case Output[*int]:
|
||||
if int64(*v.Time) != target {
|
||||
t.Errorf("Decode output time = %v,want %v", v.Time, target)
|
||||
}
|
||||
if v.Id != idValue {
|
||||
t.Errorf("Decode output id = %v,want %v", v.Id, idValue)
|
||||
}
|
||||
case Output[int32]:
|
||||
if int64(v.Time) != target {
|
||||
t.Errorf("Decode output time = %v,want %v", v.Time, target)
|
||||
}
|
||||
if v.Id != idValue {
|
||||
t.Errorf("Decode output id = %v,want %v", v.Id, idValue)
|
||||
}
|
||||
case Output[*int32]:
|
||||
if int64(*v.Time) != target {
|
||||
t.Errorf("Decode output time = %v,want %v", v.Time, target)
|
||||
}
|
||||
if v.Id != idValue {
|
||||
t.Errorf("Decode output id = %v,want %v", v.Id, idValue)
|
||||
}
|
||||
case Output[int64]:
|
||||
if int64(v.Time) != target {
|
||||
t.Errorf("Decode output time = %v,want %v", v.Time, target)
|
||||
}
|
||||
if v.Id != idValue {
|
||||
t.Errorf("Decode output id = %v,want %v", v.Id, idValue)
|
||||
}
|
||||
case Output[*int64]:
|
||||
if int64(*v.Time) != target {
|
||||
t.Errorf("Decode output time = %v,want %v", v.Time, target)
|
||||
}
|
||||
if v.Id != idValue {
|
||||
t.Errorf("Decode output id = %v,want %v", v.Id, idValue)
|
||||
}
|
||||
case Output[uint]:
|
||||
if int64(v.Time) != target {
|
||||
t.Errorf("Decode output time = %v,want %v", v.Time, target)
|
||||
}
|
||||
if v.Id != idValue {
|
||||
t.Errorf("Decode output id = %v,want %v", v.Id, idValue)
|
||||
}
|
||||
case Output[*uint]:
|
||||
if int64(*v.Time) != target {
|
||||
t.Errorf("Decode output time = %v,want %v", v.Time, target)
|
||||
}
|
||||
if v.Id != idValue {
|
||||
t.Errorf("Decode output id = %v,want %v", v.Id, idValue)
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package pkg
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/pkg/utils_ollama"
|
||||
|
||||
"github.com/google/wire"
|
||||
)
|
||||
|
||||
var ProviderSetClient = wire.NewSet(
|
||||
NewRdb,
|
||||
NewGormDb,
|
||||
utils_ollama.NewUtilOllama,
|
||||
)
|
|
@ -0,0 +1,48 @@
|
|||
package pkg
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/config"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
type Rdb struct {
|
||||
Rdb *redis.Client
|
||||
}
|
||||
|
||||
var rdb *Rdb
|
||||
|
||||
func NewRdb(c *config.Config) *Rdb {
|
||||
if rdb == nil {
|
||||
//构建 redis
|
||||
rdbBuild := buildRdb(c.Redis)
|
||||
//退出时清理资源
|
||||
rdb = &Rdb{Rdb: rdbBuild}
|
||||
}
|
||||
//cleanup := func() {
|
||||
// if rdb != nil {
|
||||
// if err := rdb.Rdb.Close(); err != nil {
|
||||
// fmt.Println("关闭 redis 失败:%v", err)
|
||||
// }
|
||||
// }
|
||||
// fmt.Println("关闭 data 中的连接资源已完成")
|
||||
//}
|
||||
return rdb
|
||||
}
|
||||
|
||||
// buildRdb 构建redis client
|
||||
func buildRdb(c config.Redis) *redis.Client {
|
||||
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: c.Host,
|
||||
Password: c.Pass,
|
||||
ReadTimeout: time.Duration(c.Tls) * time.Second,
|
||||
WriteTimeout: time.Duration(c.Tls) * time.Second,
|
||||
PoolSize: int(c.PoolSize),
|
||||
MinIdleConns: int(c.MaxIdle),
|
||||
ConnMaxIdleTime: time.Duration(c.MaxIdleTime) * time.Second,
|
||||
DB: int(c.Db),
|
||||
})
|
||||
return rdb
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package utils_gorm
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/config"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func DBConn(c config.DB) (*gorm.DB, func()) {
|
||||
mysqlConn, err := sql.Open(c.Driver, c.Source)
|
||||
gormDB, err := gorm.Open(
|
||||
mysql.New(mysql.Config{Conn: mysqlConn}),
|
||||
)
|
||||
|
||||
gormDB.Logger = NewCustomLogger(gormDB)
|
||||
if err != nil {
|
||||
panic("failed to connect database")
|
||||
}
|
||||
sqlDB, err := gormDB.DB()
|
||||
|
||||
// SetMaxIdleConns sets the maximum number of connections in the idle connection pool.
|
||||
sqlDB.SetMaxIdleConns(int(c.MaxIdle))
|
||||
|
||||
// SetMaxOpenConns sets the maximum number of open connections to the database.
|
||||
sqlDB.SetMaxOpenConns(int(c.MaxLifetime))
|
||||
|
||||
// SetConnMaxLifetime sets the maximum amount of time a connection may be reused.
|
||||
sqlDB.SetConnMaxLifetime(time.Hour)
|
||||
|
||||
return gormDB, func() {
|
||||
if mysqlConn != nil {
|
||||
fmt.Println("关闭 physicalGoodsDB")
|
||||
if err := mysqlConn.Close(); err != nil {
|
||||
fmt.Println("关闭 physicalGoodsDB 失败:", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
package utils_gorm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type CustomLogger struct {
|
||||
gormLogger logger.Interface
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewCustomLogger(db *gorm.DB) *CustomLogger {
|
||||
return &CustomLogger{
|
||||
gormLogger: logger.Default.LogMode(logger.Info),
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *CustomLogger) LogMode(level logger.LogLevel) logger.Interface {
|
||||
newlogger := *l
|
||||
newlogger.gormLogger = l.gormLogger.LogMode(level)
|
||||
return &newlogger
|
||||
}
|
||||
|
||||
func (l *CustomLogger) Info(ctx context.Context, msg string, data ...interface{}) {
|
||||
l.gormLogger.Info(ctx, msg, data...)
|
||||
}
|
||||
|
||||
func (l *CustomLogger) Warn(ctx context.Context, msg string, data ...interface{}) {
|
||||
l.gormLogger.Warn(ctx, msg, data...)
|
||||
}
|
||||
|
||||
func (l *CustomLogger) Error(ctx context.Context, msg string, data ...interface{}) {
|
||||
l.gormLogger.Error(ctx, msg, data...)
|
||||
}
|
||||
|
||||
func (l *CustomLogger) Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) {
|
||||
elapsed := time.Since(begin)
|
||||
sql, _ := fc()
|
||||
l.gormLogger.Trace(ctx, begin, fc, err)
|
||||
operation := extractOperation(sql)
|
||||
tableName := extractTableName(sql)
|
||||
fmt.Println(tableName)
|
||||
//// 将SQL语句保存到数据库
|
||||
if operation == 0 || tableName == "sql_log" {
|
||||
return
|
||||
}
|
||||
//go l.db.Model(&SqlLog{}).Create(&SqlLog{
|
||||
// OperatorID: 1,
|
||||
// OperatorName: "test",
|
||||
// SqlInfo: sql,
|
||||
// TableNames: tableName,
|
||||
// Type: operation,
|
||||
//})
|
||||
|
||||
// 如果有需要,也可以根据执行时间(elapsed)等条件过滤或处理日志记录
|
||||
if elapsed > time.Second {
|
||||
//l.gormLogger.Warn(ctx, "Slow SQL (> 1s): %s", sql)
|
||||
}
|
||||
}
|
||||
|
||||
// extractTableName extracts the table name from a SQL query, supporting quoted table names.
|
||||
func extractTableName(sql string) string {
|
||||
// 使用非捕获组匹配多种SQL操作关键词
|
||||
re := regexp.MustCompile(`(?i)\b(?:from|update|into|delete\s+from)\b\s+[\` + "`" + `"]?(\w+)[\` + "`" + `"]?`)
|
||||
match := re.FindStringSubmatch(sql)
|
||||
|
||||
// 检查是否匹配成功
|
||||
if len(match) > 1 {
|
||||
return match[1]
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// extractOperation extracts the operation type from a SQL query.
|
||||
func extractOperation(sql string) int32 {
|
||||
sql = strings.TrimSpace(strings.ToLower(sql))
|
||||
var operation int32
|
||||
if strings.HasPrefix(sql, "select") {
|
||||
operation = 0
|
||||
} else if strings.HasPrefix(sql, "insert") {
|
||||
operation = 1
|
||||
} else if strings.HasPrefix(sql, "update") {
|
||||
operation = 3
|
||||
} else if strings.HasPrefix(sql, "delete") {
|
||||
operation = 2
|
||||
}
|
||||
return operation
|
||||
}
|
|
@ -1,14 +1,15 @@
|
|||
package ollama
|
||||
package utils_ollama
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/pkg/types"
|
||||
"ai_scheduler/internal/entitys"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/ollama/ollama/api"
|
||||
"github.com/tmc/langchaingo/llms/ollama"
|
||||
)
|
||||
|
||||
// Client Ollama客户端适配器
|
||||
|
@ -18,20 +19,25 @@ type Client struct {
|
|||
}
|
||||
|
||||
// NewClient 创建新的Ollama客户端
|
||||
func NewClient(config *config.OllamaConfig) (*Client, error) {
|
||||
func NewClient(config *config.Config) (entitys.AIClient, func(), error) {
|
||||
client, err := api.ClientFromEnvironment()
|
||||
cleanup := func() {
|
||||
if client != nil {
|
||||
client = nil
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create ollama client: %w", err)
|
||||
return nil, cleanup, fmt.Errorf("failed to create ollama client: %w", err)
|
||||
}
|
||||
|
||||
return &Client{
|
||||
client: client,
|
||||
config: config,
|
||||
}, nil
|
||||
config: &config.Ollama,
|
||||
}, cleanup, nil
|
||||
}
|
||||
|
||||
// Chat 实现聊天功能
|
||||
func (c *Client) Chat(ctx context.Context, messages []types.Message, tools []types.ToolDefinition) (*types.ChatResponse, error) {
|
||||
func (c *Client) Chat(ctx context.Context, messages []entitys.Message, tools []entitys.ToolDefinition) (*entitys.ChatResponse, error) {
|
||||
// 构建聊天请求
|
||||
req := &api.ChatRequest{
|
||||
Model: c.config.Model,
|
||||
|
@ -89,29 +95,30 @@ func (c *Client) Chat(ctx context.Context, messages []types.Message, tools []typ
|
|||
}
|
||||
|
||||
// convertResponse 转换响应格式
|
||||
func (c *Client) convertResponse(resp *api.ChatResponse) *types.ChatResponse {
|
||||
result := &types.ChatResponse{
|
||||
Message: resp.Message.Content,
|
||||
Finished: resp.Done,
|
||||
}
|
||||
func (c *Client) convertResponse(resp *api.ChatResponse) *entitys.ChatResponse {
|
||||
//result := &entitys.ChatResponse{
|
||||
// Message: resp.Message.Content,
|
||||
// Finished: resp.Done,
|
||||
//}
|
||||
//
|
||||
//// 转换工具调用
|
||||
//if len(resp.Message.ToolCalls) > 0 {
|
||||
// result.ToolCalls = make([]entitys.ToolCall, len(resp.Message.ToolCalls))
|
||||
// for i, toolCall := range resp.Message.ToolCalls {
|
||||
// // 转换函数参数
|
||||
// argBytes, _ := json.Marshal(toolCall.Function.Arguments)
|
||||
//
|
||||
// result.ToolCalls[i] = entitys.ToolCall{
|
||||
// ID: fmt.Sprintf("call_%d", i),
|
||||
// Type: "function",
|
||||
// Function: entitys.FunctionCall{
|
||||
// Name: toolCall.Function.Name,
|
||||
// Arguments: json.RawMessage(argBytes),
|
||||
// },
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
// 转换工具调用
|
||||
if len(resp.Message.ToolCalls) > 0 {
|
||||
result.ToolCalls = make([]types.ToolCall, len(resp.Message.ToolCalls))
|
||||
for i, toolCall := range resp.Message.ToolCalls {
|
||||
// 转换函数参数
|
||||
argBytes, _ := json.Marshal(toolCall.Function.Arguments)
|
||||
|
||||
result.ToolCalls[i] = types.ToolCall{
|
||||
ID: fmt.Sprintf("call_%d", i),
|
||||
Type: "function",
|
||||
Function: types.FunctionCall{
|
||||
Name: toolCall.Function.Name,
|
||||
Arguments: json.RawMessage(argBytes),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
//return result
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package utils_ollama
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/config"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gofiber/fiber/v2/log"
|
||||
"github.com/tmc/langchaingo/llms/ollama"
|
||||
)
|
||||
|
||||
type UtilOllama struct {
|
||||
Llm *ollama.LLM
|
||||
}
|
||||
|
||||
func NewUtilOllama(c *config.Config, logger log.AllLogger) *UtilOllama {
|
||||
llm, err := ollama.New(
|
||||
ollama.WithModel(c.Ollama.Model),
|
||||
ollama.WithHTTPClient(http.DefaultClient),
|
||||
ollama.WithServerURL(getUrl(c)),
|
||||
ollama.WithKeepAlive("1h"),
|
||||
)
|
||||
if err != nil {
|
||||
logger.Fatal(err)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &UtilOllama{
|
||||
Llm: llm,
|
||||
}
|
||||
}
|
||||
|
||||
//func (o *UtilOllama) a() {
|
||||
// var agent agents.Agent
|
||||
// agent = agents.NewOneShotAgent(llm, tools, opts...)
|
||||
//
|
||||
// agents.NewExecutor()
|
||||
//}
|
||||
|
||||
func getUrl(c *config.Config) string {
|
||||
baseURL := c.Ollama.BaseURL
|
||||
envURL := os.Getenv("OLLAMA_BASE_URL")
|
||||
if envURL != "" {
|
||||
baseURL = envURL
|
||||
}
|
||||
return baseURL
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/gateway"
|
||||
"ai_scheduler/internal/server/router"
|
||||
"ai_scheduler/internal/services"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/logger"
|
||||
"github.com/gofiber/fiber/v2/middleware/recover"
|
||||
)
|
||||
|
||||
func NewHTTPServer(
|
||||
service *services.ChatService,
|
||||
session *services.SessionService,
|
||||
gateway *gateway.Gateway,
|
||||
) *fiber.App {
|
||||
//构建 server
|
||||
app := initRoute()
|
||||
router.SetupRoutes(app, service, session, gateway)
|
||||
return app
|
||||
}
|
||||
|
||||
func initRoute() *fiber.App {
|
||||
app := fiber.New()
|
||||
app.Use(
|
||||
recover.New(),
|
||||
logger.New(),
|
||||
)
|
||||
return app
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package server
|
||||
|
||||
import "github.com/google/wire"
|
||||
|
||||
var ProviderSetServer = wire.NewSet(NewServers, NewHTTPServer)
|
|
@ -0,0 +1,131 @@
|
|||
package router
|
||||
|
||||
import (
|
||||
errors "ai_scheduler/internal/data/error"
|
||||
"ai_scheduler/internal/gateway"
|
||||
"ai_scheduler/internal/services"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/websocket/v2"
|
||||
)
|
||||
|
||||
// SetupRoutes 设置路由
|
||||
func SetupRoutes(app *fiber.App, ChatService *services.ChatService, sessionService *services.SessionService, gateway *gateway.Gateway) {
|
||||
app.Use(func(c *fiber.Ctx) error {
|
||||
// 设置 CORS 头
|
||||
c.Set("Access-Control-Allow-Origin", "*")
|
||||
c.Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
c.Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
|
||||
// 如果是预检请求(OPTIONS),直接返回 204
|
||||
if c.Method() == "OPTIONS" {
|
||||
return c.SendStatus(fiber.StatusNoContent) // 204
|
||||
}
|
||||
|
||||
// 继续处理后续中间件或路由
|
||||
return c.Next()
|
||||
})
|
||||
routerHttp(app, sessionService, gateway)
|
||||
routerSocket(app, ChatService)
|
||||
|
||||
}
|
||||
|
||||
func routerSocket(app *fiber.App, chatService *services.ChatService) {
|
||||
ws := app.Group("ws/v1/")
|
||||
// WebSocket 路由配置
|
||||
ws.Get("/chat", websocket.New(func(c *websocket.Conn) {
|
||||
// 可以在这里添加握手前的中间件逻辑(如头校验)
|
||||
chatService.Chat(c) // 调用实际的 Chat 处理函数
|
||||
}, websocket.Config{
|
||||
// 可选配置:跨域检查、最大负载大小等
|
||||
HandshakeTimeout: 10 * time.Second,
|
||||
//Subprotocols: []string{"json", "msgpack"},
|
||||
//Origins: []string{"http://localhost:3000", "https://yourdomain.com"} //验证 Origin 头,防止跨站请求伪造(CSRF)。如果为空,则允许所有来源。
|
||||
ReadBufferSize: 8192,
|
||||
WriteBufferSize: 8192,
|
||||
WriteBufferPool: bufferPool, // 使用缓冲池
|
||||
}))
|
||||
}
|
||||
|
||||
var bufferPool = &sync.Pool{
|
||||
New: func() interface{} {
|
||||
return make([]byte, 4096) // 4KB 缓冲区
|
||||
},
|
||||
}
|
||||
|
||||
func routerHttp(app *fiber.App, sessionService *services.SessionService, gateway *gateway.Gateway) {
|
||||
r := app.Group("api/v1/")
|
||||
registerResponse(r)
|
||||
// 注册 CORS 中间件
|
||||
r.Get("/health", func(c *fiber.Ctx) error {
|
||||
c.Response().SetBody([]byte("1"))
|
||||
return nil
|
||||
})
|
||||
|
||||
r.Post("/session/init", sessionService.SessionInit) // 会话初始化,不存在则创建,存在则返回会话ID和默认条数会话历史
|
||||
r.Post("/session/list", sessionService.SessionList)
|
||||
//广播
|
||||
r.Get("/broadcast", func(ctx *fiber.Ctx) error {
|
||||
action := ctx.Query("action")
|
||||
uid := ctx.Query("uid")
|
||||
msg := ctx.Query("msg")
|
||||
|
||||
switch action {
|
||||
case "sendToAll":
|
||||
gateway.SendToAll([]byte(msg))
|
||||
return ctx.SendString("sent to all")
|
||||
case "sendToUid":
|
||||
if uid == "" {
|
||||
return ctx.Status(400).SendString("missing uid")
|
||||
}
|
||||
gateway.SendToUid(uid, []byte(msg))
|
||||
return ctx.SendString(fmt.Sprintf("sent to uid %s", uid))
|
||||
default:
|
||||
return ctx.Status(400).SendString("unknown action")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func registerResponse(router fiber.Router) {
|
||||
// 自定义返回
|
||||
router.Use(func(c *fiber.Ctx) error {
|
||||
err := c.Next()
|
||||
return registerCommon(c, err)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func registerCommon(c *fiber.Ctx, err error) error {
|
||||
// 调用下一个中间件或路由处理函数
|
||||
|
||||
bsErr, ok := err.(*errors.BusinessErr)
|
||||
if !ok {
|
||||
bsErr = errors.SystemError
|
||||
}
|
||||
// 如果有错误发生
|
||||
if err != nil {
|
||||
// 返回自定义错误响应
|
||||
return c.JSON(fiber.Map{
|
||||
"message": bsErr.Error(),
|
||||
"code": bsErr.Code(),
|
||||
"data": nil,
|
||||
})
|
||||
}
|
||||
contentType := strings.ToLower(string(c.Response().Header.Peek("Content-Type")))
|
||||
if strings.Contains(strings.ToLower(contentType), "text/event-stream") {
|
||||
// 是 SSE 请求
|
||||
return c.SendString("这是 SSE 请求")
|
||||
}
|
||||
var data interface{}
|
||||
json.Unmarshal(c.Response().Body(), &data)
|
||||
return c.JSON(fiber.Map{
|
||||
"data": data,
|
||||
"message": errors.Success.Error(),
|
||||
"code": errors.Success.Code(),
|
||||
})
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package server
|
||||
|
||||
import "github.com/gofiber/fiber/v2"
|
||||
|
||||
type Servers struct {
|
||||
HttpServer *fiber.App
|
||||
}
|
||||
|
||||
func NewServers(fiber *fiber.App) *Servers {
|
||||
return &Servers{
|
||||
HttpServer: fiber,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
errorcode "ai_scheduler/internal/data/error"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/log"
|
||||
)
|
||||
|
||||
// 响应数据
|
||||
func handRes(c *fiber.Ctx, _err error, rsp interface{}) error {
|
||||
var (
|
||||
err *errorcode.BusinessErr
|
||||
)
|
||||
if _err == nil {
|
||||
err = errorcode.Success
|
||||
} else {
|
||||
if e, ok := _err.(*errorcode.BusinessErr); ok {
|
||||
err = e
|
||||
} else {
|
||||
log.Error(c.UserContext(), "系统错误 error: ", _err)
|
||||
err = errorcode.NewBusinessErr("500", _err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
body := fiber.Map{
|
||||
"code": err.Code,
|
||||
"msg": err.Error(),
|
||||
"data": rsp,
|
||||
}
|
||||
|
||||
log.Info(c.UserContext(), c.Path(), "请求参数=", c.BodyRaw(), "响应=", body)
|
||||
return c.JSON(body)
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/data/constant"
|
||||
"ai_scheduler/internal/entitys"
|
||||
"ai_scheduler/internal/gateway"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/websocket/v2"
|
||||
)
|
||||
|
||||
// ChatHandler 聊天处理器
|
||||
type ChatService struct {
|
||||
routerService entitys.RouterService
|
||||
Gw *gateway.Gateway
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewChatHandler 创建聊天处理器
|
||||
func NewChatService(routerService entitys.RouterService, gw *gateway.Gateway) *ChatService {
|
||||
return &ChatService{
|
||||
routerService: routerService,
|
||||
Gw: gw,
|
||||
}
|
||||
}
|
||||
|
||||
// ToolCallResponse 工具调用响应
|
||||
type ToolCallResponse struct {
|
||||
ID string `json:"id" example:"call_1"`
|
||||
Type string `json:"type" example:"function"`
|
||||
Function FunctionCallResponse `json:"function"`
|
||||
Result interface{} `json:"result,omitempty"`
|
||||
}
|
||||
|
||||
// FunctionCallResponse 函数调用响应
|
||||
type FunctionCallResponse struct {
|
||||
Name string `json:"name" example:"get_weather"`
|
||||
Arguments interface{} `json:"arguments"`
|
||||
}
|
||||
|
||||
func (h *ChatService) ChatFail(c *websocket.Conn, content string) {
|
||||
err := c.WriteMessage(websocket.TextMessage, []byte(content))
|
||||
if err != nil {
|
||||
log.Println("发送错误:", err)
|
||||
}
|
||||
_ = c.Close()
|
||||
}
|
||||
|
||||
func generateClientID() string {
|
||||
// 使用时间戳+随机数确保唯一性
|
||||
timestamp := time.Now().UnixNano()
|
||||
randomBytes := make([]byte, 4)
|
||||
rand.Read(randomBytes)
|
||||
randomStr := hex.EncodeToString(randomBytes)
|
||||
return fmt.Sprintf("%d%s", timestamp, randomStr)
|
||||
}
|
||||
func (h *ChatService) Chat(c *websocket.Conn) {
|
||||
h.mu.Lock()
|
||||
clientID := generateClientID()
|
||||
h.mu.Unlock()
|
||||
client := &gateway.Client{
|
||||
ID: clientID,
|
||||
SendFunc: func(data []byte) error {
|
||||
return c.WriteMessage(websocket.TextMessage, data)
|
||||
},
|
||||
}
|
||||
h.Gw.AddClient(client)
|
||||
log.Println("client connected:", clientID)
|
||||
log.Println("客户端已连接")
|
||||
// 循环读取客户端消息
|
||||
for {
|
||||
messageType, message, err := c.ReadMessage()
|
||||
if err != nil {
|
||||
log.Println("读取错误:", err)
|
||||
break
|
||||
}
|
||||
//简单协议:bind:<uid>
|
||||
if c.Headers("Sec-Websocket-Protocol") == "bind" && c.Headers("X-Session") != "" {
|
||||
uid := c.Headers("X-Session")
|
||||
_ = h.Gw.BindUid(clientID, uid)
|
||||
log.Printf("bind %s -> uid:%s\n", clientID, uid)
|
||||
}
|
||||
msg, chatType := h.handleMessageToString(c, messageType, message)
|
||||
if chatType == constant.ConnStatusClosed {
|
||||
break
|
||||
}
|
||||
if chatType == constant.ConnStatusIgnore {
|
||||
continue
|
||||
}
|
||||
|
||||
log.Printf("收到消息: %s", string(msg))
|
||||
var req entitys.ChatSockRequest
|
||||
if err := json.Unmarshal(msg, &req); err != nil {
|
||||
log.Println("JSON parse error:", err)
|
||||
continue
|
||||
}
|
||||
err = h.routerService.RouteWithSocket(c, &req)
|
||||
if err != nil {
|
||||
log.Println("处理失败:", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
h.Gw.RemoveClient(clientID)
|
||||
_ = c.Close()
|
||||
log.Println("client disconnected:", clientID)
|
||||
}
|
||||
|
||||
func (h *ChatService) handleMessageToString(c *websocket.Conn, msgType int, msg any) (text []byte, chatType constant.ConnStatus) {
|
||||
switch msgType {
|
||||
case websocket.TextMessage:
|
||||
return msg.([]byte), constant.ConnStatusNormal
|
||||
case websocket.BinaryMessage:
|
||||
return msg.([]byte), constant.ConnStatusNormal
|
||||
case websocket.CloseMessage:
|
||||
|
||||
return nil, constant.ConnStatusClosed
|
||||
case websocket.PingMessage:
|
||||
// 可选:回复 Pong
|
||||
c.WriteMessage(websocket.PongMessage, nil)
|
||||
return nil, constant.ConnStatusIgnore
|
||||
case websocket.PongMessage:
|
||||
return nil, constant.ConnStatusIgnore
|
||||
default:
|
||||
return nil, constant.ConnStatusIgnore
|
||||
}
|
||||
return msg.([]byte), constant.ConnStatusIgnore
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/gateway"
|
||||
"github.com/google/wire"
|
||||
)
|
||||
|
||||
var ProviderSetServices = wire.NewSet(NewChatService, NewSessionService, gateway.NewGateway)
|
|
@ -1,209 +0,0 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/constants"
|
||||
"ai_scheduler/internal/tools"
|
||||
"ai_scheduler/pkg/types"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// RouterService 智能路由服务
|
||||
type RouterService struct {
|
||||
aiClient types.AIClient
|
||||
toolManager *tools.Manager
|
||||
}
|
||||
|
||||
// NewRouterService 创建路由服务
|
||||
func NewRouterService(aiClient types.AIClient, toolManager *tools.Manager) *RouterService {
|
||||
return &RouterService{
|
||||
aiClient: aiClient,
|
||||
toolManager: toolManager,
|
||||
}
|
||||
}
|
||||
|
||||
// Route 执行智能路由
|
||||
func (r *RouterService) Route(ctx context.Context, req *types.ChatRequest) (*types.ChatResponse, error) {
|
||||
// 构建消息
|
||||
messages := []types.Message{
|
||||
// {
|
||||
// Role: "system",
|
||||
// Content: r.buildSystemPrompt(),
|
||||
// },
|
||||
{
|
||||
Role: "assistant",
|
||||
Content: r.buildIntentPrompt(req.UserInput),
|
||||
},
|
||||
{
|
||||
Role: "user",
|
||||
Content: req.UserInput,
|
||||
},
|
||||
}
|
||||
|
||||
// 第1次调用AI,获取用户意图
|
||||
intentResponse, err := r.aiClient.Chat(ctx, messages, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("AI响应失败: %w", err)
|
||||
}
|
||||
|
||||
// 从AI响应中提取意图
|
||||
intent := r.extractIntent(intentResponse)
|
||||
if intent == "" {
|
||||
return nil, fmt.Errorf("未识别到用户意图")
|
||||
}
|
||||
|
||||
switch intent {
|
||||
case "order_diagnosis":
|
||||
// 订单诊断意图
|
||||
return r.handleOrderDiagnosis(ctx, req, messages)
|
||||
case "knowledge_qa":
|
||||
// 知识问答意图
|
||||
return r.handleKnowledgeQA(ctx, req, messages)
|
||||
default:
|
||||
// 未知意图
|
||||
return nil, fmt.Errorf("意图识别失败,请明确您的需求呢,我可以为您")
|
||||
}
|
||||
|
||||
// 获取工具定义
|
||||
toolDefinitions := r.toolManager.GetToolDefinitions(constants.Caller(req.Caller))
|
||||
|
||||
// 第2次调用AI,获取是否需要使用工具
|
||||
response, err := r.aiClient.Chat(ctx, messages, toolDefinitions)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to chat with AI: %w", err)
|
||||
}
|
||||
|
||||
// 如果没有工具调用,直接返回
|
||||
if len(response.ToolCalls) == 0 {
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// 执行工具调用
|
||||
toolResults, err := r.toolManager.ExecuteToolCalls(ctx, response.ToolCalls)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute tools: %w", err)
|
||||
}
|
||||
|
||||
// 构建包含工具结果的消息
|
||||
messages = append(messages, types.Message{
|
||||
Role: "assistant",
|
||||
Content: response.Message,
|
||||
})
|
||||
|
||||
// 添加工具调用结果
|
||||
for _, toolResult := range toolResults {
|
||||
toolResultStr, _ := json.Marshal(toolResult.Result)
|
||||
messages = append(messages, types.Message{
|
||||
Role: "tool",
|
||||
Content: fmt.Sprintf("Tool %s result: %s", toolResult.Function.Name, string(toolResultStr)),
|
||||
})
|
||||
}
|
||||
|
||||
// 第二次调用AI,生成最终回复
|
||||
finalResponse, err := r.aiClient.Chat(ctx, messages, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate final response: %w", err)
|
||||
}
|
||||
|
||||
// 合并工具调用信息到最终响应
|
||||
finalResponse.ToolCalls = toolResults
|
||||
|
||||
log.Printf("Router processed request: %s, used %d tools", req.UserInput, len(toolResults))
|
||||
|
||||
return finalResponse, nil
|
||||
}
|
||||
|
||||
// buildSystemPrompt 构建系统提示词
|
||||
func (r *RouterService) buildSystemPrompt() string {
|
||||
prompt := `你是一个智能路由系统,你的任务是根据用户输入判断用户的意图,并且执行对应的任务。`
|
||||
|
||||
return prompt
|
||||
}
|
||||
|
||||
// buildIntentPrompt 构建意图识别提示词
|
||||
func (r *RouterService) buildIntentPrompt(userInput string) string {
|
||||
prompt := `请分析以下用户输入,判断用户的意图类型。
|
||||
|
||||
用户输入:{user_input}
|
||||
|
||||
意图类型说明:
|
||||
1. order_diagnosis - 订单诊断:用户想要查询、诊断或了解订单相关信息
|
||||
2. knowledge_qa - 知识问答:用户想要进行一般性问答或获取知识信息
|
||||
|
||||
- 当用户意图不够清晰且不匹配 knowledge_qa 以外意图时,使用knowledge_qa
|
||||
- 当用户意图非常不清晰时使用 unknown
|
||||
|
||||
请只返回以下格式的JSON:
|
||||
{
|
||||
"intent": "order_diagnosis" | "knowledge_qa" | "unknown",
|
||||
"confidence": 0.0-1.0,
|
||||
"reasoning": "判断理由"
|
||||
}
|
||||
`
|
||||
|
||||
prompt = strings.ReplaceAll(prompt, "{user_input}", userInput)
|
||||
|
||||
return prompt
|
||||
}
|
||||
|
||||
// extractIntent 从AI响应中提取意图
|
||||
func (r *RouterService) extractIntent(response *types.ChatResponse) string {
|
||||
if response == nil || response.Message == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 尝试解析JSON
|
||||
var intent struct {
|
||||
Intent string `json:"intent"`
|
||||
Confidence string `json:"confidence"`
|
||||
Reasoning string `json:"reasoning"`
|
||||
}
|
||||
err := json.Unmarshal([]byte(response.Message), &intent)
|
||||
if err != nil {
|
||||
log.Printf("Failed to parse intent JSON: %v", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
return intent.Intent
|
||||
}
|
||||
|
||||
// handleOrderDiagnosis 处理订单诊断意图
|
||||
func (r *RouterService) handleOrderDiagnosis(ctx context.Context, req *types.ChatRequest, messages []types.Message) (*types.ChatResponse, error) {
|
||||
// 调用订单详情工具
|
||||
orderDetailTool, ok := r.toolManager.GetTool("zltxOrderDetail")
|
||||
if orderDetailTool == nil || !ok {
|
||||
return nil, fmt.Errorf("order detail tool not found")
|
||||
}
|
||||
orderDetailTool.Execute(ctx, json.RawMessage{})
|
||||
|
||||
// 获取相关工具定义
|
||||
toolDefinitions := r.toolManager.GetToolDefinitions(constants.Caller(req.Caller))
|
||||
|
||||
// 调用AI,获取是否需要使用工具
|
||||
response, err := r.aiClient.Chat(ctx, messages, toolDefinitions)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to chat with AI: %w", err)
|
||||
}
|
||||
|
||||
// 如果没有工具调用,直接返回
|
||||
if len(response.ToolCalls) == 0 {
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// 执行工具调用
|
||||
toolResults, err := r.toolManager.ExecuteToolCalls(ctx, response.ToolCalls)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute tools: %w", err)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// handleKnowledgeQA 处理知识问答意图
|
||||
func (r *RouterService) handleKnowledgeQA(ctx context.Context, req *types.ChatRequest, messages []types.Message) (*types.ChatResponse, error) {
|
||||
|
||||
return nil, nil
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/biz"
|
||||
"ai_scheduler/internal/entitys"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type SessionService struct {
|
||||
sessionBiz *biz.SessionBiz
|
||||
chatBiz *biz.ChatHistoryBiz
|
||||
}
|
||||
|
||||
func NewSessionService(sessionBiz *biz.SessionBiz, chatBiz *biz.ChatHistoryBiz) *SessionService {
|
||||
return &SessionService{
|
||||
sessionBiz: sessionBiz,
|
||||
chatBiz: chatBiz,
|
||||
}
|
||||
}
|
||||
|
||||
// SessionInit 初始化会话
|
||||
func (s *SessionService) SessionInit(c *fiber.Ctx) error {
|
||||
req := &entitys.SessionInitRequest{}
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result, err := s.sessionBiz.SessionInit(c.Context(), req)
|
||||
|
||||
return handRes(c, err, result)
|
||||
}
|
||||
|
||||
// SessionList 获取会话列表
|
||||
func (s *SessionService) SessionList(c *fiber.Ctx) error {
|
||||
req := &entitys.SessionListRequest{}
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sessionList, err := s.sessionBiz.SessionList(c.Context(), req)
|
||||
|
||||
return handRes(c, err, fiber.Map{
|
||||
"session_list": sessionList,
|
||||
})
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package test
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/entitys"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_task(t *testing.T) {
|
||||
var c entitys.TaskConfig
|
||||
config := `{"param": {"type": "object", "required": ["number"], "properties": {"number": {"type": "string", "description": "订单编号/流水号"}}}, "request": {"url": "http://www.baidu.com/${number}", "headers": {"Authorization": "${authorization}"}, "method": "GET"}}`
|
||||
err := json.Unmarshal([]byte(config), &c)
|
||||
t.Log(err)
|
||||
}
|
|
@ -1,7 +1,8 @@
|
|||
package tools
|
||||
|
||||
import (
|
||||
"ai_scheduler/pkg/types"
|
||||
"ai_scheduler/internal/entitys"
|
||||
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
@ -27,10 +28,10 @@ func (c *CalculatorTool) Description() string {
|
|||
}
|
||||
|
||||
// Definition 返回工具定义
|
||||
func (c *CalculatorTool) Definition() types.ToolDefinition {
|
||||
return types.ToolDefinition{
|
||||
func (c *CalculatorTool) Definition() entitys.ToolDefinition {
|
||||
return entitys.ToolDefinition{
|
||||
Type: "function",
|
||||
Function: types.FunctionDef{
|
||||
Function: entitys.FunctionDef{
|
||||
Name: c.Name(),
|
||||
Description: c.Description(),
|
||||
Parameters: map[string]interface{}{
|
||||
|
@ -65,11 +66,11 @@ type CalculateRequest struct {
|
|||
|
||||
// CalculateResponse 计算响应
|
||||
type CalculateResponse struct {
|
||||
Operation string `json:"operation"`
|
||||
A float64 `json:"a"`
|
||||
B float64 `json:"b"`
|
||||
Result float64 `json:"result"`
|
||||
Expression string `json:"expression"`
|
||||
Operation string `json:"operation"`
|
||||
A float64 `json:"a"`
|
||||
B float64 `json:"b"`
|
||||
Result float64 `json:"result"`
|
||||
Expression string `json:"expression"`
|
||||
}
|
||||
|
||||
// Execute 执行计算
|
||||
|
@ -117,4 +118,4 @@ func (c *CalculatorTool) Execute(ctx context.Context, args json.RawMessage) (int
|
|||
Result: result,
|
||||
Expression: expression,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,8 @@ package tools
|
|||
import (
|
||||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/constants"
|
||||
"ai_scheduler/pkg/types"
|
||||
"ai_scheduler/internal/entitys"
|
||||
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
@ -11,23 +12,23 @@ import (
|
|||
|
||||
// Manager 工具管理器
|
||||
type Manager struct {
|
||||
tools map[string]types.Tool
|
||||
tools map[string]entitys.Tool
|
||||
}
|
||||
|
||||
// NewManager 创建工具管理器
|
||||
func NewManager(config *config.ToolsConfig) *Manager {
|
||||
func NewManager(config *config.Config) *Manager {
|
||||
m := &Manager{
|
||||
tools: make(map[string]types.Tool),
|
||||
tools: make(map[string]entitys.Tool),
|
||||
}
|
||||
|
||||
// 注册天气工具
|
||||
if config.Weather.Enabled {
|
||||
if config.Tools.Weather.Enabled {
|
||||
weatherTool := NewWeatherTool()
|
||||
m.tools[weatherTool.Name()] = weatherTool
|
||||
}
|
||||
|
||||
// 注册计算器工具
|
||||
if config.Calculator.Enabled {
|
||||
if config.Tools.Calculator.Enabled {
|
||||
calcTool := NewCalculatorTool()
|
||||
m.tools[calcTool.Name()] = calcTool
|
||||
}
|
||||
|
@ -39,8 +40,8 @@ func NewManager(config *config.ToolsConfig) *Manager {
|
|||
// }
|
||||
|
||||
// 注册直连天下订单详情工具
|
||||
if config.ZltxOrderDetail.Enabled {
|
||||
zltxOrderDetailTool := NewZltxOrderDetailTool(config.ZltxOrderDetail)
|
||||
if config.Tools.ZltxOrderDetail.Enabled {
|
||||
zltxOrderDetailTool := NewZltxOrderDetailTool(config.Tools.ZltxOrderDetail)
|
||||
m.tools[zltxOrderDetailTool.Name()] = zltxOrderDetailTool
|
||||
}
|
||||
|
||||
|
@ -54,14 +55,14 @@ func NewManager(config *config.ToolsConfig) *Manager {
|
|||
}
|
||||
|
||||
// GetTool 获取工具
|
||||
func (m *Manager) GetTool(name string) (types.Tool, bool) {
|
||||
func (m *Manager) GetTool(name string) (entitys.Tool, bool) {
|
||||
tool, exists := m.tools[name]
|
||||
return tool, exists
|
||||
}
|
||||
|
||||
// GetAllTools 获取所有工具
|
||||
func (m *Manager) GetAllTools() []types.Tool {
|
||||
tools := make([]types.Tool, 0, len(m.tools))
|
||||
func (m *Manager) GetAllTools() []entitys.Tool {
|
||||
tools := make([]entitys.Tool, 0, len(m.tools))
|
||||
for _, tool := range m.tools {
|
||||
tools = append(tools, tool)
|
||||
}
|
||||
|
@ -69,8 +70,8 @@ func (m *Manager) GetAllTools() []types.Tool {
|
|||
}
|
||||
|
||||
// GetToolDefinitions 获取所有工具定义
|
||||
func (m *Manager) GetToolDefinitions(caller constants.Caller) []types.ToolDefinition {
|
||||
definitions := make([]types.ToolDefinition, 0, len(m.tools))
|
||||
func (m *Manager) GetToolDefinitions(caller constants.Caller) []entitys.ToolDefinition {
|
||||
definitions := make([]entitys.ToolDefinition, 0, len(m.tools))
|
||||
for _, tool := range m.tools {
|
||||
definitions = append(definitions, tool.Definition())
|
||||
}
|
||||
|
@ -89,8 +90,8 @@ func (m *Manager) ExecuteTool(ctx context.Context, name string, args json.RawMes
|
|||
}
|
||||
|
||||
// ExecuteToolCalls 执行多个工具调用
|
||||
func (m *Manager) ExecuteToolCalls(ctx context.Context, toolCalls []types.ToolCall) ([]types.ToolCall, error) {
|
||||
results := make([]types.ToolCall, len(toolCalls))
|
||||
func (m *Manager) ExecuteToolCalls(ctx context.Context, toolCalls []entitys.ToolCall) ([]entitys.ToolCall, error) {
|
||||
results := make([]entitys.ToolCall, len(toolCalls))
|
||||
|
||||
for i, toolCall := range toolCalls {
|
||||
results[i] = toolCall
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
package tools
|
||||
|
||||
import (
|
||||
"github.com/google/wire"
|
||||
)
|
||||
|
||||
var ProviderSetTools = wire.NewSet(NewManager)
|
|
@ -1,7 +1,8 @@
|
|||
package tools
|
||||
|
||||
import (
|
||||
"ai_scheduler/pkg/types"
|
||||
"ai_scheduler/internal/entitys"
|
||||
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
@ -30,10 +31,10 @@ func (w *WeatherTool) Description() string {
|
|||
}
|
||||
|
||||
// Definition 返回工具定义
|
||||
func (w *WeatherTool) Definition() types.ToolDefinition {
|
||||
return types.ToolDefinition{
|
||||
func (w *WeatherTool) Definition() entitys.ToolDefinition {
|
||||
return entitys.ToolDefinition{
|
||||
Type: "function",
|
||||
Function: types.FunctionDef{
|
||||
Function: entitys.FunctionDef{
|
||||
Name: w.Name(),
|
||||
Description: w.Description(),
|
||||
Parameters: map[string]interface{}{
|
||||
|
|
|
@ -2,7 +2,8 @@ package tools
|
|||
|
||||
import (
|
||||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/pkg/types"
|
||||
"ai_scheduler/internal/entitys"
|
||||
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
@ -30,10 +31,10 @@ func (w *ZltxOrderDetailTool) Description() string {
|
|||
}
|
||||
|
||||
// Definition 返回工具定义
|
||||
func (w *ZltxOrderDetailTool) Definition() types.ToolDefinition {
|
||||
return types.ToolDefinition{
|
||||
func (w *ZltxOrderDetailTool) Definition() entitys.ToolDefinition {
|
||||
return entitys.ToolDefinition{
|
||||
Type: "function",
|
||||
Function: types.FunctionDef{
|
||||
Function: entitys.FunctionDef{
|
||||
Name: w.Name(),
|
||||
Description: w.Description(),
|
||||
Parameters: map[string]interface{}{
|
||||
|
|
|
@ -0,0 +1,174 @@
|
|||
package dataTemp
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/pkg/mapstructure"
|
||||
"ai_scheduler/utils"
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/go-kratos/kratos/v2/log"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
type PrimaryKey struct {
|
||||
Id int `json:"id"`
|
||||
}
|
||||
|
||||
type GormDb struct {
|
||||
Client *gorm.DB
|
||||
}
|
||||
type contextTxKey struct{}
|
||||
|
||||
func (d *Db) DB(ctx context.Context) *gorm.DB {
|
||||
tx, ok := ctx.Value(contextTxKey{}).(*gorm.DB)
|
||||
if ok {
|
||||
return tx
|
||||
}
|
||||
return d.Db.Client
|
||||
}
|
||||
|
||||
func (t *Db) ExecTx(ctx context.Context, f func(ctx context.Context) error) error {
|
||||
return t.Db.Client.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
ctx = context.WithValue(ctx, contextTxKey{}, tx)
|
||||
return f(ctx)
|
||||
})
|
||||
}
|
||||
|
||||
type Db struct {
|
||||
Db *GormDb
|
||||
Log *log.Helper
|
||||
}
|
||||
|
||||
type DataTemp struct {
|
||||
Db *gorm.DB
|
||||
Model interface{}
|
||||
Do interface{}
|
||||
}
|
||||
|
||||
func NewDataTemp(db *utils.Db, model interface{}) *DataTemp {
|
||||
return &DataTemp{Db: db.Client, Model: model}
|
||||
}
|
||||
|
||||
func (k DataTemp) GetById(id int) (data map[string]interface{}, err error) {
|
||||
err = k.Db.Model(k.Model).Where("id = ?", id).Find(&data).Error
|
||||
if data == nil {
|
||||
err = sql.ErrNoRows
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (k DataTemp) Add(data interface{}) (id int, err error) {
|
||||
var primary *PrimaryKey
|
||||
add := k.Db.Model(k.Model).Create(data)
|
||||
_ = mapstructure.Decode(data, &primary)
|
||||
return primary.Id, add.Error
|
||||
}
|
||||
|
||||
func (k DataTemp) GetList(cond *builder.Cond, pageBoIn *ReqPageBo) (list []map[string]interface{}, pageBoOut *RespPageBo, err error) {
|
||||
var (
|
||||
query, _ = builder.ToBoundSQL(*cond)
|
||||
model = k.Db.Model(k.Model).Where(query)
|
||||
total int64
|
||||
)
|
||||
model.Count(&total)
|
||||
pageBoOut = pageBoOut.SetDataByReq(total, pageBoIn)
|
||||
model.Limit(pageBoIn.GetSize()).Offset(pageBoIn.GetOffset()).Order("updated_at desc").Find(&list)
|
||||
return
|
||||
}
|
||||
|
||||
func (k DataTemp) GetRange(cond *builder.Cond) (list []map[string]interface{}, err error) {
|
||||
var (
|
||||
query, _ = builder.ToBoundSQL(*cond)
|
||||
model = k.Db.Model(k.Model).Where(query)
|
||||
)
|
||||
err = model.Find(&list).Error
|
||||
return list, err
|
||||
}
|
||||
|
||||
func (k DataTemp) GetOneBySearch(cond *builder.Cond) (data map[string]interface{}, err error) {
|
||||
query, _ := builder.ToBoundSQL(*cond)
|
||||
err = k.Db.Model(k.Model).Where(query).Limit(1).Find(&data).Error
|
||||
if data == nil {
|
||||
err = sql.ErrNoRows
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (k DataTemp) GetOneBySearchToStrut(cond *builder.Cond, result interface{}) error {
|
||||
query, _ := builder.ToBoundSQL(*cond)
|
||||
err := k.Db.Model(k.Model).Where(query).Limit(1).Find(&result).Error
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (k DataTemp) GetListToStruct(cond *builder.Cond, pageBoIn *ReqPageBo, result interface{}, orderBy string) (pageBoOut *RespPageBo, err error) {
|
||||
var (
|
||||
query, _ = builder.ToBoundSQL(*cond)
|
||||
model = k.Db.Model(k.Model).Where(query)
|
||||
total int64
|
||||
)
|
||||
if len(orderBy) == 0 {
|
||||
orderBy = "updated_at desc"
|
||||
}
|
||||
// 1. 计算总数
|
||||
if err = model.Count(&total).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 2. 设置分页
|
||||
pageBoOut = pageBoOut.SetDataByReq(total, pageBoIn)
|
||||
|
||||
// 3. 查询数据(确保 result 是指针,如 &[]User)
|
||||
if err = model.Limit(pageBoIn.GetSize()).
|
||||
Offset(pageBoIn.GetOffset()).
|
||||
Order(orderBy).
|
||||
Find(result).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 4. 可选:用 reflect 处理结果(确保类型正确)
|
||||
val := reflect.ValueOf(result)
|
||||
if val.Kind() != reflect.Ptr {
|
||||
return nil, fmt.Errorf("result must be a pointer")
|
||||
}
|
||||
|
||||
val = val.Elem() // 解引用
|
||||
if val.Kind() == reflect.Slice {
|
||||
for i := 0; i < val.Len(); i++ {
|
||||
elem := val.Index(i)
|
||||
if elem.Kind() == reflect.Struct {
|
||||
// 示例:打印某个字段
|
||||
if field := elem.FieldByName("ID"); field.IsValid() {
|
||||
fmt.Println("ID:", field.Interface())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pageBoOut, nil
|
||||
}
|
||||
|
||||
func (k DataTemp) GetListToStruct2(cond *builder.Cond, pageBoIn *ReqPageBo, result interface{}) (pageBoOut *RespPageBo, err error) {
|
||||
var (
|
||||
query, _ = builder.ToBoundSQL(*cond)
|
||||
model = k.Db.Model(k.Model).Where(query)
|
||||
total int64
|
||||
)
|
||||
model.Count(&total)
|
||||
pageBoOut = pageBoOut.SetDataByReq(total, pageBoIn)
|
||||
model.Limit(pageBoIn.GetSize()).Offset(pageBoIn.GetOffset()).Order("updated_at desc").Find(&result)
|
||||
return
|
||||
}
|
||||
|
||||
func (k DataTemp) UpdateByCond(cond *builder.Cond, data interface{}) (err error) {
|
||||
var (
|
||||
query, _ = builder.ToBoundSQL(*cond)
|
||||
model = k.Db.Model(k.Model)
|
||||
)
|
||||
err = model.Where(query).Updates(data).Error
|
||||
return
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package dataTemp
|
||||
|
||||
// ReqPageBo 分页请求实体
|
||||
type ReqPageBo struct {
|
||||
Page int //页码,从第1页开始
|
||||
Limit int //分页大小
|
||||
}
|
||||
|
||||
// GetOffset 获取便宜量
|
||||
func (r *ReqPageBo) GetOffset() int {
|
||||
if r == nil {
|
||||
return 0
|
||||
}
|
||||
offset := (r.Page - 1) * r.Limit
|
||||
if offset < 0 {
|
||||
return 0
|
||||
}
|
||||
return offset
|
||||
}
|
||||
|
||||
// GetSize 获取分页
|
||||
func (r *ReqPageBo) GetSize() int {
|
||||
if r == nil {
|
||||
return 20
|
||||
}
|
||||
return r.Limit
|
||||
}
|
||||
|
||||
// GetNum 获取页码
|
||||
func (r *ReqPageBo) GetNum() int {
|
||||
if r == nil {
|
||||
return 1
|
||||
}
|
||||
return r.Page
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package dataTemp
|
||||
|
||||
// RespPageBo 分页响应实体
|
||||
type RespPageBo struct {
|
||||
Page int //页码
|
||||
Limit int //每页大小
|
||||
Total int64 //总数
|
||||
}
|
||||
|
||||
// SetDataByReq 通过req 设置响应参数
|
||||
func (r *RespPageBo) SetDataByReq(total int64, reqPage *ReqPageBo) *RespPageBo {
|
||||
resp := r
|
||||
if r == nil {
|
||||
resp = &RespPageBo{}
|
||||
}
|
||||
resp.Total = total
|
||||
if reqPage != nil {
|
||||
resp.Page = reqPage.Page
|
||||
resp.Limit = reqPage.Limit
|
||||
}
|
||||
return resp
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
package errcode
|
||||
|
||||
import (
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// SuccessMsg 自定义成功消息
|
||||
var SuccessMsg = "成功"
|
||||
var SuccessCode = 200
|
||||
|
||||
func SetSuccessMsg(msg string) {
|
||||
SuccessMsg = msg
|
||||
}
|
||||
|
||||
type BusinessErr struct {
|
||||
Code int32
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *BusinessErr) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
func (e *BusinessErr) GRPCStatus() *status.Status {
|
||||
var code = codes.Code(e.Code)
|
||||
return status.New(code, e.Message)
|
||||
}
|
||||
|
||||
// CustomErr 自定义错误
|
||||
func CustomErr(code int32, message string) *BusinessErr {
|
||||
return &BusinessErr{Code: code, Message: message}
|
||||
}
|
||||
|
||||
var (
|
||||
NotFoundErr = &BusinessErr{Code: 404, Message: "资源未找到"}
|
||||
ParamError = &BusinessErr{Code: 400, Message: "参数错误"}
|
||||
//未经授权
|
||||
NotAuth = &BusinessErr{Code: 401, Message: "未授权"}
|
||||
EnCrypt = &BusinessErr{Code: 401, Message: "加密失败"}
|
||||
DeCrypt = &BusinessErr{Code: 401, Message: "解密失败"}
|
||||
|
||||
//请求被禁止
|
||||
Forbidden = &BusinessErr{Code: 403, Message: "禁止访问"}
|
||||
WhiteIp = &BusinessErr{Code: 403, Message: "访问IP,不在白名单内"}
|
||||
|
||||
//系统错误
|
||||
SystemError = &BusinessErr{Code: 500, Message: "系统错误"}
|
||||
|
||||
ThirtyDayQueryLimit = &BusinessErr{Code: 420, Message: "只能查询最近31天的数据"}
|
||||
|
||||
GoodsSameErr = &BusinessErr{Code: 404, Message: "存在相同货品编码|货品名称的商品,请检查后重试"}
|
||||
|
||||
HadDefaultWareHouseErr = &BusinessErr{Code: 400, Message: "该商品已存在默认仓,请检查后重试"}
|
||||
|
||||
HadSameSupplierRelation = &BusinessErr{Code: 400, Message: "已存在相同的供应商商品关系,请检查后重试"}
|
||||
|
||||
AppRsaEncryptKeyNotFound = &BusinessErr{Code: 400, Message: "密钥缺失"}
|
||||
|
||||
AppRsaEncryptFail = &BusinessErr{Code: 400, Message: "Rsa加密失败"}
|
||||
|
||||
AppRsaDecryptKeyNotFound = &BusinessErr{Code: 400, Message: "密钥缺失"}
|
||||
|
||||
AppRsaDecryptFail = &BusinessErr{Code: 400, Message: "Rsa解密失败"}
|
||||
|
||||
AppSM2EncryptKeyNotFound = &BusinessErr{Code: 400, Message: "密钥缺失"}
|
||||
|
||||
AppSM2EncryptFail = &BusinessErr{Code: 400, Message: "Sm2加密失败"}
|
||||
|
||||
AppSM2DecryptKeyNotFound = &BusinessErr{Code: 400, Message: "密钥缺失"}
|
||||
|
||||
AppSM2DecryptFail = &BusinessErr{Code: 400, Message: "Sm2解密失败"}
|
||||
|
||||
AppSM4EncryptKeyNotFound = &BusinessErr{Code: 400, Message: "密钥缺失"}
|
||||
|
||||
AppSM4EncryptFail = &BusinessErr{Code: 400, Message: "Sm4加密失败"}
|
||||
|
||||
AppSM4DecryptKeyNotFound = &BusinessErr{Code: 400, Message: "密钥缺失"}
|
||||
|
||||
AppSM4DecryptFail = &BusinessErr{Code: 400, Message: "Sm4解密失败"}
|
||||
|
||||
BalanceNotEnough = &BusinessErr{Code: 400, Message: "余额不足"}
|
||||
|
||||
CusStatusException = &BusinessErr{Code: 400, Message: "客户状态异常"}
|
||||
|
||||
HadSameCusGoods = &BusinessErr{Code: 400, Message: "已存在相同的客户商品授权,请检查后重试"}
|
||||
)
|
|
@ -0,0 +1,25 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/pkg/utils_gorm"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Db struct {
|
||||
Client *gorm.DB
|
||||
}
|
||||
|
||||
func NewGormDb(c *config.Config) (*Db, func()) {
|
||||
transDBClient, mf := utils_gorm.DBConn(c.DB)
|
||||
//directDBClient, df := directDB(c, hLog)
|
||||
cleanup := func() {
|
||||
mf()
|
||||
//df()
|
||||
}
|
||||
return &Db{
|
||||
Client: transDBClient,
|
||||
//DirectDBClient: directDBClient,
|
||||
}, cleanup
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"github.com/google/wire"
|
||||
)
|
||||
|
||||
var ProviderUtils = wire.NewSet(
|
||||
NewRdb,
|
||||
NewGormDb,
|
||||
)
|
|
@ -0,0 +1,48 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/config"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
type Rdb struct {
|
||||
Rdb *redis.Client
|
||||
}
|
||||
|
||||
var rdb *Rdb
|
||||
|
||||
func NewRdb(c *config.Redis) *Rdb {
|
||||
if rdb == nil {
|
||||
//构建 redis
|
||||
rdbBuild := buildRdb(c)
|
||||
//退出时清理资源
|
||||
rdb = &Rdb{Rdb: rdbBuild}
|
||||
}
|
||||
//cleanup := func() {
|
||||
// if rdb != nil {
|
||||
// if err := rdb.Rdb.Close(); err != nil {
|
||||
// fmt.Println("关闭 redis 失败:%v", err)
|
||||
// }
|
||||
// }
|
||||
// fmt.Println("关闭 data 中的连接资源已完成")
|
||||
//}
|
||||
return rdb
|
||||
}
|
||||
|
||||
// buildRdb 构建redis client
|
||||
func buildRdb(c *config.Redis) *redis.Client {
|
||||
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: c.Host,
|
||||
Password: c.Pass,
|
||||
ReadTimeout: time.Duration(c.Tls) * time.Second,
|
||||
WriteTimeout: time.Duration(c.Tls) * time.Second,
|
||||
PoolSize: int(c.PoolSize),
|
||||
MinIdleConns: int(c.MaxIdle),
|
||||
ConnMaxIdleTime: time.Duration(c.MaxIdleTime) * time.Second,
|
||||
DB: int(c.Db),
|
||||
})
|
||||
return rdb
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package utils_gorm
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/config"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TransDB(c *config.DB) (*gorm.DB, func()) {
|
||||
mysqlConn, err := sql.Open(c.Driver, c.Source)
|
||||
gormDB, err := gorm.Open(
|
||||
mysql.New(mysql.Config{Conn: mysqlConn}),
|
||||
)
|
||||
|
||||
gormDB.Logger = NewCustomLogger(gormDB)
|
||||
if err != nil {
|
||||
panic("failed to connect database")
|
||||
}
|
||||
sqlDB, err := gormDB.DB()
|
||||
|
||||
// SetMaxIdleConns sets the maximum number of connections in the idle connection pool.
|
||||
sqlDB.SetMaxIdleConns(int(c.MaxIdle))
|
||||
|
||||
// SetMaxOpenConns sets the maximum number of open connections to the database.
|
||||
sqlDB.SetMaxOpenConns(int(c.MaxLifetime))
|
||||
|
||||
// SetConnMaxLifetime sets the maximum amount of time a connection may be reused.
|
||||
sqlDB.SetConnMaxLifetime(time.Hour)
|
||||
|
||||
return gormDB, func() {
|
||||
if mysqlConn != nil {
|
||||
fmt.Println("关闭 physicalGoodsDB")
|
||||
if err := mysqlConn.Close(); err != nil {
|
||||
fmt.Println("关闭 physicalGoodsDB 失败:", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
package utils_gorm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type CustomLogger struct {
|
||||
gormLogger logger.Interface
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewCustomLogger(db *gorm.DB) *CustomLogger {
|
||||
return &CustomLogger{
|
||||
gormLogger: logger.Default.LogMode(logger.Info),
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *CustomLogger) LogMode(level logger.LogLevel) logger.Interface {
|
||||
newlogger := *l
|
||||
newlogger.gormLogger = l.gormLogger.LogMode(level)
|
||||
return &newlogger
|
||||
}
|
||||
|
||||
func (l *CustomLogger) Info(ctx context.Context, msg string, data ...interface{}) {
|
||||
l.gormLogger.Info(ctx, msg, data...)
|
||||
}
|
||||
|
||||
func (l *CustomLogger) Warn(ctx context.Context, msg string, data ...interface{}) {
|
||||
l.gormLogger.Warn(ctx, msg, data...)
|
||||
}
|
||||
|
||||
func (l *CustomLogger) Error(ctx context.Context, msg string, data ...interface{}) {
|
||||
l.gormLogger.Error(ctx, msg, data...)
|
||||
}
|
||||
|
||||
func (l *CustomLogger) Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) {
|
||||
elapsed := time.Since(begin)
|
||||
sql, _ := fc()
|
||||
l.gormLogger.Trace(ctx, begin, fc, err)
|
||||
operation := extractOperation(sql)
|
||||
tableName := extractTableName(sql)
|
||||
fmt.Println(tableName)
|
||||
//// 将SQL语句保存到数据库
|
||||
if operation == 0 || tableName == "sql_log" {
|
||||
return
|
||||
}
|
||||
//go l.db.Model(&SqlLog{}).Create(&SqlLog{
|
||||
// OperatorID: 1,
|
||||
// OperatorName: "test",
|
||||
// SqlInfo: sql,
|
||||
// TableNames: tableName,
|
||||
// Type: operation,
|
||||
//})
|
||||
|
||||
// 如果有需要,也可以根据执行时间(elapsed)等条件过滤或处理日志记录
|
||||
if elapsed > time.Second {
|
||||
//l.gormLogger.Warn(ctx, "Slow SQL (> 1s): %s", sql)
|
||||
}
|
||||
}
|
||||
|
||||
// extractTableName extracts the table name from a SQL query, supporting quoted table names.
|
||||
func extractTableName(sql string) string {
|
||||
// 使用非捕获组匹配多种SQL操作关键词
|
||||
re := regexp.MustCompile(`(?i)\b(?:from|update|into|delete\s+from)\b\s+[\` + "`" + `"]?(\w+)[\` + "`" + `"]?`)
|
||||
match := re.FindStringSubmatch(sql)
|
||||
|
||||
// 检查是否匹配成功
|
||||
if len(match) > 1 {
|
||||
return match[1]
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// extractOperation extracts the operation type from a SQL query.
|
||||
func extractOperation(sql string) int32 {
|
||||
sql = strings.TrimSpace(strings.ToLower(sql))
|
||||
var operation int32
|
||||
if strings.HasPrefix(sql, "select") {
|
||||
operation = 0
|
||||
} else if strings.HasPrefix(sql, "insert") {
|
||||
operation = 1
|
||||
} else if strings.HasPrefix(sql, "update") {
|
||||
operation = 3
|
||||
} else if strings.HasPrefix(sql, "delete") {
|
||||
operation = 2
|
||||
}
|
||||
return operation
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package utils_sys_cache
|
||||
|
||||
import (
|
||||
"github.com/emirpasic/gods/maps/hashmap"
|
||||
"github.com/emirpasic/gods/maps/treemap"
|
||||
)
|
||||
|
||||
func NewCacheHashMap() *hashmap.Map {
|
||||
return hashmap.New()
|
||||
}
|
||||
|
||||
func NewCacheTreeMap() *treemap.Map {
|
||||
return treemap.NewWithIntComparator()
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/go-kratos/kratos/v2/log"
|
||||
"google.golang.org/protobuf/runtime/protoimpl"
|
||||
"gopkg.in/yaml.v3"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
baseconf "trans_hub/base_conf"
|
||||
"trans_hub/pkg"
|
||||
"trans_hub/pkg/mapstructure"
|
||||
)
|
||||
|
||||
const SPACE = "public"
|
||||
const PORT = 8848
|
||||
const User = ""
|
||||
const Pass = ""
|
||||
const IP = "192.168.110.93"
|
||||
const Group = "DEFAULT_GROUP"
|
||||
const DataId = "PG_BASE_CONFIG"
|
||||
|
||||
func TestConfig(t *testing.T) {
|
||||
type Nacos struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
Ip string `protobuf:"bytes,1,opt,name=ip,proto3" json:"ip,omitempty"`
|
||||
Port uint64 `protobuf:"varint,2,opt,name=port,proto3" json:"port,omitempty"`
|
||||
}
|
||||
type Conf struct {
|
||||
Nacos *Nacos `protobuf:"bytes,8,opt,name=nacos,proto3" json:"nacos,omitempty"`
|
||||
}
|
||||
var c Conf
|
||||
nc := &baseconf.Nacos{Ip: IP, Port: PORT, Space: SPACE, User: User, Password: Pass}
|
||||
|
||||
var s = ServerConfig(nc, Group, DataId)
|
||||
err := mapstructure.Decode(s, &c)
|
||||
t.Log(s, err)
|
||||
}
|
||||
|
||||
func TestMod(t *testing.T) {
|
||||
dir := pkg.GetRootPath()
|
||||
// 读取目录内容
|
||||
err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !d.IsDir() && filepath.Ext(path) == ".yaml" {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var result map[string]interface{}
|
||||
err = yaml.Unmarshal(data, &result) // 解析YAML到map中,使用gopkg.v3的yaml包或其他你选择的版本(例如encoding/yaml)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("File: %s\nContent: %+v\n", path, result)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestYaml(t *testing.T) {
|
||||
t.Log(GetBaseYaml())
|
||||
}
|
Loading…
Reference in New Issue