feat: 百科知识库存入向量

This commit is contained in:
Blizzard
2026-04-21 17:32:26 +08:00
parent ae0020aa71
commit b2e6e511cd
21 changed files with 802 additions and 35 deletions
+65
View File
@@ -0,0 +1,65 @@
package plant
import (
"sundynix-go/global"
"sundynix-go/model/commom/response"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
type AiChatApi struct{}
// ChatStreamPlant 接收用户输入,基于植物百科 RAG 产生流式 SSE 回答
// @Summary 植物助手聊天(SSE 流式输出)
// @Tags Plant-AiChat
// @Param query query string true "用户提问内容"
// @Produce text/event-stream
// @Router /plant/chat/stream [get]
func (a *AiChatApi) ChatStreamPlant(c *gin.Context) {
query := c.Query("query")
if query == "" {
response.FailWithMsg("参数 query 不能为空", c)
return
}
// SSE Headers(微信小程序通过 enableChunked: true 配合实现打字机效果)
w := c.Writer
header := w.Header()
header.Set("Content-Type", "text/event-stream")
header.Set("Cache-Control", "no-cache")
header.Set("Connection", "keep-alive")
header.Set("Transfer-Encoding", "chunked")
w.WriteHeader(200)
err := aiRagService.PlantChatStreamRAG(c.Request.Context(), query, func(chunk string) error {
_, writeErr := w.WriteString("data: " + chunk + "\n\n")
w.Flush()
return writeErr
})
if err != nil {
global.Logger.Error("PlantChatStreamRAG error", zap.Error(err))
_, _ = w.WriteString("data: [ERROR]" + err.Error() + "\n\n")
w.Flush()
} else {
// 流结束标志
_, _ = w.WriteString("data: [DONE]\n\n")
w.Flush()
}
}
// SyncWikiToQdrant 手动触发全量植物百科同步到 Qdrant
// @Summary 同步植物百科数据到 Qdrant 向量库
// @Tags System-SysAiConfig
// @Produce json
// @Success 200 {object} response.Response
// @Router /plant/chat/sync [post]
func (a *AiChatApi) SyncWikiToQdrant(c *gin.Context) {
if err := aiRagService.SyncWikiToQdrant(); err != nil {
global.Logger.Error("SyncWikiToQdrant error", zap.Error(err))
response.FailWithMsg("同步失败: "+err.Error(), c)
return
}
response.OkWithMsg("同步成功", c)
}
+2
View File
@@ -14,6 +14,7 @@ type ApiGroup struct {
BadgeConfigApi BadgeConfigApi
CallbackApi CallbackApi
ExchangeApi ExchangeApi
AiChatApi
} }
var ( var (
@@ -28,4 +29,5 @@ var (
badgeConfigService = service.GroupApp.PlantServiceGroup.BadgeConfigService badgeConfigService = service.GroupApp.PlantServiceGroup.BadgeConfigService
callbackService = service.GroupApp.PlantServiceGroup.CallbackService callbackService = service.GroupApp.PlantServiceGroup.CallbackService
exchangeService = service.GroupApp.PlantServiceGroup.ExchangeService exchangeService = service.GroupApp.PlantServiceGroup.ExchangeService
aiRagService = service.GroupApp.PlantServiceGroup.AiRagService
) )
+42
View File
@@ -139,6 +139,48 @@ func (a *WikiApi) DeleteWiki(c *gin.Context) {
response.OkWithMsg("删除成功", c) response.OkWithMsg("删除成功", c)
} }
// SyncWikiQdrant 单条百科同步到 Qdrant
// @Tags 百科
// @Summary 百科同步到Qdrant
// @Security BearerAuth
// @Produce application/json
// @Param data body common.GetById true "单条百科"
// @Router /wiki/sync-qdrant [post]
func (a *WikiApi) SyncWikiQdrant(c *gin.Context) {
var req common.GetById
if err := c.ShouldBindJSON(&req); err != nil {
response.FailWithMsg("请求参数错误", c)
return
}
if err := aiRagService.SyncSingleWiki(req.ID); err != nil {
global.Logger.Error("同步 Qdrant 失败", zap.Error(err))
response.FailWithMsg("同步失败: "+err.Error(), c)
return
}
response.OkWithMsg("同步成功", c)
}
// DeleteWikiQdrant 从 Qdrant 移除单条百科向量
// @Tags 百科
// @Summary 百科移除Qdrant
// @Security BearerAuth
// @Produce application/json
// @Param data body common.GetById true "单条百科"
// @Router /wiki/delete-qdrant [post]
func (a *WikiApi) DeleteWikiQdrant(c *gin.Context) {
var req common.GetById
if err := c.ShouldBindJSON(&req); err != nil {
response.FailWithMsg("请求参数错误", c)
return
}
if err := aiRagService.DeleteFromQdrant(req.ID); err != nil {
global.Logger.Error("从 Qdrant 删除向量失败", zap.Error(err))
response.FailWithMsg("移除失败: "+err.Error(), c)
return
}
response.OkWithMsg("移除成功", c)
}
// StarWiki 收藏百科 // StarWiki 收藏百科
// @Tags 百科 // @Tags 百科
// @Summary 收藏百科 // @Summary 收藏百科
+2
View File
@@ -10,6 +10,7 @@ type ApiGroup struct {
MenuApi MenuApi
OperationRecordApi OperationRecordApi
OssApi OssApi
SysAiConfigApi
} }
var ( var (
@@ -20,4 +21,5 @@ var (
menuService = service.GroupApp.SystemServiceGroup.MenuService menuService = service.GroupApp.SystemServiceGroup.MenuService
operationRecordService = service.GroupApp.SystemServiceGroup.OperationRecordService operationRecordService = service.GroupApp.SystemServiceGroup.OperationRecordService
ossService = service.GroupApp.SystemServiceGroup.OssService ossService = service.GroupApp.SystemServiceGroup.OssService
sysAiConfigService = service.GroupApp.SystemServiceGroup.SysAiConfigService
) )
+98
View File
@@ -0,0 +1,98 @@
package system
import (
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"sundynix-go/global"
"sundynix-go/model/commom/response"
"sundynix-go/model/system"
)
type SysAiConfigApi struct{}
// CreateAiConfig 创建 AI 配置
// @Summary 创建 AI 配置
// @Tags System-SysAiConfig
// @accept json
// @Produce json
// @Param data body system.SysAiConfig true "配置模型"
// @Success 200 {object} response.Response
// @Router /aiConfig/create [post]
func (a *SysAiConfigApi) CreateAiConfig(c *gin.Context) {
var cfg system.SysAiConfig
if err := c.ShouldBindJSON(&cfg); err != nil {
response.FailWithMsg("参数错误:"+err.Error(), c)
return
}
if err := sysAiConfigService.Create(&cfg); err != nil {
global.Logger.Error("创建AI配置失败", zap.Error(err))
response.FailWithMsg("创建失败:"+err.Error(), c)
return
}
response.OkWithMsg("创建成功", c)
}
// UpdateAiConfig 更新 AI 配置
// @Summary 更新 AI 配置
// @Tags System-SysAiConfig
// @accept json
// @Produce json
// @Param data body system.SysAiConfig true "配置模型"
// @Success 200 {object} response.Response
// @Router /aiConfig/update [put]
func (a *SysAiConfigApi) UpdateAiConfig(c *gin.Context) {
var cfg system.SysAiConfig
if err := c.ShouldBindJSON(&cfg); err != nil {
response.FailWithMsg("参数错误:"+err.Error(), c)
return
}
if err := sysAiConfigService.Update(&cfg); err != nil {
global.Logger.Error("更新AI配置失败", zap.Error(err))
response.FailWithMsg("更新失败:"+err.Error(), c)
return
}
response.OkWithMsg("更新成功", c)
}
// SetActive 设置激活配置
// @Summary 设置激活配置(同一时间只有一条激活)
// @Tags System-SysAiConfig
// @accept json
// @Produce json
// @Param data body object true "{ \"id\": \"xxx\" }"
// @Success 200 {object} response.Response
// @Router /aiConfig/setActive [post]
func (a *SysAiConfigApi) SetActive(c *gin.Context) {
var body struct {
Id string `json:"id" binding:"required"`
}
if err := c.ShouldBindJSON(&body); err != nil {
response.FailWithMsg("参数错误:"+err.Error(), c)
return
}
if err := sysAiConfigService.SetActive(body.Id); err != nil {
global.Logger.Error("设置激活AI配置失败", zap.Error(err))
response.FailWithMsg("设置失败:"+err.Error(), c)
return
}
response.OkWithMsg("设置成功", c)
}
// GetList 获取配置列表
// @Summary 获取 AI 配置列表
// @Tags System-SysAiConfig
// @Produce json
// @Success 200 {object} response.Response
// @Router /aiConfig/list [get]
func (a *SysAiConfigApi) GetList(c *gin.Context) {
list, err := sysAiConfigService.GetList()
if err != nil {
global.Logger.Error("获取AI配置列表失败", zap.Error(err))
response.FailWithMsg("获取失败:"+err.Error(), c)
return
}
response.OkWithData(response.PageResult{
List: list,
Total: int64(len(list)),
}, c)
}
+13 -9
View File
@@ -13,8 +13,10 @@ require (
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/minio/minio-go/v7 v7.0.95 github.com/minio/minio-go/v7 v7.0.95
github.com/mojocn/base64Captcha v1.3.8 github.com/mojocn/base64Captcha v1.3.8
github.com/qdrant/go-client v1.17.1
github.com/redis/go-redis/v9 v9.7.3 github.com/redis/go-redis/v9 v9.7.3
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/sashabaranov/go-openai v1.41.2
github.com/spf13/viper v1.20.1 github.com/spf13/viper v1.20.1
github.com/swaggo/files v1.0.1 github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.1 github.com/swaggo/gin-swagger v1.6.1
@@ -22,8 +24,9 @@ require (
github.com/tencentyun/cos-go-sdk-v5 v0.7.70 github.com/tencentyun/cos-go-sdk-v5 v0.7.70
github.com/wechatpay-apiv3/wechatpay-go v0.2.21 github.com/wechatpay-apiv3/wechatpay-go v0.2.21
go.uber.org/zap v1.27.0 go.uber.org/zap v1.27.0
golang.org/x/crypto v0.45.0 golang.org/x/crypto v0.48.0
golang.org/x/sync v0.18.0 golang.org/x/sync v0.19.0
google.golang.org/grpc v1.78.0
gorm.io/driver/mysql v1.5.7 gorm.io/driver/mysql v1.5.7
gorm.io/driver/postgres v1.5.11 gorm.io/driver/postgres v1.5.11
gorm.io/gorm v1.26.0 gorm.io/gorm v1.26.0
@@ -75,7 +78,7 @@ require (
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/compress v1.18.4 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/mailru/easyjson v0.9.1 // indirect github.com/mailru/easyjson v0.9.1 // indirect
@@ -104,12 +107,13 @@ require (
golang.org/x/arch v0.21.0 // indirect golang.org/x/arch v0.21.0 // indirect
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
golang.org/x/image v0.26.0 // indirect golang.org/x/image v0.26.0 // indirect
golang.org/x/mod v0.29.0 // indirect golang.org/x/mod v0.32.0 // indirect
golang.org/x/net v0.47.0 // indirect golang.org/x/net v0.50.0 // indirect
golang.org/x/sys v0.38.0 // indirect golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.31.0 // indirect golang.org/x/text v0.34.0 // indirect
golang.org/x/tools v0.38.0 // indirect golang.org/x/tools v0.41.0 // indirect
google.golang.org/protobuf v1.36.9 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.64.0 // indirect modernc.org/libc v1.64.0 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect
+46 -18
View File
@@ -47,6 +47,10 @@ github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GM
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.22.0 h1:TmMhghgNef9YXxTu1tOopo+0BGEytxA+okbry0HjZsM= github.com/go-openapi/jsonpointer v0.22.0 h1:TmMhghgNef9YXxTu1tOopo+0BGEytxA+okbry0HjZsM=
github.com/go-openapi/jsonpointer v0.22.0/go.mod h1:xt3jV88UtExdIkkL7NloURjRQjbeUgcxFblMjq2iaiU= github.com/go-openapi/jsonpointer v0.22.0/go.mod h1:xt3jV88UtExdIkkL7NloURjRQjbeUgcxFblMjq2iaiU=
github.com/go-openapi/jsonreference v0.21.1 h1:bSKrcl8819zKiOgxkbVNRUBIr6Wwj9KYrDbMjRs0cDA= github.com/go-openapi/jsonreference v0.21.1 h1:bSKrcl8819zKiOgxkbVNRUBIr6Wwj9KYrDbMjRs0cDA=
@@ -96,6 +100,8 @@ github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJD
github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 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/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
@@ -123,8 +129,8 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 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 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
@@ -163,6 +169,8 @@ github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/qdrant/go-client v1.17.1 h1:7QmPwDddrHL3hC4NfycwtQlraVKRLcRi++BX6TTm+3g=
github.com/qdrant/go-client v1.17.1/go.mod h1:n1h6GhkdAzcohoXt/5Z19I2yxbCkMA6Jejob3S6NZT8=
github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM=
github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
@@ -176,6 +184,8 @@ github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
github.com/sashabaranov/go-openai v1.41.2 h1:vfPRBZNMpnqu8ELsclWcAvF19lDNgh1t6TVfFFOPiSM=
github.com/sashabaranov/go-openai v1.41.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= 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/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
@@ -218,6 +228,18 @@ github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2W
github.com/wechatpay-apiv3/wechatpay-go v0.2.21 h1:uIyMpzvcaHA33W/QPtHstccw+X52HO1gFdvVL9O6Lfs= github.com/wechatpay-apiv3/wechatpay-go v0.2.21 h1:uIyMpzvcaHA33W/QPtHstccw+X52HO1gFdvVL9O6Lfs=
github.com/wechatpay-apiv3/wechatpay-go v0.2.21/go.mod h1:A254AUBVB6R+EqQFo3yTgeh7HtyqRRtN2w9hQSOrd4Q= github.com/wechatpay-apiv3/wechatpay-go v0.2.21/go.mod h1:A254AUBVB6R+EqQFo3yTgeh7HtyqRRtN2w9hQSOrd4Q=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
@@ -231,8 +253,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY= golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
@@ -243,8 +265,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
@@ -254,8 +276,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -263,8 +285,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -276,8 +298,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 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.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -295,19 +317,25 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+1
View File
@@ -38,6 +38,7 @@ func MigrateTable() {
system.Menu{}, system.Menu{},
system.SysOperationRecord{}, system.SysOperationRecord{},
system.Oss{}, system.Oss{},
system.SysAiConfig{},
plant.MyPlant{}, //我的植物 plant.MyPlant{}, //我的植物
plant.CarePlan{}, //植物养护计划 plant.CarePlan{}, //植物养护计划
+7 -5
View File
@@ -41,11 +41,12 @@ func Routers() {
{ {
//需要鉴权的路由 //需要鉴权的路由
systemRouter.InitUserRouter(NeedAuthGroup) //用户相关 systemRouter.InitUserRouter(NeedAuthGroup) //用户相关
systemRouter.InitClientRouter(NeedAuthGroup) //客户端相关 systemRouter.InitClientRouter(NeedAuthGroup) //客户端相关
systemRouter.InitRoleRouter(NeedAuthGroup) //角色相关 systemRouter.InitRoleRouter(NeedAuthGroup) //角色相关
systemRouter.InitMenuRouter(NeedAuthGroup) //菜单相关 systemRouter.InitMenuRouter(NeedAuthGroup) //菜单相关
systemRouter.InitOssRouter(NeedAuthGroup) //OSS相关 systemRouter.InitOssRouter(NeedAuthGroup) //OSS相关
systemRouter.InitSysAiConfigRouter(NeedAuthGroup) //AI配置相关
} }
{ {
@@ -60,6 +61,7 @@ func Routers() {
plantGroup.InitBadgeConfigRouter(NeedAuthGroup) //徽章配置 plantGroup.InitBadgeConfigRouter(NeedAuthGroup) //徽章配置
plantGroup.InitUserProfileRouter(NeedAuthGroup) //用户资料 plantGroup.InitUserProfileRouter(NeedAuthGroup) //用户资料
plantGroup.InitExchangeRouter(NeedAuthGroup) //兑换中心 plantGroup.InitExchangeRouter(NeedAuthGroup) //兑换中心
plantGroup.InitAiChatRouter(NeedAuthGroup) //AI聊天
} }
+2 -1
View File
@@ -7,7 +7,8 @@ import (
type Wiki struct { type Wiki struct {
global.BaseModel global.BaseModel
IsHot int `json:"isHot" form:"isHot" gorm:"column:is_hot;comment:是否推荐植物"` IsHot int `json:"isHot" form:"isHot" gorm:"column:is_hot;comment:是否推荐植物"`
IsVectorSynced int `json:"isVectorSynced" form:"isVectorSynced" gorm:"column:is_vector_synced;type:tinyint;default:0;comment:是否已同步到向量库(0否1是)"`
//基本信息 //基本信息
Name string `json:"name" form:"name" gorm:"column:name;size:50;comment:名称"` Name string `json:"name" form:"name" gorm:"column:name;size:50;comment:名称"`
LatinName string `json:"latinName" form:"latinName" gorm:"size:100;column:latin_name;comment:拉丁名"` LatinName string `json:"latinName" form:"latinName" gorm:"size:100;column:latin_name;comment:拉丁名"`
+23
View File
@@ -0,0 +1,23 @@
package system
import "sundynix-go/global"
type SysAiConfig struct {
global.BaseModel
IsActive int `gorm:"column:is_active;type:tinyint;default:0;comment:是否激活(1是0否)" json:"isActive" form:"isActive"`
// Qdrant 向量库配置
QdrantUrl string `gorm:"column:qdrant_url;type:varchar(255);comment:Qdrant接口地址" json:"qdrantUrl" form:"qdrantUrl"`
QdrantApiKey string `gorm:"column:qdrant_api_key;type:varchar(255);comment:Qdrant密钥" json:"qdrantApiKey" form:"qdrantApiKey"`
QdrantCollection string `gorm:"column:qdrant_collection;type:varchar(100);comment:Qdrant集合名" json:"qdrantCollection" form:"qdrantCollection"`
VectorDimension int `gorm:"column:vector_dimension;type:int;comment:向量维度(默认104)" json:"vectorDimension" form:"vectorDimension"`
// 对话大模型配置(如 deepseek-chat、qwen-max、ollama 本地等)
ChatProvider string `gorm:"column:chat_provider;type:varchar(50);comment:对话模型供应商(deepseek/qwen/local等)" json:"chatProvider" form:"chatProvider"`
ChatApiUrl string `gorm:"column:chat_api_url;type:varchar(255);comment:对话模型接口地址" json:"chatApiUrl" form:"chatApiUrl"`
ChatApiKey string `gorm:"column:chat_api_key;type:varchar(255);comment:对话模型ApiKey" json:"chatApiKey" form:"chatApiKey"`
ChatModelName string `gorm:"column:chat_model_name;type:varchar(100);comment:对话模型名称" json:"chatModelName" form:"chatModelName"`
// Embedding 向量化模型配置(可与对话模型用不同供应商,如 bge-m3 本地 + deepseek 对话)
EmbeddingProvider string `gorm:"column:embedding_provider;type:varchar(50);comment:Embedding模型供应商" json:"embeddingProvider" form:"embeddingProvider"`
EmbeddingApiUrl string `gorm:"column:embedding_api_url;type:varchar(255);comment:Embedding模型接口地址" json:"embeddingApiUrl" form:"embeddingApiUrl"`
EmbeddingApiKey string `gorm:"column:embedding_api_key;type:varchar(255);comment:Embedding模型ApiKey" json:"embeddingApiKey" form:"embeddingApiKey"`
EmbeddingModelName string `gorm:"column:embedding_model_name;type:varchar(100);comment:Embedding模型名称" json:"embeddingModelName" form:"embeddingModelName"`
}
+15
View File
@@ -0,0 +1,15 @@
package plant
import "github.com/gin-gonic/gin"
type AiChatRouter struct{}
func (s *AiChatRouter) InitAiChatRouter(Router *gin.RouterGroup) (R gin.IRoutes) {
aiChatRouter := Router.Group("plant/chat")
aiChatApi := aiChatApi
{
aiChatRouter.GET("stream", aiChatApi.ChatStreamPlant) // SSE 对话流
aiChatRouter.POST("sync", aiChatApi.SyncWikiToQdrant) // 后台知识库同步
}
return aiChatRouter
}
+2
View File
@@ -14,6 +14,7 @@ type RouterGroup struct {
UserProfileRouter UserProfileRouter
CallbackRouter CallbackRouter
ExchangeRouter ExchangeRouter
AiChatRouter
} }
// 初始化路由 // 初始化路由
@@ -29,4 +30,5 @@ var (
badgeConfigApi = v1.ApiGroupApp.PlantApiGroup.BadgeConfigApi badgeConfigApi = v1.ApiGroupApp.PlantApiGroup.BadgeConfigApi
callbackApi = v1.ApiGroupApp.PlantApiGroup.CallbackApi callbackApi = v1.ApiGroupApp.PlantApiGroup.CallbackApi
exchangeApi = v1.ApiGroupApp.PlantApiGroup.ExchangeApi exchangeApi = v1.ApiGroupApp.PlantApiGroup.ExchangeApi
aiChatApi = v1.ApiGroupApp.PlantApiGroup.AiChatApi
) )
+2
View File
@@ -13,6 +13,8 @@ func (p *WikiRouter) InitWikiRouter(Router *gin.RouterGroup) {
wikiRouter.GET("/detail", wikiApi.WikiDetail) wikiRouter.GET("/detail", wikiApi.WikiDetail)
wikiRouter.POST("/delete", wikiApi.DeleteWiki) wikiRouter.POST("/delete", wikiApi.DeleteWiki)
wikiRouter.POST("/uploadImg", wikiApi.UploadImg) wikiRouter.POST("/uploadImg", wikiApi.UploadImg)
wikiRouter.POST("/sync-qdrant", wikiApi.SyncWikiQdrant)
wikiRouter.POST("/delete-qdrant", wikiApi.DeleteWikiQdrant)
//用户端 //用户端
wikiRouter.GET("/star", wikiApi.StarWiki) //收藏或者取消收藏 wikiRouter.GET("/star", wikiApi.StarWiki) //收藏或者取消收藏
+2
View File
@@ -10,6 +10,7 @@ type SysRouterGroup struct {
MenuRouter MenuRouter
OperationRecordRouter OperationRecordRouter
OssRouter OssRouter
SysAiConfigRouter
} }
// 初始化路由 // 初始化路由
@@ -21,4 +22,5 @@ var (
menuApi = v1.ApiGroupApp.SystemApiGroup.MenuApi menuApi = v1.ApiGroupApp.SystemApiGroup.MenuApi
operationRecordApi = v1.ApiGroupApp.SystemApiGroup.OperationRecordApi operationRecordApi = v1.ApiGroupApp.SystemApiGroup.OperationRecordApi
ossApi = v1.ApiGroupApp.SystemApiGroup.OssApi ossApi = v1.ApiGroupApp.SystemApiGroup.OssApi
sysAiConfigApi = v1.ApiGroupApp.SystemApiGroup.SysAiConfigApi
) )
+17
View File
@@ -0,0 +1,17 @@
package system
import "github.com/gin-gonic/gin"
type SysAiConfigRouter struct{}
func (s *SysAiConfigRouter) InitSysAiConfigRouter(Router *gin.RouterGroup) (R gin.IRoutes) {
sysAiConfigRouter := Router.Group("aiConfig")
sysAiConfigApi := sysAiConfigApi
{
sysAiConfigRouter.POST("create", sysAiConfigApi.CreateAiConfig) // 创建配置
sysAiConfigRouter.PUT("update", sysAiConfigApi.UpdateAiConfig) // 更新配置
sysAiConfigRouter.POST("setActive", sysAiConfigApi.SetActive) // 设置激活状态
sysAiConfigRouter.GET("list", sysAiConfigApi.GetList) // 获取列表
}
return sysAiConfigRouter
}
+395
View File
@@ -0,0 +1,395 @@
package plant
import (
"context"
"errors"
"fmt"
"io"
"strings"
"github.com/google/uuid"
qdrant "github.com/qdrant/go-client/qdrant"
openai "github.com/sashabaranov/go-openai"
"go.uber.org/zap"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
"sundynix-go/global"
plantModel "sundynix-go/model/plant"
"sundynix-go/model/system"
systemService "sundynix-go/service/system"
)
type AiRagService struct{}
var sysAiConfigService = systemService.SysAiConfigService{}
// ──────────────────────────────────────────────────────────────
// OpenAI 客户端构建
// ──────────────────────────────────────────────────────────────
func getChatClient(cfg *system.SysAiConfig) *openai.Client {
config := openai.DefaultConfig(cfg.ChatApiKey)
if cfg.ChatApiUrl != "" {
config.BaseURL = cfg.ChatApiUrl
}
return openai.NewClientWithConfig(config)
}
func getEmbeddingClient(cfg *system.SysAiConfig) *openai.Client {
config := openai.DefaultConfig(cfg.EmbeddingApiKey)
if cfg.EmbeddingApiUrl != "" {
config.BaseURL = cfg.EmbeddingApiUrl
}
return openai.NewClientWithConfig(config)
}
// ──────────────────────────────────────────────────────────────
// Qdrant gRPC 连接
// ──────────────────────────────────────────────────────────────
func newQdrantConn(cfg *system.SysAiConfig) (*grpc.ClientConn, context.Context, error) {
addr := strings.TrimPrefix(cfg.QdrantUrl, "http://")
addr = strings.TrimPrefix(addr, "https://")
conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return nil, nil, fmt.Errorf("qdrant grpc dial failed: %w", err)
}
ctx := context.Background()
if cfg.QdrantApiKey != "" {
ctx = metadata.NewOutgoingContext(ctx, metadata.Pairs("api-key", cfg.QdrantApiKey))
}
return conn, ctx, nil
}
// EnsureCollection 确保 Collection 存在,不存在则创建
func EnsureCollection(cfg *system.SysAiConfig) error {
conn, ctx, err := newQdrantConn(cfg)
if err != nil {
return err
}
defer conn.Close()
dim := uint64(cfg.VectorDimension)
if dim == 0 {
dim = 104
}
collClient := qdrant.NewCollectionsClient(conn)
if _, getErr := collClient.Get(ctx, &qdrant.GetCollectionInfoRequest{CollectionName: cfg.QdrantCollection}); getErr == nil {
return nil // 已存在
}
_, err = collClient.Create(ctx, &qdrant.CreateCollection{
CollectionName: cfg.QdrantCollection,
VectorsConfig: &qdrant.VectorsConfig{
Config: &qdrant.VectorsConfig_Params{
Params: &qdrant.VectorParams{Size: dim, Distance: qdrant.Distance_Cosine},
},
},
})
if err != nil {
return fmt.Errorf("qdrant create collection failed: %w", err)
}
global.Logger.Info("Qdrant collection created", zap.String("collection", cfg.QdrantCollection))
return nil
}
// wikiID → Qdrant point UUID(确保幂等)
func wikiToQdrantID(wikiId string) string {
return uuid.NewMD5(uuid.NameSpaceOID, []byte(wikiId)).String()
}
// buildWikiText 拼接用于向量化的文本语料
func buildWikiText(w plantModel.Wiki) string {
return fmt.Sprintf(
"植物名字:%s. 拉丁名:%s. 科属:%s. 生命周期:%s. 生长习性:%s. 病虫害:%s. 光照类型:%s. 最佳温度:%s.",
w.Name, w.LatinName, w.Genus, w.LifeCycle, w.GrowthHabit,
w.PestsDiseases, w.LightType, w.OptimalTempPeriod,
)
}
// ──────────────────────────────────────────────────────────────
// SyncSingleWikiAsync 异步同步单条百科到 Qdrant(新增/更新时调用)
// 同步成功后将 is_vector_synced 置为 1
// ──────────────────────────────────────────────────────────────
func (s *AiRagService) SyncSingleWikiAsync(wikiId string) {
go func() {
if err := s.syncSingleWiki(wikiId); err != nil {
global.Logger.Error("Async sync wiki to Qdrant failed", zap.String("wiki_id", wikiId), zap.Error(err))
}
}()
}
// SyncSingleWiki 同步同步单条百科到 Qdrant(用于API直接调用)
func (s *AiRagService) SyncSingleWiki(wikiId string) error {
return s.syncSingleWiki(wikiId)
}
func (s *AiRagService) syncSingleWiki(wikiId string) error {
cfg, err := sysAiConfigService.GetActiveAiConfig()
if err != nil {
return fmt.Errorf("no active ai config: %w", err)
}
if err = EnsureCollection(cfg); err != nil {
global.Logger.Warn("EnsureCollection warn", zap.Error(err))
}
var w plantModel.Wiki
if err = global.DB.Where("id = ?", wikiId).First(&w).Error; err != nil {
return fmt.Errorf("wiki not found: %w", err)
}
text := buildWikiText(w)
embClient := getEmbeddingClient(cfg)
embResp, err := embClient.CreateEmbeddings(context.Background(), openai.EmbeddingRequest{
Input: []string{text},
Model: openai.EmbeddingModel(cfg.EmbeddingModelName),
})
if err != nil {
return fmt.Errorf("embedding failed: %w", err)
}
conn, qdCtx, err := newQdrantConn(cfg)
if err != nil {
return err
}
defer conn.Close()
ptsClient := qdrant.NewPointsClient(conn)
_, err = ptsClient.Upsert(qdCtx, &qdrant.UpsertPoints{
CollectionName: cfg.QdrantCollection,
Points: []*qdrant.PointStruct{{
Id: qdrant.NewID(wikiToQdrantID(wikiId)),
Vectors: qdrant.NewVectors(embResp.Data[0].Embedding...),
Payload: map[string]*qdrant.Value{
"wiki_id": qdrant.NewValueString(w.Id),
"name": qdrant.NewValueString(w.Name),
"full_text": qdrant.NewValueString(text),
},
}},
})
if err != nil {
return fmt.Errorf("qdrant upsert failed: %w", err)
}
// 更新同步状态
_ = global.DB.Model(&plantModel.Wiki{}).Where("id = ?", wikiId).Update("is_vector_synced", 1).Error
global.Logger.Info("Wiki synced to Qdrant", zap.String("wiki_id", wikiId))
return nil
}
// ──────────────────────────────────────────────────────────────
// DeleteFromQdrant 从 Qdrant 删除单条植物的向量点位
// 删除成功后将 is_vector_synced 置为 0
// ──────────────────────────────────────────────────────────────
func (s *AiRagService) DeleteFromQdrant(wikiId string) error {
cfg, err := sysAiConfigService.GetActiveAiConfig()
if err != nil {
return fmt.Errorf("no active ai config: %w", err)
}
conn, qdCtx, err := newQdrantConn(cfg)
if err != nil {
return err
}
defer conn.Close()
ptsClient := qdrant.NewPointsClient(conn)
qID := wikiToQdrantID(wikiId)
_, err = ptsClient.Delete(qdCtx, &qdrant.DeletePoints{
CollectionName: cfg.QdrantCollection,
Points: &qdrant.PointsSelector{
PointsSelectorOneOf: &qdrant.PointsSelector_Points{
Points: &qdrant.PointsIdsList{
Ids: []*qdrant.PointId{qdrant.NewID(qID)},
},
},
},
})
if err != nil {
return fmt.Errorf("qdrant delete failed: %w", err)
}
_ = global.DB.Model(&plantModel.Wiki{}).Where("id = ?", wikiId).Update("is_vector_synced", 0).Error
global.Logger.Info("Wiki deleted from Qdrant", zap.String("wiki_id", wikiId))
return nil
}
// DeleteFromQdrantBatch 批量从 Qdrant 删除(用于批量删除百科时)
func (s *AiRagService) DeleteFromQdrantBatch(wikiIds []string) {
go func() {
cfg, err := sysAiConfigService.GetActiveAiConfig()
if err != nil {
global.Logger.Warn("No active ai config for batch qdrant delete", zap.Error(err))
return
}
conn, qdCtx, err := newQdrantConn(cfg)
if err != nil {
global.Logger.Warn("Qdrant connect failed for batch delete", zap.Error(err))
return
}
defer conn.Close()
ptsClient := qdrant.NewPointsClient(conn)
var ids []*qdrant.PointId
for _, wid := range wikiIds {
ids = append(ids, qdrant.NewID(wikiToQdrantID(wid)))
}
_, err = ptsClient.Delete(qdCtx, &qdrant.DeletePoints{
CollectionName: cfg.QdrantCollection,
Points: &qdrant.PointsSelector{
PointsSelectorOneOf: &qdrant.PointsSelector_Points{
Points: &qdrant.PointsIdsList{Ids: ids},
},
},
})
if err != nil {
global.Logger.Error("Qdrant batch delete failed", zap.Error(err))
} else {
global.Logger.Info("Qdrant batch delete done", zap.Int("count", len(wikiIds)))
}
}()
}
// ──────────────────────────────────────────────────────────────
// SyncWikiToQdrant 全量同步(后台操作/手动触发)
// ──────────────────────────────────────────────────────────────
func (s *AiRagService) SyncWikiToQdrant() error {
cfg, err := sysAiConfigService.GetActiveAiConfig()
if err != nil {
return err
}
if err = EnsureCollection(cfg); err != nil {
global.Logger.Warn("EnsureCollection failed, continuing", zap.Error(err))
}
var wikis []plantModel.Wiki
if err = global.DB.Find(&wikis).Error; err != nil {
return err
}
embClient := getEmbeddingClient(cfg)
conn, qdCtx, err := newQdrantConn(cfg)
if err != nil {
return err
}
defer conn.Close()
ptsClient := qdrant.NewPointsClient(conn)
var successIds []string
for _, w := range wikis {
text := buildWikiText(w)
embResp, embErr := embClient.CreateEmbeddings(context.Background(), openai.EmbeddingRequest{
Input: []string{text},
Model: openai.EmbeddingModel(cfg.EmbeddingModelName),
})
if embErr != nil {
global.Logger.Error("Embedding failed", zap.String("wiki_id", w.Id), zap.Error(embErr))
continue
}
_, upsertErr := ptsClient.Upsert(qdCtx, &qdrant.UpsertPoints{
CollectionName: cfg.QdrantCollection,
Points: []*qdrant.PointStruct{{
Id: qdrant.NewID(wikiToQdrantID(w.Id)),
Vectors: qdrant.NewVectors(embResp.Data[0].Embedding...),
Payload: map[string]*qdrant.Value{
"wiki_id": qdrant.NewValueString(w.Id),
"name": qdrant.NewValueString(w.Name),
"full_text": qdrant.NewValueString(text),
},
}},
})
if upsertErr != nil {
global.Logger.Error("Qdrant upsert failed", zap.String("wiki_id", w.Id), zap.Error(upsertErr))
} else {
successIds = append(successIds, w.Id)
}
}
// 批量更新同步状态
if len(successIds) > 0 {
_ = global.DB.Model(&plantModel.Wiki{}).Where("id IN ?", successIds).Update("is_vector_synced", 1).Error
}
global.Logger.Info("SyncWikiToQdrant done", zap.Int("total", len(wikis)), zap.Int("success", len(successIds)))
return nil
}
// ──────────────────────────────────────────────────────────────
// PlantChatStreamRAG 向量检索 + 大模型流式对话
// ──────────────────────────────────────────────────────────────
func (s *AiRagService) PlantChatStreamRAG(ctx context.Context, userQuery string, onData func(chunk string) error) error {
cfg, err := sysAiConfigService.GetActiveAiConfig()
if err != nil {
return err
}
embClient := getEmbeddingClient(cfg)
chatClient := getChatClient(cfg)
embResp, err := embClient.CreateEmbeddings(ctx, openai.EmbeddingRequest{
Input: []string{userQuery},
Model: openai.EmbeddingModel(cfg.EmbeddingModelName),
})
if err != nil {
return fmt.Errorf("向量化查询失败: %w", err)
}
conn, qdCtx, connErr := newQdrantConn(cfg)
var contextText string
if connErr == nil {
defer conn.Close()
ptsClient := qdrant.NewPointsClient(conn)
limit := uint64(3)
searchRes, searchErr := ptsClient.Search(qdCtx, &qdrant.SearchPoints{
CollectionName: cfg.QdrantCollection,
Vector: embResp.Data[0].Embedding,
Limit: limit,
WithPayload: &qdrant.WithPayloadSelector{
SelectorOptions: &qdrant.WithPayloadSelector_Enable{Enable: true},
},
})
if searchErr != nil {
global.Logger.Warn("Qdrant search failed, using empty context", zap.Error(searchErr))
} else {
for _, pt := range searchRes.GetResult() {
if txt, ok := pt.GetPayload()["full_text"]; ok {
contextText += txt.GetStringValue() + "\n"
}
}
}
} else {
global.Logger.Warn("Qdrant connect failed, skipping RAG", zap.Error(connErr))
}
systemPrompt := "你是一个专业的植物百科助手,请基于以下知识库信息回答用户问题。如果知识库无相关信息,结合你的通用知识作答。\n"
if contextText != "" {
systemPrompt += "--- 知识库 ---\n" + contextText + "\n--------------\n"
}
stream, err := chatClient.CreateChatCompletionStream(ctx, openai.ChatCompletionRequest{
Model: cfg.ChatModelName,
Messages: []openai.ChatCompletionMessage{
{Role: openai.ChatMessageRoleSystem, Content: systemPrompt},
{Role: openai.ChatMessageRoleUser, Content: userQuery},
},
Stream: true,
})
if err != nil {
return fmt.Errorf("大模型调用失败: %w", err)
}
defer stream.Close()
for {
resp, recvErr := stream.Recv()
if errors.Is(recvErr, io.EOF) {
break
}
if recvErr != nil {
return recvErr
}
if len(resp.Choices) > 0 {
if content := resp.Choices[0].Delta.Content; content != "" {
if writeErr := onData(content); writeErr != nil {
return writeErr
}
}
}
}
return nil
}
+1
View File
@@ -12,4 +12,5 @@ type ServiceGroup struct {
UserProfileService UserProfileService
CallbackService CallbackService
ExchangeService ExchangeService
AiRagService
} }
+17 -2
View File
@@ -11,13 +11,15 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
var aiRagService = AiRagService{}
type WikiService struct{} type WikiService struct{}
var WikiServiceApp = new(WikiClassService) var WikiServiceApp = new(WikiClassService)
// CreateWiki 创建百科 // CreateWiki 创建百科
func (s *WikiService) CreateWiki(req plantReq.CreateWiki) error { func (s *WikiService) CreateWiki(req plantReq.CreateWiki) error {
return global.DB.Transaction(func(tx *gorm.DB) error { err := global.DB.Transaction(func(tx *gorm.DB) error {
//1.先模糊查询name是否存在 如果存在 则返回错误 //1.先模糊查询name是否存在 如果存在 则返回错误
if !errors.Is(tx.Where("name like ?", "%"+req.Name+"%").First(&plant.Wiki{}).Error, gorm.ErrRecordNotFound) { if !errors.Is(tx.Where("name like ?", "%"+req.Name+"%").First(&plant.Wiki{}).Error, gorm.ErrRecordNotFound) {
return errors.New("植物已经存在") return errors.New("植物已经存在")
@@ -117,6 +119,14 @@ func (s *WikiService) CreateWiki(req plantReq.CreateWiki) error {
return nil return nil
}) })
if err == nil {
// 异步同步到 Qdrant(事务提交后,wiki.Id 已可用)
var created plant.Wiki
if dbErr := global.DB.Where("name = ?", req.Name).First(&created).Error; dbErr == nil {
aiRagService.SyncSingleWikiAsync(created.Id)
}
}
return err
} }
// UpdateWiki 修改百科 // UpdateWiki 修改百科
@@ -288,7 +298,7 @@ func (s *WikiService) UploadImg(req common.UploadOss) error {
// DeleteWiki 删除百科 // DeleteWiki 删除百科
func (s *WikiService) DeleteWiki(req common.IdsReq) error { func (s *WikiService) DeleteWiki(req common.IdsReq) error {
return global.DB.Transaction(func(tx *gorm.DB) error { err := global.DB.Transaction(func(tx *gorm.DB) error {
var imgIds []string var imgIds []string
tx.Table("sundynix_wiki_oss").Where("wiki_id IN ?", req.Ids).Pluck("oss_id", &imgIds) tx.Table("sundynix_wiki_oss").Where("wiki_id IN ?", req.Ids).Pluck("oss_id", &imgIds)
// 3. 物理删除图片记录本身 // 3. 物理删除图片记录本身
@@ -311,4 +321,9 @@ func (s *WikiService) DeleteWiki(req common.IdsReq) error {
//删除百科本身 //删除百科本身
return tx.Unscoped().Where("id IN ?", req.Ids).Delete(&plant.Wiki{}).Error return tx.Unscoped().Where("id IN ?", req.Ids).Delete(&plant.Wiki{}).Error
}) })
if err == nil {
// 异步清理 Qdrant 向量点位
aiRagService.DeleteFromQdrantBatch(req.Ids)
}
return err
} }
+1
View File
@@ -8,4 +8,5 @@ type ServiceGroup struct {
MenuService MenuService
OperationRecordService OperationRecordService
OssService OssService
SysAiConfigService
} }
+49
View File
@@ -0,0 +1,49 @@
package system
import (
"errors"
"go.uber.org/zap"
"sundynix-go/global"
"sundynix-go/model/system"
)
type SysAiConfigService struct{}
// Create 创建AI配置
func (s *SysAiConfigService) Create(cfg *system.SysAiConfig) error {
return global.DB.Create(cfg).Error
}
// Update 更新AI配置
func (s *SysAiConfigService) Update(cfg *system.SysAiConfig) error {
return global.DB.Updates(cfg).Error
}
// SetActive 设置激活配置
func (s *SysAiConfigService) SetActive(id string) error {
// 先将所有的设为 0
err := global.DB.Model(&system.SysAiConfig{}).Where("1 = 1").Update("is_active", 0).Error
if err != nil {
return err
}
// 再将指定的设为 1
return global.DB.Model(&system.SysAiConfig{}).Where("id = ?", id).Update("is_active", 1).Error
}
// GetActiveAiConfig 获取当前激活的AI配置
func (s *SysAiConfigService) GetActiveAiConfig() (*system.SysAiConfig, error) {
var cfg system.SysAiConfig
err := global.DB.Where("is_active = 1").First(&cfg).Error
if err != nil {
global.Logger.Error("No active AI Config found", zap.Error(err))
return nil, errors.New("无激活状态的AI配置")
}
return &cfg, nil
}
// GetList 获取所有配置
func (s *SysAiConfigService) GetList() ([]system.SysAiConfig, error) {
var list []system.SysAiConfig
err := global.DB.Order("created_at desc").Find(&list).Error
return list, err
}