From e8c6f987e2532b85748a03025866c595ab811ccb Mon Sep 17 00:00:00 2001 From: cagoya <2561694274@qq.com> Date: Thu, 4 Dec 2025 10:36:12 +0800 Subject: [PATCH 1/5] =?UTF-8?q?docs=EF=BC=9A=E6=9B=B4=E6=96=B0README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 88 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 61 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index db53637..3d9d5fe 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ + ## 简介 @@ -12,43 +13,81 @@ 搭建了一个存储图片的Web应用,实现的功能如下: -1. 用户注册登录和修改个人信息 +1. 用户注册、登录和修改个人信息 2. 支持用户登录后上传图片,会自动从图片中提取拍摄时间、地点和像素等信息 3. 支持用户自定义标签和AI识图添加标签 4. 支持在浏览器中对图片进行编辑 5. 显示图片缩略图,支持图片轮播 6. 支持按照多种条件搜索,支持AI对话搜索 +7. 可以删除和下载图片 ### 技术栈 - 前端:Vue(ts) + Element Plus -- 后端:gin(golang) + gorm -- 数据库:MySQL + Redis +- 后端:gin(golang) + gorm + viper + zap +- 数据库:PostgreSQL + Redis + Qdrant - 鉴权:JWT ## 本地开发指南 +### 环境推荐 + +- **go:1.24+** +- **node:v22 / npm:10** + ### clone 本项目 ```bash git clone git@github.com:cagoya/ImageHub.git ``` -### 安装 mysql +### 安装 PostgreSQL + +> 这里的docker部署都没启用映射卷,如认为有必要请自行添加。 -**开发环境**的数据库可以跑在docker容器中,这里使用docker部署mysql +**开发和测试环境**的数据库可以选择使用docker部署 ```bash # 或者指定版本 -docker pull mysql:latest -docker run -d --name mysql-server -p 3306:3306 -e MYSQL_ROOT_PASSWORD="123456" mysql +docker pull postgres +docker run -d \ + --name postgre-server\ + -p 5432:5432\ + -e POSTGRES_PASSWORD="123456"\ + postgres +``` + +为了方便查询,可一并拉取PgAdmin,这里的邮箱随意,只起到一个用户名的作用。 + +```bash +docker pull dpage/pgadmin4 +docker run -d \ + --name pgadmin4 \ + -p 5433:80 \ + -e 'PGADMIN_DEFAULT_EMAIL=user@example.com' \ + -e 'PGADMIN_DEFAULT_PASSWORD=123456' \ + dpage/pgadmin4 ``` ### 安装Redis +还是使用docker部署,需要启动键空间通知(Key-space Notifications)机制 + ```bash docker pull redis -docker run -d --name redis-server -p 6379:6379 redis +docker run -d\ + --name redis-server\ + -p 6379:6379\ + --notify-keyspace-events Ex\ + redis +``` + +### 安装Qdrant + +```bash +docker pull qdrant/qdrant +docker run -p 6333:6333 -p 6334:6334 \ + qdrant/qdrant ``` ### 启动后端 @@ -73,11 +112,18 @@ npm install npm run dev ``` +### 启动mcp server + +```bash +cd ./mcp +go run main.go +``` + ## Docker 部署指南 -### 使用 Docker Compose 一键部署 +### 使用 Docker Compose -项目已配置好 Docker Compose,可以一键启动所有服务(MySQL、Redis、后端、前端)。 +项目已配置好 Docker Compose,可以一键启动所有服务 ```bash # 在项目根目录执行 @@ -100,30 +146,18 @@ docker-compose down -v - **前端**: http://localhost:5173 - **后端 API**: http://localhost:8888/api -- **MySQL**: localhost:3306 +- **PostgreSQL**: localhost:5432 - **Redis**: localhost:6379 - -### 单独构建镜像 - -如果需要单独构建前后端镜像: - -```bash -# 构建后端镜像 -cd server -docker build -t imagehub-backend:latest . - -# 构建前端镜像 -cd web -docker build -t imagehub-frontend:latest . -``` +- **Qdrant**:localhost:6334 ### 环境变量配置 -后端服务支持通过环境变量 `CONFIG_FILE` 指定配置文件,默认为 `server.yaml`。Docker 环境中会自动使用 `docker.yaml` 配置,该配置已设置好容器间的服务名称(mysql、redis)。 +后端服务支持通过环境变量 `CONFIG_FILE` 指定配置文件,开发环境为 `server.yaml`。Docker 环境中会自动使用 `docker.yaml` 配置,该配置已设置好容器间的服务名称。 ### 数据持久化 Docker Compose 已配置数据卷持久化: -- MySQL 数据存储在 `mysql_data` 卷中 +- PostgreSQL 数据存储在 `mysql_data` 卷中 - Redis 数据存储在 `redis_data` 卷中 +- Qdrant 数据存储在 `qdrant_data` 卷中 - 后端上传的图片和日志通过挂载目录持久化 \ No newline at end of file From d6ff7d5904b6767c4ca4035f4c64ee337c4d3a9b Mon Sep 17 00:00:00 2001 From: cagoya <2561694274@qq.com> Date: Thu, 4 Dec 2025 11:13:28 +0800 Subject: [PATCH 2/5] =?UTF-8?q?deploy=EF=BC=9A=E8=B0=83=E6=95=B4docker?= =?UTF-8?q?=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 52 ++++++++++++++++++++++++++++++++++----- mcp/Dockerfile | 38 ++++++++++++++++++++++++++++ mcp/main.go | 18 ++++++++++++-- server/config/docker.yaml | 11 ++++++++- server/config/server.yaml | 9 ++++++- server/config/test.yaml | 4 ++- server/global/qdrant.go | 6 ++--- server/main.go | 2 +- server/utils/ali.go | 12 ++++++--- server/utils/baidu.go | 17 ++++++++++--- server/utils/location.go | 9 ++++++- web/Dockerfile | 23 +++++++++++++---- web/nginx.conf | 44 +++++++++++++++++++++++++++++++++ 13 files changed, 218 insertions(+), 27 deletions(-) create mode 100644 mcp/Dockerfile create mode 100644 web/nginx.conf diff --git a/docker-compose.yml b/docker-compose.yml index cc1a9d2..7beb854 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -37,6 +37,43 @@ services: timeout: 3s retries: 5 + # Qdrant 向量数据库服务 + qdrant: + image: qdrant/qdrant:latest + container_name: imagehub-qdrant + restart: unless-stopped + ports: + - "6333:6333" # HTTP API + - "6334:6334" # gRPC API + volumes: + - qdrant_data:/qdrant/storage + networks: + - imagehub-network + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:6333/health"] + interval: 10s + timeout: 5s + retries: 5 + + # MCP 服务 + mcp: + build: + context: ./mcp + dockerfile: Dockerfile + container_name: imagehub-mcp + restart: unless-stopped + depends_on: + qdrant: + condition: service_healthy + ports: + - "7777:7777" + environment: + - TZ=Asia/Shanghai + - QDRANT_HOST=qdrant + - QDRANT_PORT=6334 + networks: + - imagehub-network + # 后端服务 backend: build: @@ -49,6 +86,8 @@ services: condition: service_healthy redis: condition: service_healthy + qdrant: + condition: service_healthy ports: - "8888:8888" volumes: @@ -60,7 +99,7 @@ services: networks: - imagehub-network - # 前端服务 + # 前端服务 (Nginx) frontend: build: context: ./web @@ -70,13 +109,14 @@ services: depends_on: - backend ports: - - "5173:5173" - volumes: - - ./web:/app - - /app/node_modules + - "80:80" networks: - imagehub-network networks: imagehub-network: - driver: bridge \ No newline at end of file + driver: bridge + +volumes: + qdrant_data: + driver: local \ No newline at end of file diff --git a/mcp/Dockerfile b/mcp/Dockerfile new file mode 100644 index 0000000..c2c79a9 --- /dev/null +++ b/mcp/Dockerfile @@ -0,0 +1,38 @@ +# 构建阶段 +FROM golang:1.24-alpine AS builder + +# 设置工作目录 +WORKDIR /app + +# 安装必要的构建工具 +RUN apk add --no-cache git + +# 复制 go mod 文件 +COPY go.mod go.sum ./ + +# 下载依赖 +RUN go mod download + +# 复制源代码 +COPY . . + +# 构建应用 +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o mcp-server main.go + +# 运行阶段 +FROM alpine:latest + +# 安装必要的运行时依赖 +RUN apk --no-cache add ca-certificates tzdata + +WORKDIR /app + +# 从构建阶段复制二进制文件 +COPY --from=builder /app/mcp-server . + +# 暴露端口 +EXPOSE 7777 + +# 运行应用 +CMD ["./mcp-server"] + diff --git a/mcp/main.go b/mcp/main.go index 850cd87..6260c5c 100644 --- a/mcp/main.go +++ b/mcp/main.go @@ -5,6 +5,8 @@ import ( "fmt" "log" "net/http" + "os" + "strconv" "mcp_server/utils" @@ -40,9 +42,21 @@ func QdrantSearch(ctx context.Context, _ *mcp.CallToolRequest, in QdrantSearchPa } // 3. 连接 Qdrant + // 从环境变量获取 Qdrant 配置,默认为 localhost:6334 + qdrantHost := os.Getenv("QDRANT_HOST") + if qdrantHost == "" { + qdrantHost = "localhost" + } + qdrantPort := 6334 + if portEnv := os.Getenv("QDRANT_PORT"); portEnv != "" { + if port, err := strconv.Atoi(portEnv); err == nil { + qdrantPort = port + } + } + qdrantClient, err := qdrant.NewClient(&qdrant.Config{ - Host: "localhost", - Port: 6334, + Host: qdrantHost, + Port: qdrantPort, }) if err != nil { return nil, QdrantSearchResult{}, fmt.Errorf("连接 Qdrant 失败: %v", err) diff --git a/server/config/docker.yaml b/server/config/docker.yaml index d3dce0c..2626a98 100644 --- a/server/config/docker.yaml +++ b/server/config/docker.yaml @@ -9,6 +9,9 @@ redis: port: 6379 password: "123456" db: 0 +qdrant: + host: qdrant + port: 6334 zap: prefix: image_hub timeFormat: 2006/01/02 - 15:04:05.00000 @@ -24,4 +27,10 @@ zap: compress: true output: - "./log/image_hub.log" - +gaode: + apiKey: "eaf59456283e912bb394b3f4970027e1" +baidu: + apiKey: "siOl6J2rzuM4CIgD3ZuDpVKY" + secretKey: "YyFRinKgfSMOKR8MtowoA0os8gxPpphv" +ali: + apikey: "sk-9ef56cbc5d944b65b1bb612fd7864877" \ No newline at end of file diff --git a/server/config/server.yaml b/server/config/server.yaml index 468e62e..1505d1d 100644 --- a/server/config/server.yaml +++ b/server/config/server.yaml @@ -26,4 +26,11 @@ zap: backups: 10 compress: true output: - - "./log/image_hub.log" \ No newline at end of file + - "./log/image_hub.log" +gaode: + apiKey: "eaf59456283e912bb394b3f4970027e1" +baidu: + apiKey: "siOl6J2rzuM4CIgD3ZuDpVKY" + secretKey: "YyFRinKgfSMOKR8MtowoA0os8gxPpphv" +ali: + apikey: "sk-9ef56cbc5d944b65b1bb612fd7864877" \ No newline at end of file diff --git a/server/config/test.yaml b/server/config/test.yaml index d120583..c928829 100644 --- a/server/config/test.yaml +++ b/server/config/test.yaml @@ -11,4 +11,6 @@ redis: db: 1 qdrant: host: localhost - port: 6334 \ No newline at end of file + port: 6334 +gaode: + apiKey: "eaf59456283e912bb394b3f4970027e1" \ No newline at end of file diff --git a/server/global/qdrant.go b/server/global/qdrant.go index df619fd..fb0d506 100644 --- a/server/global/qdrant.go +++ b/server/global/qdrant.go @@ -10,10 +10,10 @@ import ( var qdrantClient *qdrant.Client -func InitChroma() error { +func InitQdrant() error { var err error - host := viper.GetString("chroma.host") - port := viper.GetInt("chroma.port") + host := viper.GetString("qdrant.host") + port := viper.GetInt("qdrant.port") qdrantClient, err = qdrant.NewClient(&qdrant.Config{ Host: host, Port: port, diff --git a/server/main.go b/server/main.go index ea3bf1f..c217fa1 100644 --- a/server/main.go +++ b/server/main.go @@ -40,7 +40,7 @@ func main() { } logger.Info("redis初始化成功") // 初始化Qdrant - err = global.InitChroma() + err = global.InitQdrant() if err != nil { panic(err) } diff --git a/server/utils/ali.go b/server/utils/ali.go index b136674..6a27c49 100644 --- a/server/utils/ali.go +++ b/server/utils/ali.go @@ -10,9 +10,15 @@ import ( "os" "path/filepath" "strings" + + "github.com/spf13/viper" ) -const ALI_APIKEY = "sk-9ef56cbc5d944b65b1bb612fd7864877" +// getAliApiKey 获取阿里云API密钥,在函数调用时动态读取配置 +func getAliApiKey() string { + return viper.GetString("ali.apikey") +} + const API_URL = "https://dashscope.aliyuncs.com/api/v1/services/embeddings/multimodal-embedding/multimodal-embedding" const MODEL = "tongyi-embedding-vision-plus" @@ -144,7 +150,7 @@ func VectorizeImage(imageURL string) ([]float64, error) { } // 设置请求头 - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ALI_APIKEY)) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", getAliApiKey())) req.Header.Set("Content-Type", "application/json") // 发送请求 @@ -215,7 +221,7 @@ func VectorizeText(text string) ([]float64, error) { } // 设置请求头 - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ALI_APIKEY)) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", getAliApiKey())) req.Header.Set("Content-Type", "application/json") // 发送请求 diff --git a/server/utils/baidu.go b/server/utils/baidu.go index d5f804e..9fc5b55 100644 --- a/server/utils/baidu.go +++ b/server/utils/baidu.go @@ -15,10 +15,21 @@ import ( "github.com/redis/go-redis/v9" "server/global" + + "github.com/spf13/viper" ) -const BAIDU_APIKEY = "siOl6J2rzuM4CIgD3ZuDpVKY" -const SECRET_KEY = "YyFRinKgfSMOKR8MtowoA0os8gxPpphv" +// getBaiduApiKey 获取百度AI API密钥,在函数调用时动态读取配置 +func getBaiduApiKey() string { + return viper.GetString("baidu.apiKey") +} + +// getBaiduSecretKey 获取百度AI Secret Key,在函数调用时动态读取配置 +func getBaiduSecretKey() string { + return viper.GetString("baidu.secretKey") +} + +var SECRET_KEY = viper.GetString("baidu.secretKey") /** * 使用 AK,SK 生成鉴权签名(Access Token) @@ -34,7 +45,7 @@ func GetAccessToken() string { // 如果Redis中没有AccessToken,则重新获取 tokenUrl := "https://aip.baidubce.com/oauth/2.0/token" - postData := fmt.Sprintf("grant_type=client_credentials&client_id=%s&client_secret=%s", BAIDU_APIKEY, SECRET_KEY) + postData := fmt.Sprintf("grant_type=client_credentials&client_id=%s&client_secret=%s", getBaiduApiKey(), getBaiduSecretKey()) resp, err := http.Post(tokenUrl, "application/x-www-form-urlencoded", strings.NewReader(postData)) if err != nil { diff --git a/server/utils/location.go b/server/utils/location.go index cacbdc7..e625055 100644 --- a/server/utils/location.go +++ b/server/utils/location.go @@ -6,8 +6,15 @@ import ( "io" "net/http" "net/url" + + "github.com/spf13/viper" ) +// getGaodeApiKey 获取高德地图API密钥,在函数调用时动态读取配置 +func getGaodeApiKey() string { + return viper.GetString("gaode.apiKey") +} + // 完整的响应结构体,这个只在本文件内使用 type apiResponse struct { Status string `json:"status"` @@ -43,7 +50,7 @@ func ReverseGeocode(longitude float64, latitude float64) *GeoInfo { // 构建查询参数 params := url.Values{} - params.Add("key", "eaf59456283e912bb394b3f4970027e1") + params.Add("key", getGaodeApiKey()) params.Add("location", fmt.Sprintf("%f,%f", longitude, latitude)) params.Add("extensions", "base") diff --git a/web/Dockerfile b/web/Dockerfile index ef478fd..e3f8ed3 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -1,4 +1,5 @@ -FROM node:20-alpine +# 构建阶段 +FROM node:20-alpine AS builder # 设置工作目录 WORKDIR /app @@ -12,8 +13,20 @@ RUN npm ci # 复制源代码 COPY . . -# 暴露 Vite 默认端口 -EXPOSE 5173 +# 构建生产版本 +RUN npm run build -# 启动开发服务器 -CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] \ No newline at end of file +# 运行阶段 - 使用 Nginx +FROM nginx:alpine + +# 复制构建好的文件到 nginx 目录 +COPY --from=builder /app/dist /usr/share/nginx/html + +# 复制 nginx 配置文件 +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# 暴露端口 +EXPOSE 80 + +# 启动 nginx +CMD ["nginx", "-g", "daemon off;"] diff --git a/web/nginx.conf b/web/nginx.conf new file mode 100644 index 0000000..98e1157 --- /dev/null +++ b/web/nginx.conf @@ -0,0 +1,44 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + # 启用 gzip 压缩 + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/json application/javascript; + + # SPA 路由支持 - 所有路由都返回 index.html + location / { + try_files $uri $uri/ /index.html; + } + + # 静态资源缓存 + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # API 代理到后端服务 + location /api { + proxy_pass http://backend:8888; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + } + + # 健康检查 + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } +} + From edb0caf84c3ecde3eebbb9ec5bf85023ee057fa7 Mon Sep 17 00:00:00 2001 From: cagoya <2561694274@qq.com> Date: Thu, 4 Dec 2025 11:51:40 +0800 Subject: [PATCH 3/5] =?UTF-8?q?add=EF=BC=9A=E8=B0=83=E6=95=B4=E5=89=8D?= =?UTF-8?q?=E7=AB=AF=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/components/AppHeader.vue | 4 +-- web/src/components/ImageCard.vue | 6 ----- web/src/views/404.vue | 45 +++++++++++++++++++++++--------- web/src/views/Chat.vue | 34 +++++++----------------- 4 files changed, 44 insertions(+), 45 deletions(-) diff --git a/web/src/components/AppHeader.vue b/web/src/components/AppHeader.vue index 8e8da1d..4bcab22 100644 --- a/web/src/components/AppHeader.vue +++ b/web/src/components/AppHeader.vue @@ -13,7 +13,7 @@ 查看图片 AI搜图 用户管理 - 配置 + 设置
@@ -59,7 +59,7 @@ 用户管理 - 配置 + 设置
diff --git a/web/src/components/ImageCard.vue b/web/src/components/ImageCard.vue index bc6c279..d6ea867 100644 --- a/web/src/components/ImageCard.vue +++ b/web/src/components/ImageCard.vue @@ -487,12 +487,6 @@ const handleDelete = () => { padding: 0px; } -.tags-label { - margin-bottom: 6px; - display: block; - font-weight: 600; -} - .tags-container { display: flex; flex-wrap: nowrap; diff --git a/web/src/views/404.vue b/web/src/views/404.vue index 0bb5542..5444321 100644 --- a/web/src/views/404.vue +++ b/web/src/views/404.vue @@ -6,18 +6,25 @@ 4 \ No newline at end of file diff --git a/web/src/views/Chat.vue b/web/src/views/Chat.vue index 36c6f6c..725dffd 100644 --- a/web/src/views/Chat.vue +++ b/web/src/views/Chat.vue @@ -119,7 +119,7 @@
新对话 - 对话 {{ selectedChatID }} + 对话 {{ getChatDisplayNumber(selectedChatID) }}
@@ -308,27 +308,17 @@ const formatTime = (time: string): string => { return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); }; -// 格式化聊天时间 -const formatChatTime = (time: string): string => { - const date = new Date(time); - const now = new Date(); - const diff = now.getTime() - date.getTime(); - const days = Math.floor(diff / (1000 * 60 * 60 * 24)); - - if (days === 0) { - return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); - } else if (days === 1) { - return '昨天'; - } else if (days < 7) { - return `${days}天前`; - } else { - return date.toLocaleDateString(); - } +// 获取对话的显示编号(根据在列表中的位置从1开始) +const getChatDisplayNumber = (chatID: number): number => { + const index = chatSessions.value.findIndex(c => c.id === chatID); + // 如果找到,使用索引+1作为显示编号;如果没找到,使用原始ID + return index !== -1 ? index + 1 : chatID; }; -// 格式化聊天标题 +// 格式化聊天标题(根据用户在列表中的位置从1开始编号) const formatChatTitle = (chat: ChatSession): string => { - return `对话 ${chat.id}`; + const displayNumber = getChatDisplayNumber(chat.id); + return `对话 ${displayNumber}`; }; // 配置 marked 选项 @@ -914,12 +904,6 @@ watch(selectedChatID, () => { width: 100%; } -.chat-time { - font-size: 12px; - color: #7f8c8d; - white-space: nowrap; -} - :deep(.chat-session-item:hover), :deep(.chat-session-item.is-active) { background: linear-gradient(135deg, #f8fbff, #f0f7ff); From 36414d8e8ca21610d01619f1e61ed1b377877718 Mon Sep 17 00:00:00 2001 From: cagoya <2561694274@qq.com> Date: Thu, 4 Dec 2025 16:35:58 +0800 Subject: [PATCH 4/5] =?UTF-8?q?deploy=EF=BC=9A=E5=AE=8C=E5=96=84Ningx?= =?UTF-8?q?=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 27 ++++++++++++----------- docker-compose.yml | 36 ++++++------------------------- web/.env | 3 +++ web/Dockerfile | 6 ++++++ web/env.d.ts | 8 +++++++ web/nginx.conf | 46 ++++++++++++++++++++++++++++++---------- web/src/api/index.ts | 21 +++++++++++++++--- web/src/views/Upload.vue | 2 +- 8 files changed, 90 insertions(+), 59 deletions(-) create mode 100644 web/.env diff --git a/README.md b/README.md index 3d9d5fe..4252ae3 100644 --- a/README.md +++ b/README.md @@ -137,27 +137,26 @@ docker-compose logs -f # 停止所有服务 docker-compose down - -# 停止并删除数据卷(注意:会删除数据库数据) -docker-compose down -v ``` -### 服务访问地址 +### 服务访问 + +对于部署机,可以使用下述地址访问各项服务: -- **前端**: http://localhost:5173 +- **前端**: http://localhost:80 - **后端 API**: http://localhost:8888/api - **PostgreSQL**: localhost:5432 - **Redis**: localhost:6379 -- **Qdrant**:localhost:6334 +- **Qdrant(gRpc)**:localhost:6334 + +对于其它设备(其它PC或移动设备),需要将localhost换成部署机的ip地址,并且只能访问前端(使用了`Nginx`代理) -### 环境变量配置 +### 配置 -后端服务支持通过环境变量 `CONFIG_FILE` 指定配置文件,开发环境为 `server.yaml`。Docker 环境中会自动使用 `docker.yaml` 配置,该配置已设置好容器间的服务名称。 +后端服务支持通过环境变量 `CONFIG_FILE` 指定配置文件: -### 数据持久化 +- 开发环境为 `server.yaml` +- 测试环境为 `test.yaml` +- 生产环境(docker)为 `docker.yaml` -Docker Compose 已配置数据卷持久化: -- PostgreSQL 数据存储在 `mysql_data` 卷中 -- Redis 数据存储在 `redis_data` 卷中 -- Qdrant 数据存储在 `qdrant_data` 卷中 -- 后端上传的图片和日志通过挂载目录持久化 \ No newline at end of file +前端在开发和测试环境会硬编码后端服务的URL,在生产环境(docker)使用`Nginx`代理,故不需要担心部署机ip的变化 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 7beb854..dd46af6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,11 +15,6 @@ services: - "5432:5432" networks: - imagehub-network - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] - interval: 10s - timeout: 5s - retries: 5 # Redis 服务 redis: @@ -31,11 +26,6 @@ services: - "6379:6379" networks: - imagehub-network - healthcheck: - test: ["CMD", "redis-cli", "-a", "123456", "ping"] - interval: 10s - timeout: 3s - retries: 5 # Qdrant 向量数据库服务 qdrant: @@ -45,15 +35,8 @@ services: ports: - "6333:6333" # HTTP API - "6334:6334" # gRPC API - volumes: - - qdrant_data:/qdrant/storage networks: - imagehub-network - healthcheck: - test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:6333/health"] - interval: 10s - timeout: 5s - retries: 5 # MCP 服务 mcp: @@ -63,8 +46,7 @@ services: container_name: imagehub-mcp restart: unless-stopped depends_on: - qdrant: - condition: service_healthy + - qdrant ports: - "7777:7777" environment: @@ -82,12 +64,9 @@ services: container_name: imagehub-backend restart: unless-stopped depends_on: - postgres: - condition: service_healthy - redis: - condition: service_healthy - qdrant: - condition: service_healthy + - postgres + - redis + - qdrant ports: - "8888:8888" volumes: @@ -104,6 +83,7 @@ services: build: context: ./web dockerfile: Dockerfile + # 生产环境推荐:不设置 args,使用相对路径 /api(通过 Nginx 代理到后端) container_name: imagehub-frontend restart: unless-stopped depends_on: @@ -115,8 +95,4 @@ services: networks: imagehub-network: - driver: bridge - -volumes: - qdrant_data: - driver: local \ No newline at end of file + driver: bridge \ No newline at end of file diff --git a/web/.env b/web/.env new file mode 100644 index 0000000..7f173ac --- /dev/null +++ b/web/.env @@ -0,0 +1,3 @@ +# 后端服务地址 +# 开发环境使用实际的后端 IP 地址 +VITE_BACKEND_URL=http://10.162.199.212:8888 diff --git a/web/Dockerfile b/web/Dockerfile index e3f8ed3..72768dd 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -13,6 +13,12 @@ RUN npm ci # 复制源代码 COPY . . +# 构建参数:后端 URL(可选) +# 生产环境推荐:不设置此参数,使用相对路径 /api(通过 Nginx 代理到后端) +# 开发环境可选:设置 VITE_BACKEND_URL 为完整 URL(如 http://localhost:8888)直接访问后端 +ARG VITE_BACKEND_URL= +ENV VITE_BACKEND_URL=$VITE_BACKEND_URL + # 构建生产版本 RUN npm run build diff --git a/web/env.d.ts b/web/env.d.ts index 323c78a..ecf9b2f 100644 --- a/web/env.d.ts +++ b/web/env.d.ts @@ -1,5 +1,13 @@ /// +interface ImportMetaEnv { + readonly VITE_BACKEND_URL?: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} + declare module '*.vue' { import type { DefineComponent } from 'vue' const component: DefineComponent<{}, {}, any> diff --git a/web/nginx.conf b/web/nginx.conf index 98e1157..990e1e0 100644 --- a/web/nginx.conf +++ b/web/nginx.conf @@ -3,6 +3,9 @@ server { server_name localhost; root /usr/share/nginx/html; index index.html; + + # 全局文件上传大小限制(在 location 中也可以单独设置) + client_max_body_size 20m; # 启用 gzip 压缩 gzip on; @@ -10,17 +13,6 @@ server { gzip_min_length 1024; gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/json application/javascript; - # SPA 路由支持 - 所有路由都返回 index.html - location / { - try_files $uri $uri/ /index.html; - } - - # 静态资源缓存 - location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { - expires 1y; - add_header Cache-Control "public, immutable"; - } - # API 代理到后端服务 location /api { proxy_pass http://backend:8888; @@ -32,6 +24,38 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_cache_bypass $http_upgrade; + + # 文件上传配置 + client_max_body_size 20m; # 允许上传最大 20MB 的文件(与前端限制一致) + proxy_read_timeout 300s; # 读取后端响应的超时时间(5分钟) + proxy_connect_timeout 60s; # 连接后端的超时时间 + proxy_send_timeout 300s; # 发送请求到后端的超时时间(5分钟) + } + + # 静态文件代理到后端服务(图片文件) + # 使用 ^~ 修饰符确保优先匹配,不会被后续的正则匹配覆盖 + location ^~ /public { + proxy_pass http://backend:8888; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # 图片文件缓存配置 + expires 30d; + add_header Cache-Control "public, immutable"; + } + + # 静态资源缓存(前端构建的静态资源) + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # SPA 路由支持 - 所有路由都返回 index.html + location / { + try_files $uri $uri/ /index.html; } # 健康检查 diff --git a/web/src/api/index.ts b/web/src/api/index.ts index 142f90b..1839175 100644 --- a/web/src/api/index.ts +++ b/web/src/api/index.ts @@ -1,10 +1,25 @@ import axios from 'axios'; import { type UploadRawFile } from 'element-plus'; -// 后端的地址,方便调整 -export const backendURL = 'http://10.162.199.212:8888'; +// 后端的地址,从环境变量获取,如果没有则使用相对路径(适用于 Docker nginx 代理) +// 开发环境可以通过 .env 文件设置 VITE_BACKEND_URL +// Docker 构建时可以通过构建参数设置 +const getBackendURL = (): string => { + // 优先使用环境变量 + const envURL = import.meta.env.VITE_BACKEND_URL; + if (envURL) { + return envURL; + } + // 如果没有设置环境变量,使用相对路径(适用于 Docker 中的 nginx 代理) + // 在开发环境中,如果需要直接访问后端,可以在 .env 文件中设置 VITE_BACKEND_URL + return ''; +}; + +export const backendURL = getBackendURL(); -const apiURL = `${backendURL}/api`; +// 构建 API URL:如果 backendURL 为空,则使用相对路径 /api(适用于 nginx 代理) +// 否则使用完整的 URL +const apiURL = backendURL ? `${backendURL}/api` : '/api'; // 创建axios实例 const instance = axios.create({ diff --git a/web/src/views/Upload.vue b/web/src/views/Upload.vue index 9987933..cc0415d 100644 --- a/web/src/views/Upload.vue +++ b/web/src/views/Upload.vue @@ -2,7 +2,7 @@
-

批量图片上传

+

图片上传

支持上传多张图片,系统将自动处理您的图片

From 91f55057595b0e57520bbb86e41cd4283b37ee2c Mon Sep 17 00:00:00 2001 From: cagoya <2561694274@qq.com> Date: Thu, 4 Dec 2025 16:43:30 +0800 Subject: [PATCH 5/5] =?UTF-8?q?fix=EF=BC=9A=E6=8A=BD=E5=87=BAdeepseek?= =?UTF-8?q?=E7=9A=84apikey?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/config/docker.yaml | 4 +++- server/config/server.yaml | 4 +++- server/service/mcp_service.go | 7 ++++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/server/config/docker.yaml b/server/config/docker.yaml index 2626a98..1fa690f 100644 --- a/server/config/docker.yaml +++ b/server/config/docker.yaml @@ -33,4 +33,6 @@ baidu: apiKey: "siOl6J2rzuM4CIgD3ZuDpVKY" secretKey: "YyFRinKgfSMOKR8MtowoA0os8gxPpphv" ali: - apikey: "sk-9ef56cbc5d944b65b1bb612fd7864877" \ No newline at end of file + apikey: "sk-9ef56cbc5d944b65b1bb612fd7864877" +deepseek: + apiKey: "sk-884a6ecd756644cdb4ead4240b0b73cd" \ No newline at end of file diff --git a/server/config/server.yaml b/server/config/server.yaml index 1505d1d..f017c4a 100644 --- a/server/config/server.yaml +++ b/server/config/server.yaml @@ -33,4 +33,6 @@ baidu: apiKey: "siOl6J2rzuM4CIgD3ZuDpVKY" secretKey: "YyFRinKgfSMOKR8MtowoA0os8gxPpphv" ali: - apikey: "sk-9ef56cbc5d944b65b1bb612fd7864877" \ No newline at end of file + apikey: "sk-9ef56cbc5d944b65b1bb612fd7864877" +deepseek: + apiKey: "sk-884a6ecd756644cdb4ead4240b0b73cd" \ No newline at end of file diff --git a/server/service/mcp_service.go b/server/service/mcp_service.go index 2c2ff46..43af839 100644 --- a/server/service/mcp_service.go +++ b/server/service/mcp_service.go @@ -12,6 +12,7 @@ import ( "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/openai/openai-go" "github.com/openai/openai-go/option" + "github.com/spf13/viper" "gorm.io/gorm" ) @@ -21,9 +22,13 @@ type MCPService struct { mcpServerURL string } +func getDeepseekAPIKey() string { + return viper.GetString("deepseek.api_key") +} + func NewMCPService(db *gorm.DB) *MCPService { // 初始化 OpenAI 客户端(使用 DeepSeek API) - apiKey := "sk-884a6ecd756644cdb4ead4240b0b73cd" + apiKey := getDeepseekAPIKey() client := openai.NewClient( option.WithAPIKey(apiKey), option.WithBaseURL("https://api.deepseek.com/v1"),