Compare commits

...

14 Commits

Author SHA1 Message Date
wuchao addcf2d24d feat(internal): 实现网关服务并优化聊天功能
- 新增 Gateway 结构体和相关方法,用于管理客户端连接和消息分发
- 重构 ChatService,集成 Gateway 功能
- 添加客户端绑定 UID 功能,支持消息发送到指定用户
- 实现全局消息广播和单用户消息发送的 HTTP 接口
- 优化聊天消息处理逻辑,增加消息回显功能
2025-09-19 11:56:39 +08:00
wuchao 8a2411016c feat(internal): 实现网关服务并优化聊天功能
- 新增 Gateway 结构体和相关方法,用于管理客户端连接和消息分发
- 重构 ChatService,集成 Gateway 功能
- 添加客户端绑定 UID 功能,支持消息发送到指定用户
- 实现全局消息广播和单用户消息发送的 HTTP 接口
- 优化聊天消息处理逻辑,增加消息回显功能
2025-09-19 11:23:56 +08:00
renzhiyuan 068563b914 结构修改 2025-09-19 09:32:02 +08:00
wolter 303dd39cb3 feat: 合并分支 2025-09-18 18:30:35 +08:00
wolter c91e5519e9 Merge branch 'refs/heads/feature/fiber' into feature/session
# Conflicts:
#	internal/data/model/ai_chat_his.gen.go
2025-09-18 18:18:11 +08:00
renzhiyuan 99c397aabf 结构修改 2025-09-18 17:56:08 +08:00
wolter a43c19eb48 feat:session wire 2025-09-18 15:48:01 +08:00
wolter 11241af84c Merge branch 'refs/heads/feature/session' into feature/dev 2025-09-18 14:52:43 +08:00
wuchao 904b67a608 feat(data): 更新 ProviderImpl 以包含新实现
- 在 ProviderImpl 中添加了 NewSysImpl 和 NewTaskImpl
- 此更新扩展了数据层的功能,集成了新的系统和任务相关实现
2025-09-17 18:29:26 +08:00
renzhiyuan 949f80d417 结构修改 2025-09-17 17:35:42 +08:00
wolter ecaf2635b5 feat:session 2025-09-17 13:45:43 +08:00
renzhiyuan 2bb054940a 结构修改 2025-09-16 21:31:47 +08:00
renzhiyuan abb52eb1d3 结构修改 2025-09-16 17:14:01 +08:00
renzhiyuan b5e78cc244 结构修改 2025-09-16 10:04:44 +08:00
79 changed files with 9616 additions and 685 deletions

23
Makefile Normal file
View File

@ -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

View File

@ -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)))
}

View File

@ -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,
}
}

28
config/config.yaml Normal file
View File

@ -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

19
gen.sh Executable file
View File

@ -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
View File

@ -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
View File

@ -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=

View File

@ -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)
}

View File

@ -0,0 +1,5 @@
package biz
import "github.com/google/wire"
var ProviderSetBiz = wire.NewSet(NewAiRouterBiz, NewSessionBiz, NewChatHistoryBiz)

371
internal/biz/router.go Normal file
View File

@ -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
}

126
internal/biz/session.go Normal file
View File

@ -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
}

View File

@ -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 {

View File

@ -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 {

View File

@ -0,0 +1,16 @@
package constant
type ConnStatus int8
const (
ConnStatusClosed ConnStatus = iota
ConnStatusNormal
ConnStatusIgnore
)
type TaskType int32
const (
TaskTypeApi ConnStatus = iota + 1
TaskTypeKnowle
)

View File

@ -0,0 +1,3 @@
package constant
const ()

View File

@ -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())
}

216
internal/data/impl/base.go Normal file
View File

@ -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类型需实现具体的数据库模型如ZxCreditOrderZxCreditLog等
*/
// 定义受支持的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)
}
}

View File

@ -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))}
//}

View File

@ -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))
}
}
}

View File

@ -0,0 +1,7 @@
package impl
import (
"github.com/google/wire"
)
var ProviderImpl = wire.NewSet(NewSessionImpl, NewSysImpl, NewTaskImpl, NewChatImpl)

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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))}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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:类型1api,2:知识库" json:"type"` // 类型1api,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
}

View File

@ -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"`
}

View File

@ -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"`
}

View File

@ -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
}

104
internal/gateway/gateway.go Normal file
View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

8
internal/pkg/func.go Normal file
View File

@ -0,0 +1,8 @@
package pkg
import "encoding/json"
func JsonStringIgonErr(data interface{}) string {
dataByte, _ := json.Marshal(data)
return string(dataByte)
}

25
internal/pkg/gorm.go Normal file
View File

@ -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
}

View File

@ -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
}
}

View File

@ -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)
}
}
}

View File

@ -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

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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]
}

View File

@ -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

View File

@ -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)
}

View File

@ -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
}
}
}

View File

@ -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 = %vwant 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 = %vwant nil", err)
}
//验证测试值
if output, isOk := tt.output.(OutputTPointer); isOk {
if *output.Time != target {
t.Errorf("Decode output time = %vwant %v", *output.Time, target)
}
if output.Id != idValue {
t.Errorf("Decode output id = %vwant %v", output.Id, idValue)
}
}
if output, isOk := tt.output.(Output); isOk {
if output.Time != target {
t.Errorf("Decode output time = %vwant %v", output.Time, target)
}
if output.Id != idValue {
t.Errorf("Decode output id = %vwant %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 = %vwant 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 = %vwant nil", err)
}
//验证测试值
switch v := tt.output.(type) {
case Output[int]:
if int64(v.Time) != target {
t.Errorf("Decode output time = %vwant %v", v.Time, target)
}
if v.Id != idValue {
t.Errorf("Decode output id = %vwant %v", v.Id, idValue)
}
case Output[*int]:
if int64(*v.Time) != target {
t.Errorf("Decode output time = %vwant %v", v.Time, target)
}
if v.Id != idValue {
t.Errorf("Decode output id = %vwant %v", v.Id, idValue)
}
case Output[int32]:
if int64(v.Time) != target {
t.Errorf("Decode output time = %vwant %v", v.Time, target)
}
if v.Id != idValue {
t.Errorf("Decode output id = %vwant %v", v.Id, idValue)
}
case Output[*int32]:
if int64(*v.Time) != target {
t.Errorf("Decode output time = %vwant %v", v.Time, target)
}
if v.Id != idValue {
t.Errorf("Decode output id = %vwant %v", v.Id, idValue)
}
case Output[int64]:
if int64(v.Time) != target {
t.Errorf("Decode output time = %vwant %v", v.Time, target)
}
if v.Id != idValue {
t.Errorf("Decode output id = %vwant %v", v.Id, idValue)
}
case Output[*int64]:
if int64(*v.Time) != target {
t.Errorf("Decode output time = %vwant %v", v.Time, target)
}
if v.Id != idValue {
t.Errorf("Decode output id = %vwant %v", v.Id, idValue)
}
case Output[uint]:
if int64(v.Time) != target {
t.Errorf("Decode output time = %vwant %v", v.Time, target)
}
if v.Id != idValue {
t.Errorf("Decode output id = %vwant %v", v.Id, idValue)
}
case Output[*uint]:
if int64(*v.Time) != target {
t.Errorf("Decode output time = %vwant %v", v.Time, target)
}
if v.Id != idValue {
t.Errorf("Decode output id = %vwant %v", v.Id, idValue)
}
}
})
}
}

View File

@ -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,
)

48
internal/pkg/rds.go Normal file
View File

@ -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
}

View File

@ -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)
}
}
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

31
internal/server/http.go Normal file
View File

@ -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
}

View File

@ -0,0 +1,5 @@
package server
import "github.com/google/wire"
var ProviderSetServer = wire.NewSet(NewServers, NewHTTPServer)

View File

@ -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(),
})
}

13
internal/server/server.go Normal file
View File

@ -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,
}
}

33
internal/services/base.go Normal file
View File

@ -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)
}

133
internal/services/chat.go Normal file
View File

@ -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
}

View File

@ -0,0 +1,8 @@
package services
import (
"ai_scheduler/internal/gateway"
"github.com/google/wire"
)
var ProviderSetServices = wire.NewSet(NewChatService, NewSessionService, gateway.NewGateway)

View File

@ -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
}

View File

@ -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,
})
}

View File

@ -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)
}

View File

@ -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
}
}

View File

@ -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

View File

@ -0,0 +1,7 @@
package tools
import (
"github.com/google/wire"
)
var ProviderSetTools = wire.NewSet(NewManager)

View File

@ -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{}{

View File

@ -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{}{

174
tmpl/dataTemp/queryTempl.go Normal file
View File

@ -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
}

35
tmpl/dataTemp/req_page.go Normal file
View File

@ -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
}

View File

@ -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
}

86
tmpl/errcode/common.go Normal file
View File

@ -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: "已存在相同的客户商品授权,请检查后重试"}
)

25
utils/gorm.go Normal file
View File

@ -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
}

10
utils/provider_set.go Normal file
View File

@ -0,0 +1,10 @@
package utils
import (
"github.com/google/wire"
)
var ProviderUtils = wire.NewSet(
NewRdb,
NewGormDb,
)

48
utils/rds.go Normal file
View File

@ -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
}

41
utils/utils_gorm/gorm.go Normal file
View File

@ -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)
}
}
}
}

View File

@ -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
}

View File

@ -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()
}

72
utils/utils_test.go Normal file
View File

@ -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())
}