Repository: RaspberryCola/AI-Cloud-Go
Pull request #2: feat：优化项目代码，增加ollama本地embedding部署，增加docker-compose一键部署外部依赖
Author: login: HildaM
Created: 2025-04-22T13:30:10Z | Merged: 2025-04-22T15:07:31Z

    feat:
    1. 优化配置加载，移除env配置文件，统一使用config.yaml加载配置
    2. 增加ollama本地embedding，支持本地向量存储
    3. 补充docker-compose.yaml，支持一键部署。在 Mac 平台上测试通过
    4. 补充文档，方便新手快速上手


Diff:
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f2c6d8e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,107 @@
+# 编译生成的二进制文件
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+*.out
+
+# 测试文件
+*.test
+*.out
+coverage.txt
+
+# Go 专用
+*.o
+*.a
+*.so
+_obj
+_test
+*.[568vq]
+[568vq].out
+*.cgo1.go
+*.cgo2.c
+_cgo_defun.c
+_cgo_gotypes.go
+_cgo_export.*
+_testmain.go
+go.work
+
+# 依赖管理
+vendor/
+Godeps/
+go-build
+
+# 日志和临时文件
+logs/
+*.log
+tmp/
+temp/
+
+# 特定 IDE 和编辑器
+.idea/
+.vscode/
+*.swp
+*.swo
+*~
+.DS_Store
+
+# 环境和配置文件（注意不要排除 config 目录）
+.env
+.envrc
+env.sh
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+# 实际的配置文件（不排除示例文件）
+config.yaml
+config.yml
+config.json
+config.toml
+config.ini
+config.hcl
+config.props
+config.properties
+config/config.yaml
+config/config.yml
+config/config.json
+config/config.toml
+
+# 明确说明不排除 config 目录和示例文件
+!config/
+!*.example
+!*.example.*
+!*.sample
+!*.sample.*
+!*.template
+!*.template.*
+
+# 敏感信息和凭证
+*.pem
+*.key
+*.crt
+*.p12
+*.pfx
+*.der
+*.csr
+*.cer
+*.keystore
+*.jks
+*.p7b
+*.p7c
+*.secret
+*.password
+*.credentials
+*secret*
+*password*
+*credential*
+*token*
+*apikey*
+*api_key*
+
+# 其他可能的敏感文件
+*.bak
+*.backup
+
diff --git a/README.md b/README.md
index ad3e0ed..e3e46f4 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
 AI-Cloud-Go 是一个基于 Go 语言开发的智能云存储系统，提供文件存储、用户管理、知识库管理等功能，采用现代化的技术栈和架构设计。系统支持多种存储后端，并集成了向量数据库以支持智能检索功能。
 
 前端界面展示：<svg height="16" width="16" viewBox="0 0 16 16" fill="currentColor" style="display: inline-block; vertical-align: middle;">
-<path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path>
+<path fill-rule="evenodd" d="M8 0C3.58 0 0 0 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path>
 </svg> [AI-Cloud-Frontend](https://github.com/RaspberryCola/AI-Cloud-Frontend)
 
 
@@ -70,8 +70,140 @@ go mod download
 go run cmd/main.go
 ```
 
-## 开发计划
-- [ ] 消息队列：处理文件上传、文档解析；
-- [ ] 支持自定义LLM和Embedding模型；
-- [ ] Agent模块；
+# AI-Cloud-Go Docker 完整环境配置
 
+> 更多详情请看 [docs 文件夹](/docs/)
+
+## 前置条件
+- 已安装 Docker 和 Docker Compose
+- 基本了解 Docker 的使用
+
+## 服务组件
+本配置包含以下服务：
+- **MySQL**: 数据库服务器，配置 `ai_cloud` 数据库
+- **MinIO**: 对象存储服务，兼容 S3 协议，配置 `ai-cloud` 存储桶
+- **Milvus**: 向量数据库，用于存储和检索文档向量
+
+## 启动步骤
+
+1. 环境配置
+   - 修改 `config/config.yaml` 文件，配置嵌入模型和语言模型的API密钥
+   ```yaml
+   # 嵌入模型配置
+   embedding:
+     service: "remote" # remote 或 ollama
+     remote:
+       api_key: "sk-your-api-key"
+       model: "text-embedding-3-large"
+       base_url: "https://api.openai.com/v1"
+     
+     # Ollama嵌入模型配置
+     ollama:
+       url: "http://localhost:11434"
+       model: "mxbai-embed-large"
+
+   # 语言模型配置
+   llm:
+     service: "remote"
+     remote:
+       api_key: "your-llm-api-key"
+       model: "deepseek-chat"
+       base_url: "https://api.deepseek.com/v1"
+   ```
+
+2. 启动 Docker 容器
+   ```bash
+   docker-compose up -d
+   ```
+
+3. 检查服务状态
+   ```bash
+   docker ps
+   ```
+
+4. 运行项目
+   ```bash
+   go run cmd/main.go
+   ```
+
+5. 停止容器
+   ```bash
+   docker-compose down
+   ```
+
+## 配置详情
+
+### MySQL
+- 主机: localhost
+- 端口: 3306
+- 用户名: root
+- 密码: 123456
+- 数据库: ai_cloud
+
+### MinIO (对象存储)
+- 端点: localhost:9000
+- 管理控制台: http://localhost:9001
+- 访问密钥: minioadmin
+- 密钥: minioadmin
+- 存储桶: ai-cloud
+
+### Milvus (向量数据库)
+- 端点: localhost:19530
+- 管理界面: 需要额外安装Attu (Milvus官方GUI工具)
+  - 在端口9091只提供监控信息: http://localhost:9091/webui (Milvus 2.5.0+版本)
+  - 完整管理界面需安装Attu: `docker run -p 8000:3000 -e MILVUS_URL=localhost:19530 zilliz/attu:latest`
+  - 访问Attu: http://localhost:8000
+
+### 环境配置
+项目使用 `config/config.yaml` 文件配置第三方 AI 模型的访问，主要包括：
+
+1. **嵌入模型配置**
+   - 支持远程API服务（如OpenAI）和Ollama本地Embedding模型
+   - 详见 [EMBEDDING_CONFIG_README.md](./EMBEDDING_CONFIG_README.md)
+
+2. **语言模型配置**
+   - 用于知识库问答和智能处理
+   - 默认使用 DeepSeek 的 deepseek-chat 模型
+
+## 故障排除
+
+### 常见问题
+
+1. **程序启动卡住**
+   - 检查 Milvus 是否正常启动，查看日志 `docker logs milvus-standalone`
+   - 检查 `config/config.yaml` 文件是否配置了正确的 API 密钥
+   - 确保 MySQL 中已创建 ai_cloud 数据库
+
+2. **MinIO 连接问题**
+   - 检查 MinIO 服务是否正常运行
+   - 验证 config.yaml 中的 MinIO 配置是否与实际运行环境一致
+
+3. **向量数据库操作失败**
+   - 检查 Milvus 服务状态
+   - 确认向量维度设置是否与模型输出一致
+
+4. **Milvus 管理界面访问失败**
+   - Milvus 2.5以上版本在端口9091提供简易WebUI: http://localhost:9091/webui
+   - 完整的管理界面需要安装Attu工具: `docker run -p 8000:3000 -e MILVUS_URL=localhost:19530 zilliz/attu:latest`
+
+## 开发调试
+
+### API 测试
+项目启动后，可通过以下端点进行测试：
+- 用户注册: POST http://localhost:8080/api/users/register
+- 用户登录: POST http://localhost:8080/api/users/login
+- 文件上传: POST http://localhost:8080/api/files/upload
+- 更多 API 详见代码中的路由配置
+
+### 服务地址
+- 应用后端: http://localhost:8080
+- MinIO 控制台: http://localhost:9001
+- Milvus 管理界面: http://localhost:9091
+- Ollama 服务: http://localhost:11434 (如果启用)
+
+## 注意事项
+- 如果您已经在本地运行了 MySQL，可以直接使用 `mysql -u root -p < init.sql` 创建 ai_cloud 数据库
+- 首次启动时，Milvus 会自动创建必要的集合和索引，可能需要一些时间
+- 使用生产环境时，请替换配置文件中的示例 API 密钥为您自己的有效密钥
+- 配置 config.yaml 时确保数据库和存储服务的连接信息与 Docker 环境一致
+- Go主程序使用端口8080，Ollama服务使用端口11434，避免端口冲突
\ No newline at end of file
diff --git a/cmd/main.go b/cmd/main.go
index da42435..85f4dbd 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -10,17 +10,9 @@ import (
 	"ai-cloud/internal/service"
 	"context"
 	"github.com/gin-gonic/gin"
-	"github.com/joho/godotenv"
-	"log"
 )
 
 func main() {
-
-	err := godotenv.Load()
-	if err != nil {
-		log.Fatal("Error loading .env file")
-	}
-
 	config.InitConfig()
 
 	db, _ := database.InitDB()
diff --git a/config/config.go b/config/config.go
index 1aa4b66..70337a2 100644
--- a/config/config.go
+++ b/config/config.go
@@ -3,79 +3,15 @@ package config
 import (
 	"log"
 
+	"github.com/fsnotify/fsnotify"
 	"github.com/spf13/viper"
 )
 
-type ServerConfig struct {
-	Port string `mapstructure:"port"`
-}
-
-type DatabaseConfig struct {
-	Host     string `mapstructure:"host"`
-	Port     string `mapstructure:"port"`
-	User     string `mapstructure:"user"`
-	Password string `mapstructure:"password"`
-	Name     string `mapstructure:"name"`
-}
-
-type JWTConfig struct {
-	Secret          string `mapstructure:"secret"`
-	ExpirationHours int    `mapstructure:"expiration_hours"`
-}
-
-type MinioConfig struct {
-	Endpoint        string `mapstructure:"endpoint"`
-	Bucket          string `mapstructure:"bucket"`
-	AccessKeyID     string `mapstructure:"access_key_id"`
-	AccessKeySecret string `mapstructure:"access_key_secret"`
-	UseSSL          bool   `mapstructure:"use_ssl"`
-	Region          string `mapstructure:"region"`
-}
-
-type StorageConfig struct {
-	Type  string      `mapstructure:"type"` // local/oss/minio
-	Local LocalConfig `mapstructure:"local"`
-	OSS   OSSConfig   `mapstructure:"oss"`
-	Minio MinioConfig `mapstructure:"minio"`
-}
-
-type LocalConfig struct {
-	BaseDir string `mapstructure:"base_dir"` // 本地存储根目录（如 /data/storage）
-}
-
-type OSSConfig struct {
-	Endpoint        string `mapstructure:"endpoint"`
-	Bucket          string `mapstructure:"bucket"`
-	AccessKeyID     string `mapstructure:"access_key_id"`
-	AccessKeySecret string `mapstructure:"access_key_secret"`
-}
-
-type CORSConfig struct {
-	AllowOrigins     []string `mapstructure:"allow_origins"`
-	AllowMethods     []string `mapstructure:"allow_methods"`
-	AllowHeaders     []string `mapstructure:"allow_headers"`
-	ExposeHeaders    []string `mapstructure:"expose_headers"`
-	AllowCredentials bool     `mapstructure:"allow_credentials"`
-	MaxAge           string   `mapstructure:"max_age"` // 使用字符串表示时间，便于配置
-}
-
-type RAGConfig struct {
-	ChunkSize   int `mapstructure:"chunk_size"`
-	OverlapSize int `mapstructure:"overlap_size"`
-}
-
-type AppConfig struct {
-	Server   ServerConfig   `mapstructure:"server"`
-	Database DatabaseConfig `mapstructure:"database"`
-	JWT      JWTConfig      `mapstructure:"jwt"`
-	Storage  StorageConfig  `mapstructure:"storage"`
-	CORS     CORSConfig     `mapstructure:"cors"`
-	RAG      RAGConfig      `mapstructure:"rag"`
-}
-
 var AppConfigInstance *AppConfig
 
+// InitConfig 初始化配置
 func InitConfig() {
+	// 加载配置
 	viper.SetConfigName("config")
 	viper.SetConfigType("yaml")
 	viper.AddConfigPath("./config")
@@ -84,12 +20,21 @@ func InitConfig() {
 		log.Fatalf("Error reading config file: %v", err)
 	}
 
-	AppConfigInstance = &AppConfig{}
+	// 监听配置变化
+	viper.WatchConfig()
+	viper.OnConfigChange(func(e fsnotify.Event) {
+		if err := viper.Unmarshal(AppConfigInstance); err != nil {
+			log.Printf("loadConfig failed, unmarshal config err: %v", err)
+		}
+	})
+
+	// 解析配置
 	if err := viper.Unmarshal(AppConfigInstance); err != nil {
 		log.Fatalf("Unable to decode into struct: %v", err)
 	}
 }
 
+// GetConfig 获取配置
 func GetConfig() *AppConfig {
 	return AppConfigInstance
 }
diff --git a/config/config.yaml b/config/config.yaml.example
similarity index 58%
rename from config/config.yaml
rename to config/config.yaml.example
index 8f9b975..e0c4dd0 100644
--- a/config/config.yaml
+++ b/config/config.yaml.example
@@ -5,7 +5,7 @@ database:
   host: "localhost"
   port: "3306"
   user: "root"
-  password: "wyb"
+  password: "123456"
   name: "ai_cloud"
 
 jwt:
@@ -51,4 +51,27 @@ cors:
   expose_headers:
     - "Content-Length"
   allow_credentials: true
-  max_age: "12h"
\ No newline at end of file
+  max_age: "12h"
+
+# 嵌入模型配置
+embedding:
+  service: "ollama" # remote 或 ollama
+
+  # 远程嵌入模型配置（OpenAI API，当 service=remote 时使用）
+  remote:
+    api_key: "sk-example-embedding-key"
+    model: "text-embedding-3-large"
+    base_url: "https://api.openai.com/v1"
+
+  # Ollama嵌入模型配置（当 service=ollama 时使用）
+  ollama:
+    url: "http://localhost:11434"
+    model: "mxbai-embed-large"
+
+# 语言模型配置
+llm:
+  api_key: "your-key"
+  model: "deepseek-chat"
+  base_url: "https://api.deepseek.com/v1"
+  max_tokens: 10240
+  temperature: 0.7
\ No newline at end of file
diff --git a/config/types.go b/config/types.go
new file mode 100644
index 0000000..ca99f14
--- /dev/null
+++ b/config/types.go
@@ -0,0 +1,114 @@
+package config
+
+// ServerConfig 服务器配置
+type ServerConfig struct {
+	Port string `mapstructure:"port"`
+}
+
+// DatabaseConfig 数据库配置
+type DatabaseConfig struct {
+	Host     string `mapstructure:"host"`
+	Port     string `mapstructure:"port"`
+	User     string `mapstructure:"user"`
+	Password string `mapstructure:"password"`
+	Name     string `mapstructure:"name"`
+}
+
+// JWTConfig JWT配置
+type JWTConfig struct {
+	Secret          string `mapstructure:"secret"`
+	ExpirationHours int    `mapstructure:"expiration_hours"`
+}
+
+// MinioConfig Minio配置
+type MinioConfig struct {
+	Endpoint        string `mapstructure:"endpoint"`
+	Bucket          string `mapstructure:"bucket"`
+	AccessKeyID     string `mapstructure:"access_key_id"`
+	AccessKeySecret string `mapstructure:"access_key_secret"`
+	UseSSL          bool   `mapstructure:"use_ssl"`
+	Region          string `mapstructure:"region"`
+}
+
+// StorageConfig 存储配置
+type StorageConfig struct {
+	Type  string      `mapstructure:"type"` // local/oss/minio
+	Local LocalConfig `mapstructure:"local"`
+	OSS   OSSConfig   `mapstructure:"oss"`
+	Minio MinioConfig `mapstructure:"minio"`
+}
+
+// LocalConfig 本地存储配置
+type LocalConfig struct {
+	BaseDir string `mapstructure:"base_dir"` // 本地存储根目录（如 /data/storage）
+}
+
+// OSSConfig OSS配置
+type OSSConfig struct {
+	Endpoint        string `mapstructure:"endpoint"`
+	Bucket          string `mapstructure:"bucket"`
+	AccessKeyID     string `mapstructure:"access_key_id"`
+	AccessKeySecret string `mapstructure:"access_key_secret"`
+}
+
+// CORSConfig CORS配置
+type CORSConfig struct {
+	AllowOrigins     []string `mapstructure:"allow_origins"`
+	AllowMethods     []string `mapstructure:"allow_methods"`
+	AllowHeaders     []string `mapstructure:"allow_headers"`
+	ExposeHeaders    []string `mapstructure:"expose_headers"`
+	AllowCredentials bool     `mapstructure:"allow_credentials"`
+	MaxAge           string   `mapstructure:"max_age"` // 使用字符串表示时间，便于配置
+}
+
+// RAGConfig RAG配置
+type RAGConfig struct {
+	ChunkSize   int `mapstructure:"chunk_size"`
+	OverlapSize int `mapstructure:"overlap_size"`
+}
+
+// EmbeddingConfig 嵌入模型配置
+type EmbeddingConfig struct {
+	// 使用哪种嵌入模型: remote 或 ollama
+	Service string `mapstructure:"service"`
+
+	// 远程嵌入模型配置（如OpenAI API）
+	Remote RemoteEmbeddingConfig `mapstructure:"remote"`
+
+	// Ollama嵌入模型配置
+	Ollama OllamaEmbeddingConfig `mapstructure:"ollama"`
+}
+
+// RemoteEmbeddingConfig 远程嵌入模型配置
+type RemoteEmbeddingConfig struct {
+	APIKey  string `mapstructure:"api_key"`
+	Model   string `mapstructure:"model"`
+	BaseURL string `mapstructure:"base_url"`
+}
+
+// OllamaEmbeddingConfig Ollama嵌入模型配置
+type OllamaEmbeddingConfig struct {
+	URL   string `mapstructure:"url"`
+	Model string `mapstructure:"model"`
+}
+
+// LLMConfig 语言模型配置
+type LLMConfig struct {
+	APIKey      string  `mapstructure:"api_key"`
+	Model       string  `mapstructure:"model"`
+	BaseURL     string  `mapstructure:"base_url"`
+	MaxTokens   int     `mapstructure:"max_tokens"`
+	Temperature float32 `mapstructure:"temperature"`
+}
+
+// AppConfig 应用配置
+type AppConfig struct {
+	Server    ServerConfig    `mapstructure:"server"`
+	Database  DatabaseConfig  `mapstructure:"database"`
+	JWT       JWTConfig       `mapstructure:"jwt"`
+	Storage   StorageConfig   `mapstructure:"storage"`
+	CORS      CORSConfig      `mapstructure:"cors"`
+	RAG       RAGConfig       `mapstructure:"rag"`
+	Embedding EmbeddingConfig `mapstructure:"embedding"`
+	LLM       LLMConfig       `mapstructure:"llm"`
+}
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..d533dbf
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,121 @@
+version: '3'
+
+services:
+  # MySQL service - create ai_cloud database
+  mysql-init:
+    image: mysql:8.0
+    command: --init-file /data/application/init.sql
+    volumes:
+      - ./init.sql:/data/application/init.sql
+    environment:
+      MYSQL_ROOT_PASSWORD: 123456
+    networks:
+      - ai-cloud-network
+    restart: on-failure
+
+  # MinIO service
+  minio:
+    image: minio/minio
+    ports:
+      - "9000:9000"
+      - "9001:9001"
+    volumes:
+      - minio_data:/data
+    environment:
+      MINIO_ROOT_USER: minioadmin
+      MINIO_ROOT_PASSWORD: minioadmin
+    command: server /data --console-address ":9001"
+    networks:
+      - ai-cloud-network
+    healthcheck:
+      test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
+      interval: 30s
+      timeout: 20s
+      retries: 3
+
+  # Create MinIO bucket
+  minio-init:
+    image: minio/mc
+    depends_on:
+      - minio
+    entrypoint: >
+      /bin/sh -c "
+      /usr/bin/mc config host add myminio http://minio:9000 minioadmin minioadmin;
+      /usr/bin/mc mb myminio/ai-cloud;
+      exit 0;
+      "
+    networks:
+      - ai-cloud-network
+
+  # Milvus standalone service
+  etcd:
+    container_name: milvus-etcd
+    image: quay.io/coreos/etcd:v3.5.5
+    environment:
+      - ETCD_AUTO_COMPACTION_MODE=revision
+      - ETCD_AUTO_COMPACTION_RETENTION=1000
+      - ETCD_QUOTA_BACKEND_BYTES=4294967296
+      - ETCD_SNAPSHOT_COUNT=50000
+    volumes:
+      - etcd_data:/etcd
+    command: etcd -advertise-client-urls=http://127.0.0.1:2379 -listen-client-urls http://0.0.0.0:2379 --data-dir /etcd
+    networks:
+      - ai-cloud-network
+
+  minio-for-milvus:
+    container_name: milvus-minio
+    image: minio/minio:RELEASE.2023-03-20T20-16-18Z
+    environment:
+      MINIO_ACCESS_KEY: minioadmin
+      MINIO_SECRET_KEY: minioadmin
+    volumes:
+      - minio_data_for_milvus:/minio_data
+    command: minio server /minio_data
+    healthcheck:
+      test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
+      interval: 30s
+      timeout: 20s
+      retries: 3
+    networks:
+      - ai-cloud-network
+
+  standalone:
+    container_name: milvus-standalone
+    image: milvusdb/milvus:v2.4.1
+    command: ["milvus", "run", "standalone"]
+    environment:
+      ETCD_ENDPOINTS: etcd:2379
+      MINIO_ADDRESS: minio-for-milvus:9000
+    volumes:
+      - milvus_data:/var/lib/milvus
+    ports:
+      - "19530:19530"
+      - "9091:9091"
+    depends_on:
+      - etcd
+      - minio-for-milvus
+    networks:
+      - ai-cloud-network
+
+  # Attu - Milvus管理界面
+  attu:
+    container_name: milvus-attu
+    image: zilliz/attu:latest
+    environment:
+      MILVUS_URL: standalone:19530
+    ports:
+      - "8000:3000"
+    depends_on:
+      - standalone
+    networks:
+      - ai-cloud-network
+
+volumes:
+  minio_data:
+  etcd_data:
+  minio_data_for_milvus:
+  milvus_data:
+
+networks:
+  ai-cloud-network:
+    driver: bridge 
\ No newline at end of file
diff --git a/docs/CONFIG_README.md b/docs/CONFIG_README.md
new file mode 100644
index 0000000..05e3b42
--- /dev/null
+++ b/docs/CONFIG_README.md
@@ -0,0 +1,188 @@
+# AI-Cloud-Go 配置指南
+
+## 配置迁移说明
+
+我们已将项目的所有配置从环境变量(`.env`文件)迁移到YAML配置文件(`config/config.yaml`)中，以统一配置管理方式，提高可维护性。
+
+### 主要变更
+
+1. 将嵌入模型(Embedding)相关配置从环境变量迁移到YAML配置
+2. 将语言模型(LLM)相关配置从环境变量迁移到YAML配置
+3. 更新了相关服务实现，使用配置文件而非环境变量
+
+### 配置文件结构
+
+配置文件(`config/config.yaml`)结构如下：
+
+```yaml
+server:
+  port: "8080"
+
+database:
+  host: "localhost"
+  port: "3306"
+  user: "root"
+  password: "123456"
+  name: "ai_cloud"
+
+jwt:
+  secret: "your-jwt-secret"
+  expiration_hours: 24
+
+storage:
+  type: "minio" # local, oss, minio
+  local:
+    base_dir: "./storage_data"
+  oss:
+    endpoint: "oss-endpoint"
+    bucket: "bucket-name"
+    access_key_id: ""
+    access_key_secret: ""
+  minio:
+    endpoint: "localhost:9000"
+    bucket: "ai-cloud"
+    access_key_id: "minioadmin"
+    access_key_secret: "minioadmin"
+    use_ssl: false
+    region: ""
+
+rag:
+  chunk_size: 1500
+  overlap_size: 500
+
+cors:
+  # CORS配置...
+
+# 嵌入模型配置
+embedding:
+  service: "ollama" # remote, local, ollama
+
+  # 远程嵌入模型配置（OpenAI API，当 service=remote 时使用）
+  remote:
+    api_key: "your-api-key"
+    model: "text-embedding-3-large"
+    base_url: "https://api.openai.com/v1"
+
+  # 本地FastAPI嵌入模型配置（当 service=local 时使用）
+  local:
+    url: "http://embedding-api:8000"
+    url_host: "http://localhost:8008"
+    model: "paraphrase-multilingual-MiniLM-L12-v2"
+    dimension: 384
+
+  # Ollama嵌入模型配置（当 service=ollama 时使用）
+  ollama:
+    url: "http://localhost:11434"
+    model: "mxbai-embed-large"
+
+# 语言模型配置
+llm:
+  api_key: "your-llm-api-key"
+  model: "deepseek-chat"
+  base_url: "https://api.deepseek.com/v1"
+  max_tokens: 4096
+  temperature: 0.7
+```
+
+## 配置使用
+
+在Go代码中，可以通过以下方式访问配置：
+
+```go
+import "ai-cloud/config"
+
+func main() {
+    // 初始化配置（在应用启动时调用一次）
+    config.InitConfig()
+    
+    // 获取配置实例
+    cfg := config.GetConfig()
+    
+    // 访问配置项
+    port := cfg.Server.Port
+    embeddingService := cfg.Embedding.Service
+    llmModel := cfg.LLM.Model
+    
+    // ...
+}
+```
+
+## 嵌入服务配置
+
+### 远程API服务 (OpenAI)
+
+使用 OpenAI API 进行文本向量嵌入：
+
+```yaml
+embedding:
+  service: "remote"
+  remote:
+    api_key: "your-api-key"
+    model: "text-embedding-3-large"
+    base_url: "https://api.openai.com/v1"
+```
+
+### 本地FastAPI服务
+
+使用本地部署的FastAPI服务进行文本向量嵌入：
+
+```yaml
+embedding:
+  service: "local"
+  local:
+    url: "http://embedding-api:8000"  # Docker网络内部地址
+    url_host: "http://localhost:8008" # 宿主机访问地址
+    model: "paraphrase-multilingual-MiniLM-L12-v2"
+    dimension: 384
+```
+
+### Ollama本地服务
+
+使用Ollama本地服务进行文本向量嵌入：
+
+```yaml
+embedding:
+  service: "ollama"
+  ollama:
+    url: "http://localhost:11434"
+    model: "mxbai-embed-large"
+```
+
+## 语言模型配置
+
+配置LLM服务：
+
+```yaml
+llm:
+  api_key: "your-api-key"
+  model: "deepseek-chat"  # 或其他支持的模型
+  base_url: "https://api.deepseek.com/v1"
+  max_tokens: 4096
+  temperature: 0.7
+```
+
+## 从环境变量迁移到配置文件
+
+如果您之前使用`.env`文件配置项目，请按照以下对应关系迁移到`config.yaml`：
+
+| 环境变量 | 配置文件路径 |
+|---------|------------|
+| `EMBEDDING_SERVICE` | `embedding.service` |
+| `EMBEDDING_API_KEY` | `embedding.remote.api_key` |
+| `EMBEDDING_MODEL` | `embedding.remote.model` |
+| `EMBEDDING_BASE_URL` | `embedding.remote.base_url` |
+| `LOCAL_EMBEDDING_URL` | `embedding.local.url` |
+| `LOCAL_EMBEDDING_URL_HOST` | `embedding.local.url_host` |
+| `LOCAL_EMBEDDING_MODEL` | `embedding.local.model` |
+| `LOCAL_EMBEDDING_DIM` | `embedding.local.dimension` |
+| `OLLAMA_URL` | `embedding.ollama.url` |
+| `OLLAMA_EMBEDDING_MODEL` | `embedding.ollama.model` |
+| `LLM_API_KEY` | `llm.api_key` |
+| `LLM_MODEL` | `llm.model` |
+| `LLM_BASE_URL` | `llm.base_url` |
+
+## 注意事项
+
+1. 配置文件中的敏感信息（如API密钥）不应提交到版本控制系统
+2. 可以考虑使用环境变量覆盖配置文件中的敏感信息
+3. 为不同环境（开发、测试、生产）准备不同的配置文件 
\ No newline at end of file
diff --git a/docs/EMBEDDING_CONFIG_README.md b/docs/EMBEDDING_CONFIG_README.md
new file mode 100644
index 0000000..abec502
--- /dev/null
+++ b/docs/EMBEDDING_CONFIG_README.md
@@ -0,0 +1,87 @@
+# 嵌入服务配置说明
+
+## 嵌入服务架构变更
+
+我们已经移除了本地 FastAPI 嵌入服务（embedding-service），现在嵌入功能仅支持以下两种方式：
+
+1. 远程 API 嵌入服务（如 OpenAI API）
+2. 本地 Ollama 嵌入服务
+
+这种简化的架构降低了系统复杂度，减少了依赖，同时保留了最常用的两种嵌入方式。
+
+## 配置文件
+
+所有嵌入相关配置都集中在 `config/config.yaml` 文件中：
+
+```yaml
+# 嵌入模型配置
+embedding:
+  service: "ollama" # remote 或 ollama
+
+  # 远程嵌入模型配置（OpenAI API，当 service=remote 时使用）
+  remote:
+    api_key: "sk-example-embedding-key"
+    model: "text-embedding-3-large"
+    base_url: "https://api.openai.com/v1"
+
+  # Ollama嵌入模型配置（当 service=ollama 时使用）
+  ollama:
+    url: "http://localhost:11434"
+    model: "mxbai-embed-large"
+```
+
+## 配置说明
+
+### 远程 API 服务
+
+使用 OpenAI 或其他兼容 API 的远程嵌入服务：
+
+```yaml
+embedding:
+  service: "remote"
+  remote:
+    api_key: "your-api-key"
+    model: "text-embedding-3-large"
+    base_url: "https://api.openai.com/v1"
+```
+
+* `api_key`: 您的 API 密钥
+* `model`: 使用的嵌入模型名称
+* `base_url`: API 基础 URL，可以替换为其他兼容 OpenAI API 的服务提供商
+
+### Ollama 本地服务
+
+使用 Ollama 在本地运行嵌入模型：
+
+```yaml
+embedding:
+  service: "ollama"
+  ollama:
+    url: "http://localhost:11434"
+    model: "mxbai-embed-large"
+```
+
+* `url`: Ollama 服务的 URL，默认为 http://localhost:11434
+* `model`: 使用的 Ollama 模型名称，默认为 mxbai-embed-large
+
+## Ollama 设置指南
+
+要使用 Ollama 嵌入服务，请按照以下步骤操作：
+
+1. 从 [Ollama 官网](https://ollama.com/download) 下载并安装 Ollama
+2. 运行以下命令拉取嵌入模型：
+   ```bash
+   ollama pull mxbai-embed-large
+   ```
+3. 启动 Ollama 服务：
+   ```bash
+   OLLAMA_HOST="0.0.0.0" OLLAMA_ORIGINS="*" OLLAMA_KEEP_ALIVE="24h" ollama serve
+   ```
+4. 在配置文件中设置 `embedding.service` 为 `"ollama"`
+
+## 性能对比
+
+| 服务类型 | 向量维度 | 本地资源占用 | 网络依赖 | API密钥 | 适用场景 |
+|---------|---------|------------|---------|---------|---------|
+| 远程 API | 1536 | 无 | 需要稳定网络 | 需要 | 生产环境，精确向量化 |
+| Ollama | 1024 | 约1.2GB | 不需要 | 不需要 | 生产/开发，高质量离线替代 | 
\ No newline at end of file
diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md
new file mode 100644
index 0000000..57f5e60
--- /dev/null
+++ b/docs/QUICKSTART.md
@@ -0,0 +1,274 @@
+# AI-Cloud-Go 快速启动指南
+
+这份指南将帮助您快速设置并运行AI-Cloud-Go系统，包括各种支持的嵌入服务配置。
+
+## 前置条件
+
+- Go 1.16+
+- Docker和Docker Compose
+- Git
+- 确保以下端口未被占用: 8080, 3306, 9000, 9001, 19530, 9091, 11434
+
+## 步骤一：获取代码
+
+```bash
+git clone https://github.com/RaspberryCola/AI-Cloud-Go.git
+cd AI-Cloud-Go
+```
+
+## 步骤二：设置环境
+
+### 配置系统参数
+
+1. 确保项目根目录存在`config`文件夹，如果不存在请创建
+2. 在`config`文件夹中创建或修改`config.yaml`文件:
+
+```yaml
+server:
+  port: "8080"
+
+database:
+  host: "localhost"
+  port: "3306"
+  user: "root"
+  password: "123456"
+  name: "ai_cloud"
+
+storage:
+  type: "minio"  # local, oss, minio
+  minio:
+    endpoint: "localhost:9000"
+    bucket: "ai-cloud"
+    access_key_id: "minioadmin"
+    access_key_secret: "minioadmin"
+    use_ssl: false
+    region: ""
+
+# 嵌入模型配置
+embedding:
+  service: "remote" # remote 或 ollama
+  
+  # 远程嵌入模型配置（OpenAI API，当 service=remote 时使用）
+  remote:
+    api_key: "your-api-key" # 替换为您的API密钥
+    model: "text-embedding-3-large"
+    base_url: "https://api.openai.com/v1"
+  
+  # Ollama嵌入模型配置（当 service=ollama 时使用）
+  ollama:
+    url: "http://localhost:11434"
+    model: "mxbai-embed-large"
+
+# 语言模型配置
+llm:
+  api_key: "your-llm-api-key" # 替换为您的语言模型API密钥
+  model: "deepseek-chat"
+  base_url: "https://api.deepseek.com/v1"
+  max_tokens: 4096
+  temperature: 0.7
+
+rag:
+  chunk_size: 1500
+  overlap_size: 500
+```
+
+## 步骤三：启动基础服务
+
+### 启动所有Docker服务
+
+确保项目根目录包含`docker-compose.yml`文件，然后运行:
+
+```bash
+# 启动MySQL, MinIO, Milvus服务
+docker-compose up -d
+```
+
+或者，如果您只想启动特定服务：
+
+```bash
+# 仅启动MySQL和MinIO
+docker-compose up -d mysql-init minio minio-init
+
+# 仅启动Milvus相关服务
+docker-compose up -d etcd minio-for-milvus standalone
+```
+
+### 检查服务状态
+
+```bash
+# 列出所有容器
+docker ps
+
+# 检查MySQL是否正常初始化
+docker logs mysql-init
+
+# 检查MinIO是否正常启动
+curl http://localhost:9000
+
+# 检查Milvus是否正常启动
+docker logs milvus-standalone
+```
+
+确保所有服务都正常运行，没有错误信息。
+
+## 步骤四：设置Ollama (如果使用Ollama作为嵌入服务)
+
+如果您选择使用Ollama作为嵌入服务：
+
+1. 从[Ollama官网](https://ollama.com/download)下载并安装Ollama
+
+2. 拉取嵌入模型：
+   ```bash
+   ollama pull mxbai-embed-large
+   ```
+
+3. 启动Ollama服务：
+   ```bash
+   OLLAMA_HOST="0.0.0.0" OLLAMA_ORIGINS="*" ollama serve
+   ```
+   
+   您也可以创建一个启动脚本`start-ollama.sh`:
+   ```bash
+   #!/bin/bash
+   OLLAMA_HOST="0.0.0.0" OLLAMA_ORIGINS="*" OLLAMA_KEEP_ALIVE="24h" ollama serve
+   ```
+   
+   然后运行`chmod +x start-ollama.sh`和`./start-ollama.sh`
+
+4. 在`config/config.yaml`中设置：
+   ```yaml
+   embedding:
+     service: "ollama"
+     ollama:
+       url: "http://localhost:11434"
+       model: "mxbai-embed-large"
+   ```
+
+5. 验证Ollama服务是否正常运行：
+   ```bash
+   curl http://localhost:11434/api/tags
+   ```
+   应返回包含`mxbai-embed-large`的模型列表。
+
+## 步骤五：启动AI-Cloud-Go
+
+### 下载依赖
+
+```bash
+go mod download
+```
+
+### 运行应用
+
+```bash
+go run cmd/main.go
+```
+
+应用将在http://localhost:8080运行。您应该看到类似以下的输出：
+
+```
+[GIN-debug] Listening and serving HTTP on :8080
+```
+
+## 步骤六：验证安装
+
+### 检查API服务
+
+```bash
+# 检查健康状态
+curl http://localhost:8080/api/health
+
+# 使用Swagger查看API文档
+# 在浏览器中访问: http://localhost:8080/swagger/index.html
+```
+
+### 测试嵌入服务
+
+根据您选择的嵌入服务，验证其正常工作：
+
+```bash
+# 如果使用OpenAI
+curl -H "Authorization: Bearer 您的API密钥" https://api.openai.com/v1/embeddings -d '{"model":"text-embedding-3-large", "input":"测试文本"}'
+
+# 如果使用Ollama
+curl -X POST http://localhost:11434/api/embed -d '{"model":"mxbai-embed-large", "input":"测试文本"}'
+```
+
+### 注册和登录
+
+要使用系统的大多数功能，您需要先注册并登录以获取JWT令牌：
+
+1. 注册用户:
+   ```bash
+   curl -X POST http://localhost:8080/api/users/register -H "Content-Type: application/json" -d '{"username":"testuser", "password":"testpassword", "email":"test@example.com"}'
+   ```
+
+2. 登录并获取Token:
+   ```bash
+   curl -X POST http://localhost:8080/api/users/login -H "Content-Type: application/json" -d '{"username":"testuser", "password":"testpassword"}'
+   ```
+   
+   复制返回的`token`值，供后续API调用使用。
+
+## 使用知识库功能
+
+使用您在登录时获得的JWT令牌：
+
+1. 创建知识库：
+   ```bash
+   curl -X POST http://localhost:8080/api/kb/create \
+     -H "Authorization: Bearer 您的JWT令牌" \
+     -H "Content-Type: application/json" \
+     -d '{"name":"测试知识库","description":"这是一个测试知识库"}'
+   ```
+
+2. 上传文档到知识库（使用multipart/form-data）：
+   ```bash
+   curl -X POST http://localhost:8080/api/kb/addNew \
+     -H "Authorization: Bearer 您的JWT令牌" \
+     -F "kb_id=知识库ID" \
+     -F "file=@/path/to/your/document.pdf"
+   ```
+
+3. 查询知识库：
+   ```bash
+   curl -X POST http://localhost:8080/api/kb/retrieve \
+     -H "Authorization: Bearer 您的JWT令牌" \
+     -H "Content-Type: application/json" \
+     -d '{"kb_id":"知识库ID","query":"您的问题","top_k":3}'
+   ```
+
+## 故障排除
+
+### 初始化问题
+- 如果启动时显示找不到配置文件，请确保`config/config.yaml`文件存在并格式正确
+- 如果显示找不到`init.sql`，请确认项目根目录中有此文件
+
+### MySQL连接问题
+- 错误：`dial tcp 127.0.0.1:3306: connect: connection refused`
+- 解决：确保MySQL容器已启动，运行`docker ps | grep mysql`
+
+### Milvus连接问题
+- 错误：`存储向量到 Milvus 失败: 插入数据失败: the length of document_name exceeds max length`
+- 解决：文档名称过长，已在新版本中自动处理，如果仍有问题，请升级代码
+
+### 向量数组为空
+- 错误：`num_rows should be greater than 0: invalid parameter`
+- 解决：检查文档内容是否可提取文本，确保嵌入服务正常工作
+
+### 嵌入服务问题
+- 如果使用OpenAI：检查API密钥和网络连接
+- 如果使用Ollama：确保模型已下载并且服务运行正常
+  - 运行`ollama list`查看已安装的模型
+  - 运行`curl http://localhost:11434/api/tags`检查Ollama服务
+
+### 权限问题
+- 如果遇到API权限错误，确保您已登录并在请求头中包含有效的JWT令牌
+- 错误格式：`{"error":"无效的token"}`或`{"error":"请先登录"}`
+
+## 下一步
+
+- 查看[完整文档](./README.md)了解更多细节
+- 阅读[嵌入服务配置文档](./EMBEDDING_CONFIG_README.md)了解嵌入服务架构
+- 阅读[配置指南](./CONFIG_README.md)了解完整配置选项
+- 探索[API文档](http://localhost:8080/swagger/index.html)了解所有可用端点 
\ No newline at end of file
diff --git a/go.mod b/go.mod
index e69e5b2..e8efdd6 100644
--- a/go.mod
+++ b/go.mod
@@ -2,25 +2,19 @@ module ai-cloud
 
 go 1.23.4
 
-toolchain go1.24.0
-
 require (
 	code.sajari.com/docconv/v2 v2.0.0-pre.4
 	github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
 	github.com/cloudwego/eino v0.3.20
-	github.com/cloudwego/eino-ext/components/document/loader/file v0.0.0-20250328102648-b47e7f1587fa
 	github.com/cloudwego/eino-ext/components/document/loader/url v0.0.0-20250328102648-b47e7f1587fa
-	github.com/cloudwego/eino-ext/components/document/parser/pdf v0.0.0-20250403035559-e5332ba7144a
 	github.com/cloudwego/eino-ext/components/document/transformer/splitter/recursive v0.0.0-20250328102648-b47e7f1587fa
 	github.com/cloudwego/eino-ext/components/embedding/openai v0.0.0-20250328102648-b47e7f1587fa
-	github.com/cloudwego/eino-ext/components/model/ollama v0.0.0-20250221090944-e8ef7aabbe10
 	github.com/cloudwego/eino-ext/components/model/openai v0.0.0-20250328102648-b47e7f1587fa
 	github.com/gin-contrib/cors v1.7.3
 	github.com/gin-gonic/gin v1.10.0
 	github.com/golang-jwt/jwt/v5 v5.2.1
 	github.com/google/uuid v1.6.0
 	github.com/joho/godotenv v1.5.1
-	github.com/ledongthuc/pdf v0.0.0-20240201131950-da5b75280b06
 	github.com/milvus-io/milvus-sdk-go/v2 v2.4.2
 	github.com/minio/minio-go/v7 v7.0.84
 	github.com/spf13/viper v1.19.0
@@ -45,7 +39,6 @@ require (
 	github.com/cockroachdb/errors v1.9.1 // indirect
 	github.com/cockroachdb/logtags v0.0.0-20211118104740-dabe8e521a4f // indirect
 	github.com/cockroachdb/redact v1.1.3 // indirect
-	github.com/dslipak/pdf v0.0.2 // indirect
 	github.com/dustin/go-humanize v1.0.1 // indirect
 	github.com/fatih/set v0.2.1 // indirect
 	github.com/fsnotify/fsnotify v1.7.0 // indirect
@@ -94,7 +87,6 @@ require (
 	github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
 	github.com/nikolalohinski/gonja v1.5.3 // indirect
 	github.com/olekukonko/tablewriter v0.0.5 // indirect
-	github.com/ollama/ollama v0.4.0 // indirect
 	github.com/otiai10/gosseract/v2 v2.2.4 // indirect
 	github.com/pelletier/go-toml/v2 v2.2.3 // indirect
 	github.com/perimeterx/marshmallow v1.1.5 // indirect
diff --git a/go.sum b/go.sum
index f07a891..6b6d78c 100644
--- a/go.sum
+++ b/go.sum
@@ -47,20 +47,14 @@ github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCy
 github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
 github.com/cloudwego/eino v0.3.20 h1:SiQH1SgRlu0aTTtUPjmbkZlC3VjCg8L6B9lDIEcNJmo=
 github.com/cloudwego/eino v0.3.20/go.mod h1:wUjz990apdsaOraOXdh6CdhVXq8DJsOvLsVlxNTcNfY=
-github.com/cloudwego/eino-ext/components/document/loader/file v0.0.0-20250328102648-b47e7f1587fa h1:3G1VHmZQ5cq/p/k83TENg8c8+xS1RYzmrBiatqI1YQI=
-github.com/cloudwego/eino-ext/components/document/loader/file v0.0.0-20250328102648-b47e7f1587fa/go.mod h1:7WMKVsrqApZ4wm50XWqyU7/I82kQ0Khllv7z2hCxBP8=
 github.com/cloudwego/eino-ext/components/document/loader/url v0.0.0-20250328102648-b47e7f1587fa h1:DLakSO//ExpIHha8dmxVxNAyST0EhYKwCOL4p8Y1mcA=
 github.com/cloudwego/eino-ext/components/document/loader/url v0.0.0-20250328102648-b47e7f1587fa/go.mod h1:yqX1vtLONkjD1Uh+iAjDBf04+9O7C0utRBVYdFbvvRY=
 github.com/cloudwego/eino-ext/components/document/parser/html v0.0.0-20250117061805-cd80d1780d76 h1:kK4f2kunb5xlc0XTkg6wkjy8Z/BDfJjWAVm9EOdRErg=
 github.com/cloudwego/eino-ext/components/document/parser/html v0.0.0-20250117061805-cd80d1780d76/go.mod h1:LWR+h0EfIELl/I1tDSVH0Tgx8j2gymxa174U1C8BNps=
-github.com/cloudwego/eino-ext/components/document/parser/pdf v0.0.0-20250403035559-e5332ba7144a h1:IG3MwibVnBEylG6S4zeIJhuCxhNfmWQltRyul2QYWoE=
-github.com/cloudwego/eino-ext/components/document/parser/pdf v0.0.0-20250403035559-e5332ba7144a/go.mod h1:NMLchaIO76JFQ4+2Hxl6l+Apbz8pC7NUNM2za8sor/Y=
 github.com/cloudwego/eino-ext/components/document/transformer/splitter/recursive v0.0.0-20250328102648-b47e7f1587fa h1:2olIEnw1JCJbfHOxlc5TNgp9KbXf0sSzXjXDzn8cTSo=
 github.com/cloudwego/eino-ext/components/document/transformer/splitter/recursive v0.0.0-20250328102648-b47e7f1587fa/go.mod h1:BkGHi997tkJShBm7xFvTjAApySy1GhdkZBvZhAUvyfA=
 github.com/cloudwego/eino-ext/components/embedding/openai v0.0.0-20250328102648-b47e7f1587fa h1:tSpAIyntjZXQlkMuSjys10GIxspjVGzNOT1d9rPQWTs=
 github.com/cloudwego/eino-ext/components/embedding/openai v0.0.0-20250328102648-b47e7f1587fa/go.mod h1:dfxLdXir+K2DhMtAfrh5yfPnyqp0f4gL763BLpfNbtw=
-github.com/cloudwego/eino-ext/components/model/ollama v0.0.0-20250221090944-e8ef7aabbe10 h1:szRTjISOn310TwL4yJqqkvKVv9N/31g3zuhOhR9X1WI=
-github.com/cloudwego/eino-ext/components/model/ollama v0.0.0-20250221090944-e8ef7aabbe10/go.mod h1:zjHos5yMjmbBIZunQ1PKD6aY7F3/QjQMBI8TkOFTNU0=
 github.com/cloudwego/eino-ext/components/model/openai v0.0.0-20250328102648-b47e7f1587fa h1:JvRaFkACsLS+XQNaFt7XQocV+lYnEM3EH6pwGUeB7uU=
 github.com/cloudwego/eino-ext/components/model/openai v0.0.0-20250328102648-b47e7f1587fa/go.mod h1:IsUnQHbZX8mk0FYl9hwl0oOiGIfM+dIL/TDJun0jbCY=
 github.com/cloudwego/eino-ext/libs/acl/openai v0.0.0-20250305023926-469de0301955 h1:fgvkmTqAalDfjdy3b6Ur2mh/KEwB9L2uvqS4MFgTOqc=
@@ -87,8 +81,6 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4=
 github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
-github.com/dslipak/pdf v0.0.2 h1:djAvcM5neg9Ush+zR6QXB+VMJzR6TdnX766HPIg1JmI=
-github.com/dslipak/pdf v0.0.2/go.mod h1:2L3SnkI9cQwnAS9gfPz2iUoLC0rUZwbucpbKi5R1mUo=
 github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
 github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
@@ -280,8 +272,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/labstack/echo/v4 v4.5.0/go.mod h1:czIriw4a0C1dFun+ObrXp7ok03xON0N1awStJ6ArI7Y=
 github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
-github.com/ledongthuc/pdf v0.0.0-20240201131950-da5b75280b06 h1:kacRlPN7EN++tVpGUorNGPn/4DnB7/DfTY82AOn6ccU=
-github.com/ledongthuc/pdf v0.0.0-20240201131950-da5b75280b06/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
 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/levigross/exp-html v0.0.0-20120902181939-8df60c69a8f5 h1:W7p+m/AECTL3s/YR5RpQ4hz5SjNeKzZBl1q36ws12s0=
@@ -348,8 +338,6 @@ github.com/nikolalohinski/gonja v1.5.3/go.mod h1:RmjwxNiXAEqcq1HeK5SSMmqFJvKOfTf
 github.com/olekukonko/tablewriter v0.0.0-20180506121414-d4647c9c7a84/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
 github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
 github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
-github.com/ollama/ollama v0.4.0 h1:8CxFpQxHmCUkXypNl4Suy9+X4SNdsCvE8DY5ih7BIrU=
-github.com/ollama/ollama v0.4.0/go.mod h1:QDxM/t2teuubbfN/FT2pBRMPF0K1N3IakgT1OZBD4NY=
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@@ -496,8 +484,6 @@ go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
 go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
 go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
-go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
-go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
 go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
 go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
 go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
diff --git a/init.sql b/init.sql
new file mode 100644
index 0000000..7d6393a
--- /dev/null
+++ b/init.sql
@@ -0,0 +1,7 @@
+CREATE DATABASE IF NOT EXISTS ai_cloud;
+USE ai_cloud;
+
+-- You can add more initialization steps here if needed, such as:
+-- CREATE TABLE statements
+-- INSERT initial data
+-- CREATE USER and GRANT privileges 
\ No newline at end of file
diff --git a/internal/controller/kb_controller.go b/internal/controller/kb_controller.go
index 28dd223..f6447e1 100644
--- a/internal/controller/kb_controller.go
+++ b/internal/controller/kb_controller.go
@@ -8,6 +8,8 @@ import (
 	"ai-cloud/pkgs/response"
 	"encoding/json"
 	"fmt"
+	"path/filepath"
+	"strings"
 
 	"github.com/gin-gonic/gin"
 )
@@ -162,6 +164,13 @@ func (kc *KBController) AddNewFile(ctx *gin.Context) {
 		response.ParamError(ctx, errcode.ParamBindError, "文件上传失败")
 		return
 	}
+
+	// 检查文件大小
+	if fileHeader.Size > 20*1024*1024 { // 20MB限制
+		response.ParamError(ctx, errcode.ParamBindError, "文件大小不能超过20MB")
+		return
+	}
+
 	file, err := fileHeader.Open()
 	if err != nil {
 		response.ParamError(ctx, errcode.ParamBindError, "文件打开失败")
@@ -186,6 +195,19 @@ func (kc *KBController) AddNewFile(ctx *gin.Context) {
 		response.InternalError(ctx, errcode.InternalServerError, "获取文件信息失败")
 		return
 	}
+
+	// 文档名称长度检查
+	if len(f.Name) > 200 {
+		// 截断文件名
+		nameBase := filepath.Base(f.Name)
+		nameExt := filepath.Ext(nameBase)
+		nameWithoutExt := strings.TrimSuffix(nameBase, nameExt)
+		if len(nameWithoutExt) > 195 {
+			nameWithoutExt = nameWithoutExt[:195]
+		}
+		f.Name = nameWithoutExt + nameExt
+	}
+
 	doc, err := kc.kbService.CreateDocument(userID, kbID, f)
 	if err != nil {
 		response.InternalError(ctx, errcode.InternalServerError, "添加文件到知识库失败")
@@ -194,7 +216,7 @@ func (kc *KBController) AddNewFile(ctx *gin.Context) {
 
 	doc.Status = 1 // 正在处理文档
 	if err = kc.kbService.ProcessDocument(doc); err != nil {
-		response.InternalError(ctx, errcode.InternalServerError, err.Error())
+		response.InternalError(ctx, errcode.InternalServerError, "处理文档失败: "+err.Error())
 		return
 	}
 	response.SuccessWithMessage(ctx, "添加文件到知识库成功", nil)
diff --git a/internal/controller/user_controller.go b/internal/controller/user_controller.go
index f973dcf..e8fa619 100644
--- a/internal/controller/user_controller.go
+++ b/internal/controller/user_controller.go
@@ -20,12 +20,12 @@ func (c *UserController) Register(ctx *gin.Context) {
 	var req model.User
 
 	if err := ctx.ShouldBindJSON(&req); err != nil {
-		response.ParamError(ctx, errcode.ParamBindError, "用户注册参数错误")
+		response.ParamError(ctx, errcode.ParamBindError, "用户注册参数错误: "+err.Error())
 		return
 	}
 
 	if err := c.userService.Register(&req); err != nil {
-		response.InternalError(ctx, errcode.InternalServerError, "注册失败")
+		response.InternalError(ctx, errcode.InternalServerError, "注册失败: "+err.Error())
 		return
 	}
 
diff --git a/internal/dao/milvus_dao.go b/internal/dao/milvus_dao.go
index 59e6370..75b6db1 100644
--- a/internal/dao/milvus_dao.go
+++ b/internal/dao/milvus_dao.go
@@ -9,6 +9,7 @@ import (
 	"log"
 	"sort"
 	"strings"
+	"time"
 )
 
 type MilvusDao interface {
@@ -29,6 +30,12 @@ func (m *milvusDao) SaveChunks(chunks []model.Chunk) error {
 	ctx := context.Background()
 	collectionName := "text_chunks"
 
+	// 添加日志，检查传入的chunks数量
+	fmt.Printf("SaveChunks: 准备插入%d个文本块\n", len(chunks))
+	if len(chunks) == 0 {
+		return fmt.Errorf("num_rows should be greater than 0: invalid parameter[expected=invalid num_rows][actual=0")
+	}
+
 	// 准备插入数据
 	var ids []string
 	var contents []string
@@ -38,16 +45,37 @@ func (m *milvusDao) SaveChunks(chunks []model.Chunk) error {
 	var chunkIndices []int32
 	var vectors [][]float32
 
-	for _, chunk := range chunks {
+	for i, chunk := range chunks {
+		// 验证chunk数据
+		if len(chunk.Content) == 0 {
+			fmt.Printf("警告: 第%d个文本块内容为空，跳过\n", i)
+			continue
+		}
+		if len(chunk.Embeddings) == 0 {
+			fmt.Printf("警告: 第%d个文本块向量为空，跳过\n", i)
+			continue
+		}
+
+		// 确保文档名不超过限制长度
+		docName := chunk.DocumentName
+		if len(docName) > 250 {
+			docName = docName[:250]
+		}
+
 		ids = append(ids, chunk.ID)
 		contents = append(contents, chunk.Content)
 		documentIDs = append(documentIDs, chunk.DocumentID)
-		documentNames = append(documentNames, chunk.DocumentName)
+		documentNames = append(documentNames, docName)
 		kbIDs = append(kbIDs, chunk.KBID)
 		chunkIndices = append(chunkIndices, int32(chunk.Index))
 		vectors = append(vectors, chunk.Embeddings)
 	}
 
+	// 重新检查，确保有数据要插入
+	if len(ids) == 0 {
+		return fmt.Errorf("过滤无效数据后，没有有效的文本块可以插入")
+	}
+
 	// 创建插入的数据列
 	idColumn := entity.NewColumnVarChar("id", ids)
 	contentColumn := entity.NewColumnVarChar("content", contents)
@@ -57,24 +85,33 @@ func (m *milvusDao) SaveChunks(chunks []model.Chunk) error {
 	indexColumn := entity.NewColumnInt32("chunk_index", chunkIndices)
 	vectorColumn := entity.NewColumnFloatVector("vector", 1024, vectors)
 
-	// 插入数据
-	_, err := m.mv.Insert(
-		ctx,
-		collectionName,
-		"",
-		idColumn,
-		contentColumn,
-		documentIDColumn,
-		documentNameColumn,
-		kbIDColumn,
-		indexColumn,
-		vectorColumn,
-	)
-	if err != nil {
-		return fmt.Errorf("插入数据失败: %w", err)
+	// 插入数据，最多重试3次
+	var lastErr error
+	for i := 0; i < 3; i++ {
+		fmt.Printf("尝试插入数据到Milvus (%d/3)...\n", i+1)
+		_, err := m.mv.Insert(
+			ctx,
+			collectionName,
+			"",
+			idColumn,
+			contentColumn,
+			documentIDColumn,
+			documentNameColumn,
+			kbIDColumn,
+			indexColumn,
+			vectorColumn,
+		)
+		if err == nil {
+			fmt.Printf("成功插入%d条数据到Milvus\n", len(ids))
+			return nil
+		}
+
+		lastErr = err
+		fmt.Printf("插入失败 (%d/3): %v\n", i+1, err)
+		time.Sleep(1 * time.Second) // 等待1秒后重试
 	}
 
-	return nil
+	return fmt.Errorf("插入数据失败，已重试3次: %w", lastErr)
 }
 
 func (m *milvusDao) DeleteChunks(docIDs []string) error {
diff --git a/internal/database/milvus.go b/internal/database/milvus.go
index c789986..f529f02 100644
--- a/internal/database/milvus.go
+++ b/internal/database/milvus.go
@@ -10,9 +10,7 @@ import (
 
 func InitMilvus(ctx context.Context) (client.Client, error) {
 	milvusClient, err := client.NewClient(ctx, client.Config{
-		Address:  "localhost:19530",
-		Username: "ai_cloud",
-		Password: "aicloud666",
+		Address: "localhost:19530",
 	})
 	if err != nil {
 		return nil, fmt.Errorf("无法连接到Milvus: %w", err)
@@ -32,81 +30,88 @@ func initTextChunksCollection(ctx context.Context, milvusClinet client.Client) e
 		return fmt.Errorf("检查集合存在失败: %w", err)
 	}
 
-	if !exists {
-		schema := &entity.Schema{
-			CollectionName: collectionName,
-			Description:    "存储文档分块和向量",
-			AutoID:         false,
-			Fields: []*entity.Field{
-				{
-					Name:       "id",
-					DataType:   entity.FieldTypeVarChar,
-					PrimaryKey: true,
-					AutoID:     false,
-					TypeParams: map[string]string{
-						"max_length": "64",
-					},
-				},
-				{
-					Name:     "content",
-					DataType: entity.FieldTypeVarChar,
-					TypeParams: map[string]string{
-						"max_length": "65535",
-					},
+	if exists {
+		// 如果集合已存在，先删除它
+		if err := milvusClinet.DropCollection(ctx, collectionName); err != nil {
+			return fmt.Errorf("删除现有集合失败: %w", err)
+		}
+		fmt.Println("已删除现有的text_chunks集合，将重新创建")
+	}
+
+	// 创建集合
+	schema := &entity.Schema{
+		CollectionName: collectionName,
+		Description:    "存储文档分块和向量",
+		AutoID:         false,
+		Fields: []*entity.Field{
+			{
+				Name:       "id",
+				DataType:   entity.FieldTypeVarChar,
+				PrimaryKey: true,
+				AutoID:     false,
+				TypeParams: map[string]string{
+					"max_length": "64",
 				},
-				{
-					Name:     "document_id",
-					DataType: entity.FieldTypeVarChar,
-					TypeParams: map[string]string{
-						"max_length": "64",
-					},
+			},
+			{
+				Name:     "content",
+				DataType: entity.FieldTypeVarChar,
+				TypeParams: map[string]string{
+					"max_length": "65535",
 				},
-				{
-					Name:     "document_name",
-					DataType: entity.FieldTypeVarChar,
-					TypeParams: map[string]string{
-						"max_length": "64",
-					},
+			},
+			{
+				Name:     "document_id",
+				DataType: entity.FieldTypeVarChar,
+				TypeParams: map[string]string{
+					"max_length": "64",
 				},
-				{
-					Name:     "kb_id",
-					DataType: entity.FieldTypeVarChar,
-					TypeParams: map[string]string{
-						"max_length": "64",
-					},
+			},
+			{
+				Name:     "document_name",
+				DataType: entity.FieldTypeVarChar,
+				TypeParams: map[string]string{
+					"max_length": "256",
 				},
-				{
-					Name:     "chunk_index",
-					DataType: entity.FieldTypeInt32,
+			},
+			{
+				Name:     "kb_id",
+				DataType: entity.FieldTypeVarChar,
+				TypeParams: map[string]string{
+					"max_length": "64",
 				},
-				{
-					Name:     "vector",
-					DataType: entity.FieldTypeFloatVector,
-					TypeParams: map[string]string{
-						"dim": "1024",
-					},
+			},
+			{
+				Name:     "chunk_index",
+				DataType: entity.FieldTypeInt32,
+			},
+			{
+				Name:     "vector",
+				DataType: entity.FieldTypeFloatVector,
+				TypeParams: map[string]string{
+					"dim": "1024",
 				},
 			},
-		}
+		},
+	}
 
-		if err := milvusClinet.CreateCollection(ctx, schema, 1); err != nil {
-			return fmt.Errorf("创建集合失败: %w", err)
-		}
+	if err := milvusClinet.CreateCollection(ctx, schema, 1); err != nil {
+		return fmt.Errorf("创建集合失败: %w", err)
+	}
 
-		// 创建索引
-		idx, err := entity.NewIndexIvfFlat(entity.COSINE, 128)
-		if err != nil {
-			return fmt.Errorf("创建索引失败: %w", err)
-		}
+	// 创建索引
+	idx, err := entity.NewIndexIvfFlat(entity.COSINE, 128)
+	if err != nil {
+		return fmt.Errorf("创建索引失败: %w", err)
+	}
 
-		if err := milvusClinet.CreateIndex(ctx, collectionName, "vector", idx, false); err != nil {
-			return fmt.Errorf("创建索引失败: %w", err)
-		}
+	if err := milvusClinet.CreateIndex(ctx, collectionName, "vector", idx, false); err != nil {
+		return fmt.Errorf("创建索引失败: %w", err)
+	}
 
-		// 加载集合到内存
-		if err := milvusClinet.LoadCollection(ctx, collectionName, false); err != nil {
-			return fmt.Errorf("加载集合失败: %w", err)
-		}
+	// 加载集合到内存
+	if err := milvusClinet.LoadCollection(ctx, collectionName, false); err != nil {
+		return fmt.Errorf("加载集合失败: %w", err)
 	}
 
 	return nil
diff --git a/internal/service/customembedder.go b/internal/service/customembedder.go
new file mode 100644
index 0000000..54f5951
--- /dev/null
+++ b/internal/service/customembedder.go
@@ -0,0 +1,190 @@
+package service
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+	"time"
+
+	openaiEmbed "github.com/cloudwego/eino-ext/components/embedding/openai"
+)
+
+// OllamaEmbedder 实现自定义的Ollama嵌入服务
+type OllamaEmbedder struct {
+	BaseURL    string
+	Model      string
+	HTTPClient *http.Client
+	Dimensions int
+}
+
+// NewOllamaEmbedder 创建一个新的Ollama嵌入服务实例
+func NewOllamaEmbedder(ollamaURL, model string, dimensions int) *OllamaEmbedder {
+	return &OllamaEmbedder{
+		BaseURL: ollamaURL,
+		Model:   model,
+		HTTPClient: &http.Client{
+			Timeout: 60 * time.Second, // 设置较长的超时时间
+		},
+		Dimensions: dimensions,
+	}
+}
+
+// EmbedStrings 将文本字符串转换为嵌入向量
+func (oe *OllamaEmbedder) EmbedStrings(ctx context.Context, texts []string) ([][]float64, error) {
+	if len(texts) == 0 {
+		return [][]float64{}, nil
+	}
+
+	// 准备请求数据
+	reqBody := map[string]interface{}{
+		"model": oe.Model,
+		"input": texts[0], // Ollama只能处理单个文本
+	}
+
+	jsonData, err := json.Marshal(reqBody)
+	if err != nil {
+		return nil, fmt.Errorf("序列化请求失败: %w", err)
+	}
+
+	// 创建请求
+	req, err := http.NewRequestWithContext(
+		ctx,
+		"POST",
+		oe.BaseURL+"/api/embed",
+		bytes.NewBuffer(jsonData),
+	)
+	if err != nil {
+		return nil, fmt.Errorf("创建请求失败: %w", err)
+	}
+
+	// 设置请求头
+	req.Header.Set("Content-Type", "application/json")
+
+	// 发送请求
+	resp, err := oe.HTTPClient.Do(req)
+	if err != nil {
+		return nil, fmt.Errorf("发送请求失败: %w", err)
+	}
+	defer resp.Body.Close()
+
+	// 检查状态码
+	if resp.StatusCode != http.StatusOK {
+		bodyBytes, _ := io.ReadAll(resp.Body)
+		return nil, fmt.Errorf("API错误, 状态码: %d, 响应: %s", resp.StatusCode, string(bodyBytes))
+	}
+
+	// 解析响应
+	var ollamaResp struct {
+		Embeddings    [][]float64 `json:"embeddings"`
+		Model         string      `json:"model"`
+		TotalDuration int64       `json:"total_duration"`
+	}
+
+	if err := json.NewDecoder(resp.Body).Decode(&ollamaResp); err != nil {
+		bodyBytes, _ := io.ReadAll(resp.Body)
+		return nil, fmt.Errorf("解析响应失败: %w, 响应: %s", err, string(bodyBytes))
+	}
+
+	// 检查是否有向量
+	if len(ollamaResp.Embeddings) == 0 {
+		return nil, fmt.Errorf("获取到空的embeddings数组")
+	}
+
+	fmt.Printf("成功获取到向量，模型: %s, 长度: %d, 处理时间: %.2f毫秒\n",
+		ollamaResp.Model,
+		len(ollamaResp.Embeddings[0]),
+		float64(ollamaResp.TotalDuration)/1000000)
+
+	return ollamaResp.Embeddings, nil
+}
+
+// CreateOpenAILikeEmbedder 创建一个使用openaiEmbed.Embedder的包装，但实际调用Ollama API
+func CreateOpenAILikeEmbedder(ctx context.Context, ollamaURL, ollamaModel string, dimension int) *openaiEmbed.Embedder {
+	// 创建标准OpenAI embedder作为返回值
+	embedder, _ := openaiEmbed.NewEmbedder(ctx, &openaiEmbed.EmbeddingConfig{
+		BaseURL:    ollamaURL, // 注意: 这是不正确的设置，实际会在kb_service.go中特殊处理
+		Model:      ollamaModel,
+		Timeout:    60 * time.Second,
+		Dimensions: &dimension,
+	})
+
+	// 由于无法安全地替换embedder.EmbedStrings方法
+	// 我们在kb_service.go中将不直接使用该方法，而是使用自定义的拦截方式
+
+	return embedder
+}
+
+// 直接使用此函数进行Ollama向量化，绕过openai embedder
+func OllamaEmbedStrings(ollamaURL, ollamaModel string, ctx context.Context, texts []string) ([][]float64, error) {
+	if len(texts) == 0 {
+		return [][]float64{}, nil
+	}
+
+	// 创建单次使用的HTTP客户端
+	ollamaClient := &http.Client{
+		Timeout: 60 * time.Second, // 更长超时
+	}
+
+	// 准备请求数据 (Ollama API格式)
+	reqBody := map[string]interface{}{
+		"model": ollamaModel,
+		"input": texts[0], // Ollama每次只能处理一个文本
+	}
+
+	jsonData, err := json.Marshal(reqBody)
+	if err != nil {
+		return nil, fmt.Errorf("序列化Ollama请求失败: %w", err)
+	}
+
+	// 创建HTTP请求，使用正确的API端点
+	req, err := http.NewRequestWithContext(
+		ctx,
+		"POST",
+		ollamaURL+"/api/embed",
+		bytes.NewBuffer(jsonData),
+	)
+	if err != nil {
+		return nil, fmt.Errorf("创建Ollama请求失败: %w", err)
+	}
+
+	// 设置请求头
+	req.Header.Set("Content-Type", "application/json")
+
+	// 发送请求
+	resp, err := ollamaClient.Do(req)
+	if err != nil {
+		return nil, fmt.Errorf("发送Ollama请求失败: %w", err)
+	}
+	defer resp.Body.Close()
+
+	// 检查状态码
+	if resp.StatusCode != http.StatusOK {
+		bodyBytes, _ := io.ReadAll(resp.Body)
+		return nil, fmt.Errorf("Ollama API错误, 状态码: %d, 响应: %s", resp.StatusCode, string(bodyBytes))
+	}
+
+	// 解析响应
+	var ollamaResp struct {
+		Embeddings    [][]float64 `json:"embeddings"`
+		Model         string      `json:"model"`
+		TotalDuration int64       `json:"total_duration"`
+	}
+
+	if err := json.NewDecoder(resp.Body).Decode(&ollamaResp); err != nil {
+		// 尝试读取错误消息
+		bodyBytes, _ := io.ReadAll(resp.Body)
+		return nil, fmt.Errorf("解析Ollama响应失败: %w, 响应内容: %s", err, string(bodyBytes))
+	}
+
+	// 检查是否有向量
+	if len(ollamaResp.Embeddings) == 0 {
+		return nil, fmt.Errorf("Ollama返回了空的embeddings数组")
+	}
+
+	fmt.Printf("成功从Ollama获取到向量，长度: %d\n", len(ollamaResp.Embeddings[0]))
+
+	return ollamaResp.Embeddings, nil
+}
diff --git a/internal/service/embedding_service.go b/internal/service/embedding_service.go
new file mode 100644
index 0000000..382a1ef
--- /dev/null
+++ b/internal/service/embedding_service.go
@@ -0,0 +1,130 @@
+package service
+
+import (
+	"context"
+	"fmt"
+	"strings"
+	"time"
+
+	"ai-cloud/config"
+	"github.com/cloudwego/eino-ext/components/embedding/openai"
+)
+
+// EmbeddingService 定义向量嵌入服务的通用接口
+type EmbeddingService interface {
+	// EmbedStrings 将文本转换为向量表示
+	EmbedStrings(ctx context.Context, texts []string) ([][]float64, error)
+
+	// GetDimension 返回嵌入向量的维度
+	GetDimension() int
+}
+
+// OpenAIEmbeddingService 使用OpenAI API的嵌入服务
+type OpenAIEmbeddingService struct {
+	embedder  *openai.Embedder
+	dimension int
+}
+
+// OllamaEmbeddingService 使用Ollama的嵌入服务
+type OllamaEmbeddingService struct {
+	baseURL   string
+	model     string
+	embedder  *openai.Embedder // 作为标准接口保留，但不直接使用
+	dimension int
+}
+
+// NewOpenAIEmbeddingService 创建OpenAI嵌入服务实例
+func NewOpenAIEmbeddingService(ctx context.Context) (*OpenAIEmbeddingService, error) {
+	cfg := config.GetConfig().Embedding.Remote
+
+	apiKey := cfg.APIKey
+	model := cfg.Model
+	baseURL := cfg.BaseURL
+
+	dimension := 1024 // 默认维度
+
+	fmt.Println("创建OpenAI嵌入服务:", baseURL, "模型:", model)
+
+	embedder, err := openai.NewEmbedder(ctx, &openai.EmbeddingConfig{
+		APIKey:     apiKey,
+		Model:      model,
+		BaseURL:    baseURL,
+		Timeout:    30 * time.Second,
+		Dimensions: &dimension,
+	})
+
+	if err != nil {
+		return nil, fmt.Errorf("创建OpenAI嵌入服务失败: %w", err)
+	}
+
+	return &OpenAIEmbeddingService{
+		embedder:  embedder,
+		dimension: dimension,
+	}, nil
+}
+
+// EmbedStrings OpenAI实现的向量嵌入
+func (s *OpenAIEmbeddingService) EmbedStrings(ctx context.Context, texts []string) ([][]float64, error) {
+	return s.embedder.EmbedStrings(ctx, texts)
+}
+
+// GetDimension 返回OpenAI嵌入向量的维度
+func (s *OpenAIEmbeddingService) GetDimension() int {
+	return s.dimension
+}
+
+// NewOllamaEmbeddingService 创建Ollama嵌入服务实例
+func NewOllamaEmbeddingService(ctx context.Context) (*OllamaEmbeddingService, error) {
+	cfg := config.GetConfig().Embedding.Ollama
+
+	ollamaURL := cfg.URL
+	ollamaModel := cfg.Model
+
+	dimension := 1024 // Ollama模型的默认维度
+
+	fmt.Println("创建Ollama嵌入服务:", ollamaURL, "模型:", ollamaModel)
+
+	// 创建一个标准的embedder作为后备，但实际不会使用它的EmbedStrings方法
+	embedder, err := openai.NewEmbedder(ctx, &openai.EmbeddingConfig{
+		BaseURL:    ollamaURL,
+		Model:      ollamaModel,
+		Timeout:    60 * time.Second,
+		Dimensions: &dimension,
+	})
+
+	if err != nil {
+		return nil, fmt.Errorf("创建Ollama嵌入服务失败: %w", err)
+	}
+
+	return &OllamaEmbeddingService{
+		baseURL:   ollamaURL,
+		model:     ollamaModel,
+		embedder:  embedder,
+		dimension: dimension,
+	}, nil
+}
+
+// EmbedStrings Ollama实现的向量嵌入，使用自定义API调用
+func (s *OllamaEmbeddingService) EmbedStrings(ctx context.Context, texts []string) ([][]float64, error) {
+	// 使用customembedder.go中实现的OllamaEmbedStrings函数
+	return OllamaEmbedStrings(s.baseURL, s.model, ctx, texts)
+}
+
+// GetDimension 返回Ollama嵌入向量的维度
+func (s *OllamaEmbeddingService) GetDimension() int {
+	return s.dimension
+}
+
+// NewEmbeddingService 工厂函数，根据配置创建合适的嵌入服务
+func NewEmbeddingService(ctx context.Context) (EmbeddingService, error) {
+	embeddingType := strings.ToLower(config.GetConfig().Embedding.Service)
+
+	switch embeddingType {
+	case "ollama":
+		return NewOllamaEmbeddingService(ctx)
+	case "remote", "openai", "":
+		return NewOpenAIEmbeddingService(ctx)
+	default:
+		return NewOpenAIEmbeddingService(ctx)
+	}
+}
diff --git a/internal/service/kb_service.go b/internal/service/kb_service.go
index c2ac44d..6104e30 100644
--- a/internal/service/kb_service.go
+++ b/internal/service/kb_service.go
@@ -6,23 +6,21 @@ import (
 	"ai-cloud/internal/model"
 	"ai-cloud/internal/storage"
 	"ai-cloud/internal/utils"
+	"context"
+	"errors"
+	"fmt"
+	"io"
+	"log"
 	"os"
 	"path/filepath"
 	"strings"
+	"time"
 
 	"github.com/cloudwego/eino-ext/components/document/loader/url"
 	"github.com/cloudwego/eino-ext/components/document/transformer/splitter/recursive"
 	"github.com/cloudwego/eino-ext/components/model/openai"
 	"github.com/cloudwego/eino/schema"
 
-	"context"
-	"errors"
-	"fmt"
-	"io"
-	"time"
-
-	openaiEmbed "github.com/cloudwego/eino-ext/components/embedding/openai"
-
 	"github.com/cloudwego/eino/components/document"
 	"github.com/cloudwego/eino/components/document/parser"
 )
@@ -51,12 +49,12 @@ type KBService interface {
 }
 
 type kbService struct {
-	kbDao         dao.KnowledgeBaseDao
-	milvusDao     dao.MilvusDao
-	fileService   FileService
-	storageDriver storage.Driver
-	embedder      *openaiEmbed.Embedder
-	llm           *openai.ChatModel
+	kbDao            dao.KnowledgeBaseDao
+	milvusDao        dao.MilvusDao
+	fileService      FileService
+	storageDriver    storage.Driver
+	embeddingService EmbeddingService // 使用新的嵌入服务接口
+	llm              *openai.ChatModel
 }
 
 func NewKBService(kbDao dao.KnowledgeBaseDao, milvusDao dao.MilvusDao, fileService FileService) KBService {
@@ -68,37 +66,45 @@ func NewKBService(kbDao dao.KnowledgeBaseDao, milvusDao dao.MilvusDao, fileServi
 		panic("无法连接到存储服务: " + err.Error())
 	}
 
-	// embedder
-	dimesion := 1024
-	embedder, _ := openaiEmbed.NewEmbedder(ctx, &openaiEmbed.EmbeddingConfig{
-		APIKey:     os.Getenv("EMBEDDING_API_KEY"),
-		Model:      os.Getenv("EMBEDDING_MODEL"),
-		BaseURL:    os.Getenv("EMBEDDING_BASE_URL"),
-		Timeout:    30 * time.Second,
-		Dimensions: &dimesion,
-	})
+	// 使用工厂函数创建合适的嵌入服务
+	embeddingService, err := NewEmbeddingService(ctx)
+	if err != nil {
+		panic("无法创建嵌入服务: " + err.Error())
+	}
+
+	// 从配置文件获取LLM配置
+	llmCfg := config.GetConfig().LLM
+	maxTokens := llmCfg.MaxTokens
+	temp := llmCfg.Temperature
 
-	// llm
 	// 初始化LLM
-	maxTokens := 4096
-	var temp float32 = 0.7
 	llm, _ := openai.NewChatModel(ctx, &openai.ChatModelConfig{
-		BaseURL:     os.Getenv("LLM_BASE_URL"),
-		APIKey:      os.Getenv("LLM_API_KEY"),
-		Model:       os.Getenv("LLM_MODEL"),
+		BaseURL:     llmCfg.BaseURL,
+		APIKey:      llmCfg.APIKey,
+		Model:       llmCfg.Model,
 		MaxTokens:   &maxTokens,
 		Temperature: &temp,
 	})
 
 	return &kbService{
-		kbDao:         kbDao,
-		milvusDao:     milvusDao,
-		fileService:   fileService,
-		storageDriver: driver,
-		embedder:      embedder,
-		llm:           llm,
+		kbDao:            kbDao,
+		milvusDao:        milvusDao,
+		fileService:      fileService,
+		storageDriver:    driver,
+		embeddingService: embeddingService,
+		llm:              llm,
+	}
+}
+
+// getEnvWithDefault 获取环境变量，如果不存在则返回默认值
+func getEnvWithDefault(key, defaultValue string) string {
+	value := os.Getenv(key)
+	if value == "" {
+		return defaultValue
 	}
+	return value
 }
+
 func (ks *kbService) CreateDB(name, description string, userID uint) error {
 	if name == "" {
 		return errors.New("知识库名称不能为空")
@@ -222,9 +228,10 @@ func (ks *kbService) ProcessDocument(doc *model.Document) error {
 	}
 
 	fURL, _ := ks.storageDriver.GetURL(f.StorageKey)
-	fmt.Println("fURL: ", fURL)
+	fmt.Println("处理文档:", f.Name, "URL:", fURL)
 
 	ext := strings.ToLower(filepath.Ext(f.Name))
+	fmt.Println("文件扩展名:", ext)
 
 	// 2. Loader 加载文档，获取schema.Document
 	var p parser.Parser
@@ -235,6 +242,7 @@ func (ks *kbService) ProcessDocument(doc *model.Document) error {
 			return fmt.Errorf("获取pdfparser失败：%v", err)
 		}
 	default:
+		fmt.Println("未找到适合扩展名的解析器:", ext)
 		p = nil
 	}
 
@@ -249,6 +257,7 @@ func (ks *kbService) ProcessDocument(doc *model.Document) error {
 	if err != nil {
 		return fmt.Errorf("加载文档失败: %w", err)
 	}
+	fmt.Printf("文档加载成功，共%d个文档部分\n", len(docs))
 	for _, d := range docs {
 		d.ID = f.Name
 	}
@@ -268,11 +277,34 @@ func (ks *kbService) ProcessDocument(doc *model.Document) error {
 	if err != nil {
 		return fmt.Errorf("分块失败: %w", err)
 	}
+	fmt.Printf("文本分块成功，共%d个文本块\n", len(texts))
+
+	if len(texts) == 0 {
+		return fmt.Errorf("文档解析未生成有效文本块，请检查文档内容或格式")
+	}
 
 	for i, text := range texts {
 		textString := []string{text.Content}
-		vectors64, _ := ks.embedder.EmbedStrings(ctx, textString)
+		fmt.Printf("处理第%d个文本块，长度: %d字符\n", i+1, len(text.Content))
+
+		if len(text.Content) < 10 {
+			fmt.Printf("文本块内容过短，跳过: '%s'\n", text.Content)
+			continue
+		}
+
+		// 使用抽象的嵌入服务接口进行向量化
+		vectors64, err := ks.embeddingService.EmbedStrings(ctx, textString)
+		if err != nil {
+			fmt.Printf("向量化失败: %v\n", err)
+			return fmt.Errorf("文本向量化失败: %w", err)
+		}
+
 		float32Vectors := utils.ConvertFloat64ToFloat32Embeddings(vectors64)
+		if len(float32Vectors) == 0 {
+			fmt.Printf("获取到空的embedding向量，跳过该文本块\n")
+			continue
+		}
+
 		chunk := model.Chunk{
 			ID:           GenerateUUID(),
 			Content:      text.Content,
@@ -285,6 +317,11 @@ func (ks *kbService) ProcessDocument(doc *model.Document) error {
 		chunks = append(chunks, chunk)
 	}
 
+	fmt.Printf("成功生成%d个向量化的文本块\n", len(chunks))
+	if len(chunks) == 0 {
+		return fmt.Errorf("未能生成任何有效的向量化文本块，请检查文本内容或向量化服务")
+	}
+
 	// 4. 将 chunks 存储到 Milvus
 	if err := ks.milvusDao.SaveChunks(chunks); err != nil {
 		return fmt.Errorf("存储向量到 Milvus 失败: %w", err)
@@ -301,7 +338,6 @@ func (ks *kbService) ProcessDocument(doc *model.Document) error {
 }
 
 func (ks *kbService) Retrieve(userID uint, kbID string, query string, topK int) ([]model.Chunk, error) {
-
 	ctx := context.Background()
 
 	// 1. 权限校验
@@ -315,16 +351,24 @@ func (ks *kbService) Retrieve(userID uint, kbID string, query string, topK int)
 
 	queryList := []string{query}
 
-	// 2. 向量化query
-	embeddings, err := ks.embedder.EmbedStrings(ctx, queryList)
+	// 2. 向量化query，使用抽象的嵌入服务接口
+	embeddings, err := ks.embeddingService.EmbedStrings(ctx, queryList)
 	if err != nil {
 		return nil, fmt.Errorf("查询向量化失败: %w", err)
 	}
 
+	// 检查返回的向量数组是否为空
+	if len(embeddings) == 0 {
+		return nil, fmt.Errorf("获取到空的embedding向量数组，查询文本: %s", query)
+	}
+
 	// 3. Milvus向量检索
-	float32Vector := utils.ConvertFloat64ToFloat32Embeddings(embeddings)[0]
+	float32Vectors := utils.ConvertFloat64ToFloat32Embeddings(embeddings)
+	if len(float32Vectors) == 0 {
+		return nil, fmt.Errorf("转换后获取到空的float32向量数组，查询文本: %s", query)
+	}
 
-	return ks.milvusDao.Search(kbID, float32Vector, topK)
+	return ks.milvusDao.Search(kbID, float32Vectors[0], topK)
 }
 
 // RAGQuery 实现RAG查询
@@ -416,6 +460,8 @@ func (ks *kbService) RAGQueryStream(ctx context.Context, userID uint, query stri
 		schema.UserMessage(query),
 	}
 
+	log.Printf("messages: %v", messages)
+
 	// 4. 启动goroutine处理流式响应
 	go func() {
 		defer close(responseChan)
diff --git a/internal/service/user_service.go b/internal/service/user_service.go
index a5f67e6..e1f414e 100644
--- a/internal/service/user_service.go
+++ b/internal/service/user_service.go
@@ -53,7 +53,7 @@ func (s *userService) Register(user *model.User) error {
 	}
 	err = s.userDao.CreateUser(newUser)
 	if err != nil {
-		return errors.New("注册失败")
+		return err
 	}
 	return nil
 }
diff --git a/internal/utils/customPdfParser.go b/internal/utils/customPdfParser.go
index f2fe653..a3fb271 100644
--- a/internal/utils/customPdfParser.go
+++ b/internal/utils/customPdfParser.go
@@ -45,7 +45,23 @@ func (pp *CustomPdfParser) Parse(ctx context.Context, reader io.Reader, opts ...
 	}, opts...)
 
 	// 3. 实现解析逻辑
-	res, _, _ := docconv.ConvertPDF(reader)
+	fmt.Println("开始解析PDF文档...")
+	res, meta, err := docconv.ConvertPDF(reader)
+	if err != nil {
+		fmt.Printf("PDF解析错误: %v\n", err)
+		return nil, fmt.Errorf("PDF解析失败: %w", err)
+	}
+
+	fmt.Printf("PDF解析完成，文本长度: %d字符\n", len(res))
+	fmt.Printf("PDF元数据: %+v\n", meta)
+
+	// 检查解析结果是否为空
+	if len(res) < 100 { // 至少需要100个字符才算有效
+		fmt.Println("警告: PDF解析结果太短或为空")
+		if len(res) == 0 {
+			return nil, fmt.Errorf("PDF解析结果为空，可能是扫描PDF或无文本内容")
+		}
+	}
 
 	if *specificOpts.toPages {
 		fmt.Println("待处理分页")


Reviews:
user: login: RaspberryCola
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-22T14:54:56Z

Review comments:
diff_hunk: @@ -32,81 +30,88 @@ func initTextChunksCollection(ctx context.Context, milvusClinet client.Client) e
 		return fmt.Errorf("检查集合存在失败: %w", err)
 	}
 
-	if !exists {
-		schema := &entity.Schema{
-			CollectionName: collectionName,
-			Description:    "存储文档分块和向量",
-			AutoID:         false,
-			Fields: []*entity.Field{
-				{
-					Name:       "id",
-					DataType:   entity.FieldTypeVarChar,
-					PrimaryKey: true,
-					AutoID:     false,
-					TypeParams: map[string]string{
-						"max_length": "64",
-					},
-				},
-				{
-					Name:     "content",
-					DataType: entity.FieldTypeVarChar,
-					TypeParams: map[string]string{
-						"max_length": "65535",
-					},
+	if exists {
+		// 如果集合已存在，先删除它
path: internal/database/milvus.go
user: login: RaspberryCola
body: 感谢你的 PR！关于 initTextChunksCollection 中 ​​“集合存在则先删除”​​ 的逻辑，我想了解一下这个修改的考虑。
我认为这样会导致每次项目启动时清空集合，是否是为了测试环境开发方便？

created_at: 2025-04-22T14:32:25Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,130 @@
+package service
+
+import (
+	"context"
+	"fmt"
+	"strings"
+	"time"
+
+	"ai-cloud/config"
+	"github.com/cloudwego/eino-ext/components/embedding/openai"
+)
+
+// EmbeddingService 定义向量嵌入服务的通用接口
+type EmbeddingService interface {
+	// EmbedStrings 将文本转换为向量表示
+	EmbedStrings(ctx context.Context, texts []string) ([][]float64, error)
+
+	// GetDimension 返回嵌入向量的维度
+	GetDimension() int
+}
+
+// OpenAIEmbeddingService 使用OpenAI API的嵌入服务
+type OpenAIEmbeddingService struct {
+	embedder  *openai.Embedder
+	dimension int
+}
+
+// OllamaEmbeddingService 使用Ollama的嵌入服务
+type OllamaEmbeddingService struct {
+	baseURL   string
+	model     string
+	embedder  *openai.Embedder // 作为标准接口保留，但不直接使用
+	dimension int
+}
+
+// NewOpenAIEmbeddingService 创建OpenAI嵌入服务实例
+func NewOpenAIEmbeddingService(ctx context.Context) (*OpenAIEmbeddingService, error) {
+	cfg := config.GetConfig().Embedding.Remote
+
+	apiKey := cfg.APIKey
+	model := cfg.Model
+	baseURL := cfg.BaseURL
+
+	dimension := 1024 // 默认维度
path: internal/service/embedding_service.go
user: login: RaspberryCola
body: 感谢贡献！关于embedding这里，不同嵌入模型的维度规格存在差异，我认为openai和ollama的维度设置也应该从配置中获取。另外这个改动也会影响milvus中collection的向量维度配置，在后续迭代中进一步完善这部分逻辑。
created_at: 2025-04-22T14:53:44Z
author_association: OWNER

PR comments:
user: login: HildaM
created_at: 2025-04-22T13:32:00Z
body: 理论上支持 “自定义的 llm 模型和 embedding 服务”
author_association: CONTRIBUTOR
user: login: HildaM
created_at: 2025-04-22T13:40:36Z
body: 项目里面很多魔数，这些理应配置在 config.yaml 之中，而不是写死在代码中。涉及过多，后面再改吧
author_association: CONTRIBUTOR
user: login: HildaM
created_at: 2025-04-22T15:02:16Z
body: 是的，为了测试，这里可以优化一下



---- 回复的原邮件 ----
| 发件人 | ***@***.***> |
| 日期 | 2025年04月22日 22:55 |
| 收件人 | ***@***.***> |
| 抄送至 | ***@***.***>***@***.***> |
| 主题 | Re: [RaspberryCola/AI-Cloud-Go] feat：优化项目代码，增加ollama本地embedding部署，增加docker-compose一键部署外部依赖 (PR #2) |

@RaspberryCola commented on this pull request.

In internal/database/milvus.go:

> -					Name:       "id",
-					DataType:   entity.FieldTypeVarChar,
-					PrimaryKey: true,
-					AutoID:     false,
-					TypeParams: map[string]string{
-						"max_length": "64",
-					},
-				},
-				{
-					Name:     "content",
-					DataType: entity.FieldTypeVarChar,
-					TypeParams: map[string]string{
-						"max_length": "65535",
-					},
+	if exists {
+		// 如果集合已存在，先删除它


感谢你的 PR！关于 initTextChunksCollection 中 ​​“集合存在则先删除”​​ 的逻辑，我想了解一下这个修改的考虑。
我认为这样会导致每次项目启动时清空集合，是否是为了测试环境开发方便？

In internal/service/embedding_service.go:

> +type OllamaEmbeddingService struct {
+	baseURL   string
+	model     string
+	embedder  *openai.Embedder // 作为标准接口保留，但不直接使用
+	dimension int
+}
+
+// NewOpenAIEmbeddingService 创建OpenAI嵌入服务实例
+func NewOpenAIEmbeddingService(ctx context.Context) (*OpenAIEmbeddingService, error) {
+	cfg := config.GetConfig().Embedding.Remote
+
+	apiKey := cfg.APIKey
+	model := cfg.Model
+	baseURL := cfg.BaseURL
+
+	dimension := 1024 // 默认维度


感谢贡献！关于embedding这里，不同嵌入模型的维度规格存在差异，我认为openai和ollama的维度设置也应该从配置中获取。另外这个改动也会影响milvus中collection的向量维度配置，在后续迭代中进一步完善这部分逻辑。

—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you authored the thread.Message ID: ***@***.***>
author_association: CONTRIBUTOR