一个网页AI聊天,图像识别项目,使用到的技术栈有,Gin框架,GORM,rabbitmq,redis,eino框架,Vue框架等
项目地址:https://github.com/youngyangyang04/GopherAI
GopherAI第二版在第一版的基础上扩展了RAG服务,文件上传服务,MCP服务调用工具,TTS语言识别功能,下面进行一一梳理
由于引入了RAG,需要用户上传文件以作为检索和生成的知识来源,文件服务正式为此设计
路由层
新增一个api用于文件上传,也需要jwt鉴权
{
FileGroup := enterRouter.Group("/file")
FileGroup.Use(jwt.Auth())
FileRouter(FileGroup)
}
func FileRouter(r *gin.RouterGroup) {
r.POST("/upload", file.UploadRagFile)
}
控制器层
首先定义了api的响应结构体i,便于参数绑定,相比基础的api响应多了一个字符串用于表示文件上传到服务器后存放的路径
type (
UploadFileResponse struct {
FilePath string `json:"file_path,omitempty"`
controller.Response
}
)
然后就是handler函数了,从前端发来的请求中读取文件之后,调用服务层的file.UploadRagFile函数把文件上传到服务器
func UploadRagFile(c *gin.Context) {
res := new(UploadFileResponse)
uploadedFile, err := c.FormFile("file") // 从form-data里取文件
if err != nil {
log.Println("FormFile fail ", err)
c.JSON(http.StatusOK, res.CodeOf(code.CodeInvalidParams))
return
}
username := c.GetString("userName") // from jwt
if username == "" {
log.Println("Username not found in context")
c.JSON(http.StatusOK, res.CodeOf(code.CodeInvalidToken))
return
}
// 存储文件,indexer 会在 service 层根据实际文件名创建
filePath, err := file.UploadRagFile(username, uploadedFile)
if err != nil {
log.Println("UploadFile fail ", err)
c.JSON(http.StatusOK, res.CodeOf(code.CodeServerBusy))
return
}
res.Success()
res.FilePath = filePath
c.JSON(http.StatusOK, res)
}
服务层
文件上传服务只有一个核心函数UploadRagFile,首先是校验文件名和类型,随后为用户创建一个用户文件夹,用于存放用户的RAG文件,随后清空用户文件夹以及RedisSearch中的索引
生成最终存储的RAG文件名,命名规则:uuid+原始后缀,存放在用户文件夹
打开file,拷贝到用户RAG文件中
最后创建RAG索引器并对文件向量化,读取文件内容并创建向量索引
// 上传rag相关文件(这里只允许文本文件)
// 其实可以直接将其向量化进行保存,但这边依旧存储到服务器上以便后续可以在服务器上查看历史RAG文件
func UploadRagFile(username string, file *multipart.FileHeader) (string, error) {
// 校验文件类型和文件名,只允许文本文件
if err := utils.ValidateFile(file); err != nil {
log.Printf("File validation failed: %v", err)
return "", err
}
// 创建用户目录,存放该用户上传的RAG文件
userDir := filepath.Join("uploads", username)
if err := os.MkdirAll(userDir, 0755); err != nil {
log.Printf("Failed to create user directory %s: %v", userDir, err)
return "", err
}
// 删除用户目录中的所有现有文件及其索引(每个用户只能有一个文件)
files, err := os.ReadDir(userDir)
if err == nil {
for _, f := range files {
if !f.IsDir() {
filename := f.Name()
// 删除该文件对应的 RedisSearch 索引
if err := rag.DeleteIndex(context.Background(), filename); err != nil {
log.Printf("Failed to delete index for %s: %v", filename, err)
// 继续执行,不因为索引删除失败而中断文件上传
}
}
}
}
// 删除用户目录中的所有文件
if err := utils.RemoveAllFilesInDir(userDir); err != nil {
log.Printf("Failed to clean user directory %s: %v", userDir, err)
return "", err
}
// 生成UUID作为唯一文件名
uuid := utils.GenerateUUID()
ext := filepath.Ext(file.Filename) // 获取原文件后缀
filename := uuid + ext // RAG文件命名为uuid+源文件后缀
filePath := filepath.Join(userDir, filename) // 获取RAG文件路径
// 打开上传的文件
src, err := file.Open()
if err != nil {
log.Printf("Failed to open uploaded file: %v", err)
return "", err
}
defer src.Close()
// 创建用户的RAG文件
dst, err := os.Create(filePath)
if err != nil {
log.Printf("Failed to create destination file %s: %v", filePath, err)
return "", err
}
defer dst.Close()
// 拷贝数据
if _, err := io.Copy(dst, src); err != nil {
log.Printf("Failed to copy file content: %v", err)
return "", err
}
log.Printf("File uploaded successfully: %s", filePath)
// ==============================================
// 创建 RAG 索引器并对文件进行向量化
indexer, err := rag.NewRAGIndexer(filename, config.GetConfig().RagModelConfig.RagEmbeddingModel)
if err != nil {
log.Printf("Failed to create RAG indexer: %v", err)
// 删除已上传的文件
os.Remove(filePath)
return "", err
}
// 读取文件内容并创建向量索引
if err := indexer.IndexFile(context.Background(), filePath); err != nil {
log.Printf("Failed to index file: %v", err)
// 删除已上传的文件和索引
os.Remove(filePath)
rag.DeleteIndex(context.Background(), filename)
return "", err
}
log.Printf("File indexed successfully: %s", filename)
return filePath, nil
}
使用到的Utils
封装了一个删除文件夹所有文件的工具函数
// RemoveAllFilesInDir 删除目录中的所有文件(不删除子目录)
func RemoveAllFilesInDir(dir string) error {
entries, err := os.ReadDir(dir)
if err != nil {
if os.IsNotExist(err) {
return nil // 目录不存在就算了
}
return err
}
for _, entry := range entries {
if !entry.IsDir() { // 只删除文件,目录忽略
filePath := filepath.Join(dir, entry.Name())
if err := os.Remove(filePath); err != nil {
return err
}
}
}
return nil
}
检测RAG文件合法性的工具函数
// ValidateFile 校验文件是否为允许的文本文件(.md 或 .txt)
func ValidateFile(file *multipart.FileHeader) error {
// 校验文件扩展名
ext := strings.ToLower(filepath.Ext(file.Filename))
if ext != ".md" && ext != ".txt" {
return fmt.Errorf("文件类型不正确,只允许 .md 或 .txt 文件,当前扩展名: %s", ext)
}
return nil
} 


