diff --git a/.air.toml b/.air.toml
new file mode 100644
index 00000000..ac235403
--- /dev/null
+++ b/.air.toml
@@ -0,0 +1,46 @@
+root = "."
+testdata_dir = "testdata"
+tmp_dir = "tmp"
+
+[build]
+ args_bin = []
+ bin = "./tmp/main"
+ cmd = "go build -o ./tmp/main ./cmd/server"
+ delay = 1000
+ exclude_dir = ["assets", "tmp", "vendor", "testdata", "frontend", "migrations", "node_modules", "docs"]
+ exclude_file = []
+ exclude_regex = ["_test.go"]
+ exclude_unchanged = false
+ follow_symlink = false
+ # 使用 full_bin 来设置环境变量(从父进程继承)
+ full_bin = ""
+ include_dir = []
+ include_ext = ["go", "tpl", "tmpl", "html", "yaml"]
+ include_file = []
+ kill_delay = "0s"
+ log = "build-errors.log"
+ poll = false
+ poll_interval = 0
+ rerun = false
+ rerun_delay = 500
+ send_interrupt = false
+ stop_on_error = false
+
+[color]
+ app = ""
+ build = "yellow"
+ main = "magenta"
+ runner = "green"
+ watcher = "cyan"
+
+[log]
+ main_only = false
+ time = false
+
+[misc]
+ clean_on_exit = false
+
+[screen]
+ clear_on_rebuild = false
+ keep_scroll = true
+
diff --git a/.gitignore b/.gitignore
index eff04ae2..97c58d13 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,6 +23,8 @@ node_modules/
# 临时文件
tmp/
temp/
+logs/
+*.pid
WeKnora
/models/
diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md
new file mode 100644
index 00000000..a3b14fe6
--- /dev/null
+++ b/DEVELOPMENT.md
@@ -0,0 +1,267 @@
+# WeKnora 开发环境快速入门
+
+> 无需重新构建 Docker 镜像,实现秒级代码热更新!
+
+## 🚀 快速开始(3 种方式)
+
+### 方式 1:推荐 - 使用 Make 命令
+
+**终端 1** - 启动基础设施:
+```bash
+make dev-start
+```
+
+**终端 2** - 启动后端:
+```bash
+make dev-app
+```
+
+**终端 3** - 启动前端:
+```bash
+make dev-frontend
+```
+
+访问 http://localhost:5173 开始开发!
+
+### 方式 2:使用脚本命令
+
+```bash
+# 终端 1
+./scripts/dev.sh start
+
+# 终端 2
+./scripts/dev.sh app
+
+# 终端 3
+./scripts/dev.sh frontend
+```
+
+### 方式 3:一键启动(交互式)
+
+```bash
+./scripts/quick-dev.sh
+```
+
+按照提示选择是否启动后端和前端。
+
+## 📋 常用命令
+
+| 命令 | 说明 |
+|------|------|
+| `make dev-start` | 启动基础设施(postgres, redis, minio 等) |
+| `make dev-stop` | 停止所有服务 |
+| `make dev-restart` | 重启所有服务 |
+| `make dev-status` | 查看服务状态 |
+| `make dev-logs` | 查看服务日志 |
+| `make dev-app` | 启动后端(需先启动基础设施) |
+| `make dev-frontend` | 启动前端(需先启动基础设施) |
+
+## 🎯 访问地址
+
+| 服务 | 地址 |
+|------|------|
+| 前端开发服务器 | http://localhost:5173 |
+| 后端 API | http://localhost:8080 |
+| PostgreSQL | localhost:5432 |
+| Redis | localhost:6379 |
+| MinIO Console | http://localhost:9001 |
+| Neo4j Browser | http://localhost:7474 |
+| Jaeger UI | http://localhost:16686 |
+
+## 💡 开发工作流对比
+
+### ❌ 旧方式(慢)
+
+```bash
+# 每次修改代码后
+sh scripts/build_images.sh -p # 重新构建镜像(2-5分钟)
+sh scripts/start_all.sh --no-pull # 重启容器
+```
+
+### ✅ 新方式(快)
+
+```bash
+# 首次启动(只需一次)
+make dev-start
+
+# 修改后端代码
+# → Ctrl+C 停止 → make dev-app 重启(5-10秒)
+
+# 修改前端代码
+# → 自动热重载(无需任何操作)
+```
+
+**时间对比**:
+- 旧方式:每次修改 2-5 分钟
+- 新方式首次:1-2 分钟
+- 新方式后续:
+ - 后端修改:5-10 秒
+ - 前端修改:实时热重载
+
+## 🔥 进阶:后端热重载
+
+安装 Air 实现后端代码修改后自动重启:
+
+```bash
+# 安装 Air
+go install github.com/cosmtrek/air@latest
+
+# 确保 $GOPATH/bin 在 PATH 中
+export PATH=$PATH:$(go env GOPATH)/bin
+
+# 使用 Air 启动(自动检测)
+make dev-app
+# 或直接运行
+air
+```
+
+修改 Go 代码后,Air 会自动重新编译和重启,无需手动操作!
+
+## 📝 开发场景示例
+
+### 场景 1:只修改前端
+
+```bash
+# 一次性启动基础设施
+make dev-start
+
+# 启动前端,修改代码自动热重载
+make dev-frontend
+```
+
+### 场景 2:只修改后端
+
+```bash
+# 启动基础设施
+make dev-start
+
+# 启动后端(如果安装了 Air,支持热重载)
+make dev-app
+```
+
+### 场景 3:同时开发前后端
+
+```bash
+# 终端 1:启动基础设施
+make dev-start
+
+# 终端 2:启动后端
+make dev-app
+
+# 终端 3:启动前端
+make dev-frontend
+```
+
+## 🛠 VS Code 调试配置
+
+创建 `.vscode/launch.json`:
+
+```json
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Launch WeKnora Server",
+ "type": "go",
+ "request": "launch",
+ "mode": "auto",
+ "program": "${workspaceFolder}/cmd/server",
+ "env": {
+ "DB_HOST": "localhost",
+ "DB_PORT": "5432",
+ "DOCREADER_ADDR": "localhost:50051",
+ "MINIO_ENDPOINT": "localhost:9000",
+ "REDIS_ADDR": "localhost:6379",
+ "OTEL_EXPORTER_OTLP_ENDPOINT": "localhost:4317",
+ "NEO4J_URI": "bolt://localhost:7687"
+ }
+ }
+ ]
+}
+```
+
+然后按 F5 开始调试!
+
+## 🐛 故障排除
+
+### 问题:启动后端时报错连接不到数据库
+
+**解决**:确保先运行了 `make dev-start` 并等待 30 秒让服务完全启动。
+
+查看服务状态:
+```bash
+make dev-status
+```
+
+### 问题:前端访问 API 时报 CORS 错误
+
+**解决**:前端已配置代理,确保:
+1. 后端运行在 `localhost:8080`
+2. 前端运行在 `localhost:5173`
+3. 查看 `frontend/vite.config.ts` 的代理配置
+
+### 问题:端口被占用
+
+**解决**:修改 `.env` 文件中的端口配置:
+
+```bash
+# .env
+DB_PORT=5433 # 默认 5432
+REDIS_PORT=6380 # 默认 6379
+MINIO_PORT=9001 # 默认 9000
+```
+
+然后重启服务:
+```bash
+make dev-restart
+```
+
+### 问题:DocReader 需要重新构建
+
+**解决**:DocReader 仍使用 Docker 镜像,需要重新构建:
+
+```bash
+sh scripts/build_images.sh -d
+make dev-restart
+```
+
+## 🎯 生产环境部署
+
+开发完成后需要部署时:
+
+```bash
+# 构建所有镜像
+sh scripts/build_images.sh
+
+# 或只构建特定镜像
+sh scripts/build_images.sh -p # 后端
+sh scripts/build_images.sh -f # 前端
+sh scripts/build_images.sh -d # DocReader
+
+# 启动生产环境
+sh scripts/start_all.sh
+```
+
+## 📚 更多文档
+
+- [完整开发指南](docs/开发指南.md)
+- [API 文档](docs/API.md)
+- [Agent 开发](docs/AGENT.md)
+
+## 💪 最佳实践
+
+1. **日常开发**:使用 `make dev-*` 命令,享受快速迭代
+2. **提交前测试**:使用完整 Docker 环境测试集成功能
+3. **生产部署**:使用构建镜像的方式部署
+
+## 🎉 总结
+
+使用开发模式,你可以:
+
+✅ **不重新构建镜像** - 直接在本地运行代码
+✅ **秒级热更新** - 前端自动热重载,后端快速重启
+✅ **完整调试支持** - IDE 断点调试,实时查看变量
+✅ **节省时间** - 每次修改从 2-5 分钟降至 5-10 秒
+
+祝你开发愉快!🚀
+
diff --git a/Makefile b/Makefile
index 1ac93ab4..f74e4c46 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-.PHONY: help build run test clean docker-build-app docker-build-docreader docker-build-frontend docker-build-all docker-run migrate-up migrate-down docker-restart docker-stop start-all stop-all start-ollama stop-ollama build-images build-images-app build-images-docreader build-images-frontend clean-images check-env list-containers pull-images show-platform
+.PHONY: help build run test clean docker-build-app docker-build-docreader docker-build-frontend docker-build-all docker-run migrate-up migrate-down docker-restart docker-stop start-all stop-all start-ollama stop-ollama build-images build-images-app build-images-docreader build-images-frontend clean-images check-env list-containers pull-images show-platform dev-start dev-stop dev-restart dev-logs dev-status dev-app dev-frontend
# Show help
help:
@@ -46,6 +46,15 @@ help:
@echo " list-containers 列出运行中的容器"
@echo " pull-images 拉取最新镜像"
@echo " show-platform 显示当前构建平台"
+ @echo ""
+ @echo "开发模式(推荐):"
+ @echo " dev-start 启动开发环境基础设施(仅启动依赖服务)"
+ @echo " dev-stop 停止开发环境"
+ @echo " dev-restart 重启开发环境"
+ @echo " dev-logs 查看开发环境日志"
+ @echo " dev-status 查看开发环境状态"
+ @echo " dev-app 启动后端应用(本地运行,需先运行 dev-start)"
+ @echo " dev-frontend 启动前端(本地运行,需先运行 dev-start)"
# Go related variables
BINARY_NAME=WeKnora
@@ -212,4 +221,26 @@ show-platform:
@echo "当前系统架构: $(shell uname -m)"
@echo "Docker构建平台: $(PLATFORM)"
+# Development mode commands
+dev-start:
+ ./scripts/dev.sh start
+
+dev-stop:
+ ./scripts/dev.sh stop
+
+dev-restart:
+ ./scripts/dev.sh restart
+
+dev-logs:
+ ./scripts/dev.sh logs
+
+dev-status:
+ ./scripts/dev.sh status
+
+dev-app:
+ ./scripts/dev.sh app
+
+dev-frontend:
+ ./scripts/dev.sh frontend
+
diff --git a/README_CN.md b/README_CN.md
index 5903a727..bbb48bc3 100644
--- a/README_CN.md
+++ b/README_CN.md
@@ -260,6 +260,33 @@ WeKnora 支持将文档转化为知识图谱,展示文档中不同段落之间
## 🧭 开发指南
+### ⚡ 快速开发模式(推荐)
+
+如果你需要频繁修改代码,**不需要每次重新构建 Docker 镜像**!使用快速开发模式:
+
+```bash
+# 方式 1:使用 Make 命令(推荐)
+make dev-start # 启动基础设施
+make dev-app # 启动后端(新终端)
+make dev-frontend # 启动前端(新终端)
+
+# 方式 2:一键启动
+./scripts/quick-dev.sh
+
+# 方式 3:使用脚本
+./scripts/dev.sh start # 启动基础设施
+./scripts/dev.sh app # 启动后端(新终端)
+./scripts/dev.sh frontend # 启动前端(新终端)
+```
+
+**开发优势:**
+- ✅ 前端修改自动热重载(无需重启)
+- ✅ 后端修改快速重启(5-10秒,支持 Air 热重载)
+- ✅ 无需重新构建 Docker 镜像
+- ✅ 支持 IDE 断点调试
+
+**详细文档:** [开发环境快速入门](./DEVELOPMENT.md) | [完整开发指南](./docs/开发指南.md)
+
### 📁 项目目录结构
```
@@ -277,8 +304,19 @@ WeKnora/
### 🔧 常用命令
```bash
-# 清空数据库(慎用!)
-make clean-db
+# 开发模式
+make dev-start # 启动开发环境基础设施
+make dev-stop # 停止开发环境
+make dev-app # 启动后端应用(本地)
+make dev-frontend # 启动前端(本地)
+
+# 生产部署
+make build-images # 构建所有镜像
+make start-all # 启动所有服务
+make stop-all # 停止所有服务
+
+# 数据库
+make clean-db # 清空数据库(慎用!)
```
## 🤝 贡献指南
diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
new file mode 100644
index 00000000..9cdea507
--- /dev/null
+++ b/docker-compose.dev.yml
@@ -0,0 +1,148 @@
+# 开发环境配置 - 只启动基础设施服务,app 和 frontend 在本地运行
+services:
+ # 只启动依赖的基础设施服务
+ postgres:
+ image: paradedb/paradedb:v0.18.9-pg17
+ container_name: WeKnora-postgres-dev
+ ports:
+ - "${DB_PORT:-5432}:5432"
+ environment:
+ - POSTGRES_USER=${DB_USER}
+ - POSTGRES_PASSWORD=${DB_PASSWORD}
+ - POSTGRES_DB=${DB_NAME}
+ volumes:
+ - postgres-data-dev:/var/lib/postgresql/data
+ - ./migrations/paradedb/00-init-db.sql:/docker-entrypoint-initdb.d/00-init-db.sql
+ - ./migrations/paradedb/01-migrate-to-paradedb.sql:/docker-entrypoint-initdb.d/01-migrate-to-paradedb.sql
+ - ./migrations/paradedb/02-add-agent-config-if-missing.sql:/docker-entrypoint-initdb.d/02-add-agent-config-if-missing.sql
+ - ./migrations/paradedb/03-cleanup-unreferenced-models.sql:/docker-entrypoint-initdb.d/03-cleanup-unreferenced-models.sql
+ networks:
+ - WeKnora-network-dev
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
+ interval: 10s
+ timeout: 10s
+ retries: 3
+ start_period: 30s
+ restart: unless-stopped
+ stop_grace_period: 1m
+
+ redis:
+ image: redis:7.0-alpine
+ container_name: WeKnora-redis-dev
+ ports:
+ - "${REDIS_PORT:-6379}:6379"
+ volumes:
+ - redis_data_dev:/data
+ command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
+ restart: always
+ networks:
+ - WeKnora-network-dev
+
+ minio:
+ image: minio/minio:latest
+ container_name: WeKnora-minio-dev
+ ports:
+ - "${MINIO_PORT:-9000}:9000"
+ - "${MINIO_CONSOLE_PORT:-9001}:9001"
+ environment:
+ - MINIO_ROOT_USER=${MINIO_ACCESS_KEY_ID:-minioadmin}
+ - MINIO_ROOT_PASSWORD=${MINIO_SECRET_ACCESS_KEY:-minioadmin}
+ command: server --console-address ":9001" /data
+ volumes:
+ - minio_data_dev:/data
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
+ interval: 30s
+ timeout: 20s
+ retries: 3
+ networks:
+ - WeKnora-network-dev
+
+ neo4j:
+ image: neo4j:latest
+ container_name: WeKnora-neo4j-dev
+ volumes:
+ - neo4j-data-dev:/data
+ environment:
+ - NEO4J_AUTH=${NEO4J_USERNAME:-neo4j}/${NEO4J_PASSWORD:-password}
+ - NEO4J_apoc_export_file_enabled=true
+ - NEO4J_apoc_import_file_enabled=true
+ - NEO4J_apoc_import_file_use__neo4j__config=true
+ - NEO4JLABS_PLUGINS=["apoc"]
+ ports:
+ - "7474:7474"
+ - "7687:7687"
+ restart: always
+ networks:
+ - WeKnora-network-dev
+
+ docreader:
+ image: wechatopenai/weknora-docreader:latest
+ container_name: WeKnora-docreader-dev
+ ports:
+ - "${DOCREADER_PORT:-50051}:50051"
+ environment:
+ - COS_SECRET_ID=${COS_SECRET_ID:-}
+ - COS_SECRET_KEY=${COS_SECRET_KEY:-}
+ - COS_REGION=${COS_REGION:-}
+ - COS_BUCKET_NAME=${COS_BUCKET_NAME:-}
+ - COS_APP_ID=${COS_APP_ID:-}
+ - COS_PATH_PREFIX=${COS_PATH_PREFIX:-}
+ - COS_ENABLE_OLD_DOMAIN=${COS_ENABLE_OLD_DOMAIN:-}
+ - VLM_MODEL_BASE_URL=${VLM_MODEL_BASE_URL:-}
+ - VLM_MODEL_NAME=${VLM_MODEL_NAME:-}
+ - VLM_MODEL_API_KEY=${VLM_MODEL_API_KEY:-}
+ - STORAGE_TYPE=${STORAGE_TYPE:-}
+ - MINIO_PUBLIC_ENDPOINT=http://localhost:${MINIO_PORT:-9000}
+ - MINIO_ENDPOINT=minio:9000
+ - MINIO_ACCESS_KEY_ID=${MINIO_ACCESS_KEY_ID:-minioadmin}
+ - MINIO_SECRET_ACCESS_KEY=${MINIO_SECRET_ACCESS_KEY:-minioadmin}
+ - MINIO_BUCKET_NAME=${MINIO_BUCKET_NAME:-}
+ - MINIO_USE_SSL=${MINIO_USE_SSL:-}
+ - WEB_PROXY=${WEB_PROXY:-}
+ healthcheck:
+ test: ["CMD", "grpc_health_probe", "-addr=:50051"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+ start_period: 60s
+ networks:
+ - WeKnora-network-dev
+ restart: unless-stopped
+ extra_hosts:
+ - "host.docker.internal:host-gateway"
+
+ jaeger:
+ image: jaegertracing/all-in-one:latest
+ container_name: WeKnora-jaeger-dev
+ ports:
+ - "6831:6831/udp"
+ - "6832:6832/udp"
+ - "5778:5778"
+ - "16686:16686"
+ - "4317:4317"
+ - "4318:4318"
+ - "14250:14250"
+ - "14268:14268"
+ - "9411:9411"
+ environment:
+ - COLLECTOR_OTLP_ENABLED=true
+ - COLLECTOR_ZIPKIN_HOST_PORT=:9411
+ volumes:
+ - jaeger_data_dev:/var/lib/jaeger
+ networks:
+ - WeKnora-network-dev
+ restart: unless-stopped
+
+networks:
+ WeKnora-network-dev:
+ driver: bridge
+
+volumes:
+ postgres-data-dev:
+ redis_data_dev:
+ minio_data_dev:
+ neo4j-data-dev:
+ jaeger_data_dev:
+
diff --git a/docs/开发指南.md b/docs/开发指南.md
new file mode 100644
index 00000000..16093845
--- /dev/null
+++ b/docs/开发指南.md
@@ -0,0 +1,283 @@
+# WeKnora 开发指南
+
+## 快速开发模式(推荐)
+
+如果你需要频繁修改 `app` 或 `frontend` 代码,**不需要每次都重新构建 Docker 镜像**,可以使用本地开发模式。
+
+### 方式一:使用 Make 命令(推荐)
+
+#### 1. 启动基础设施服务
+
+```bash
+make dev-start
+```
+
+这将启动以下服务的 Docker 容器:
+- PostgreSQL(数据库)
+- Redis(缓存)
+- MinIO(对象存储)
+- Neo4j(图数据库)
+- DocReader(文档读取服务)
+- Jaeger(链路追踪)
+
+#### 2. 启动后端应用(新终端)
+
+```bash
+make dev-app
+```
+
+这将在本地直接运行 Go 应用,修改代码后 Ctrl+C 停止,重新运行即可。
+
+#### 3. 启动前端(新终端)
+
+```bash
+make dev-frontend
+```
+
+这将启动 Vite 开发服务器,支持热重载,修改代码后自动刷新。
+
+#### 4. 查看服务状态
+
+```bash
+make dev-status
+```
+
+#### 5. 停止所有服务
+
+```bash
+make dev-stop
+```
+
+### 方式二:使用脚本命令
+
+如果你更喜欢直接使用脚本:
+
+```bash
+# 启动基础设施
+./scripts/dev.sh start
+
+# 启动后端(新终端)
+./scripts/dev.sh app
+
+# 启动前端(新终端)
+./scripts/dev.sh frontend
+
+# 查看日志
+./scripts/dev.sh logs
+
+# 停止所有服务
+./scripts/dev.sh stop
+```
+
+## 访问地址
+
+### 开发环境
+
+- **前端开发服务器**: http://localhost:5173
+- **后端 API**: http://localhost:8080
+- **PostgreSQL**: localhost:5432
+- **Redis**: localhost:6379
+- **MinIO Console**: http://localhost:9001
+- **Neo4j Browser**: http://localhost:7474
+- **Jaeger UI**: http://localhost:16686
+
+## 开发工作流对比
+
+### ❌ 旧方式(慢)
+
+```bash
+# 每次修改代码后都需要:
+sh scripts/build_images.sh -p # 重新构建镜像(很慢)
+sh scripts/start_all.sh --no-pull # 重启容器
+```
+
+**耗时**:每次修改需要 2-5 分钟
+
+### ✅ 新方式(快)
+
+```bash
+# 首次启动(只需要一次):
+make dev-start
+
+# 在另外两个终端分别运行:
+make dev-app # 修改 Go 代码后 Ctrl+C 重启即可(秒级)
+make dev-frontend # 修改前端代码自动热重载(无需重启)
+```
+
+**耗时**:
+- 首次启动:1-2 分钟
+- 后续修改后端:5-10 秒(重启 Go 应用)
+- 后续修改前端:实时热重载
+
+## 使用 Air 实现后端热重载(可选)
+
+如果你希望后端代码修改后也能自动重启,可以安装 `air`:
+
+### 1. 安装 Air
+
+```bash
+go install github.com/cosmtrek/air@latest
+```
+
+### 2. 创建配置文件
+
+在项目根目录创建 `.air.toml`:
+
+```toml
+root = "."
+testdata_dir = "testdata"
+tmp_dir = "tmp"
+
+[build]
+ args_bin = []
+ bin = "./tmp/main"
+ cmd = "go build -o ./tmp/main ./cmd/server"
+ delay = 1000
+ exclude_dir = ["assets", "tmp", "vendor", "testdata", "frontend", "migrations"]
+ exclude_file = []
+ exclude_regex = ["_test.go"]
+ exclude_unchanged = false
+ follow_symlink = false
+ full_bin = ""
+ include_dir = []
+ include_ext = ["go", "tpl", "tmpl", "html", "yaml"]
+ include_file = []
+ kill_delay = "0s"
+ log = "build-errors.log"
+ poll = false
+ poll_interval = 0
+ rerun = false
+ rerun_delay = 500
+ send_interrupt = false
+ stop_on_error = false
+
+[color]
+ app = ""
+ build = "yellow"
+ main = "magenta"
+ runner = "green"
+ watcher = "cyan"
+
+[log]
+ main_only = false
+ time = false
+
+[misc]
+ clean_on_exit = false
+
+[screen]
+ clear_on_rebuild = false
+ keep_scroll = true
+```
+
+### 3. 使用 Air 启动
+
+```bash
+# 在项目根目录
+air
+```
+
+现在修改 Go 代码后会自动重新编译和重启!
+
+## 其他开发技巧
+
+### 只修改前端
+
+如果只修改前端,只需要:
+
+```bash
+cd frontend
+npm run dev
+```
+
+前端会连接到 http://localhost:8080 的后端 API。
+
+### 只修改后端
+
+如果只修改后端,只需要:
+
+```bash
+# 启动基础设施
+make dev-start
+
+# 运行后端
+make dev-app
+```
+
+### 调试模式
+
+#### 后端调试
+
+使用 VS Code 或 GoLand 的调试功能,配置连接到本地运行的 Go 应用。
+
+VS Code 配置示例(`.vscode/launch.json`):
+
+```json
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Launch Server",
+ "type": "go",
+ "request": "launch",
+ "mode": "auto",
+ "program": "${workspaceFolder}/cmd/server",
+ "env": {
+ "DB_HOST": "localhost",
+ "DOCREADER_ADDR": "localhost:50051",
+ "MINIO_ENDPOINT": "localhost:9000",
+ "REDIS_ADDR": "localhost:6379",
+ "OTEL_EXPORTER_OTLP_ENDPOINT": "localhost:4317",
+ "NEO4J_URI": "bolt://localhost:7687"
+ },
+ "args": []
+ }
+ ]
+}
+```
+
+#### 前端调试
+
+使用浏览器开发者工具即可,Vite 提供了 source map。
+
+## 生产环境部署
+
+当你完成开发需要部署时,才需要构建镜像:
+
+```bash
+# 构建所有镜像
+sh scripts/build_images.sh
+
+# 或只构建特定镜像
+sh scripts/build_images.sh -p # 只构建后端
+sh scripts/build_images.sh -f # 只构建前端
+
+# 启动生产环境
+sh scripts/start_all.sh
+```
+
+## 常见问题
+
+### Q: 启动 dev-app 时报错连接不到数据库
+
+A: 确保先运行了 `make dev-start`,并等待所有服务启动完成(大约 30 秒)。
+
+### Q: 前端访问 API 时报 CORS 错误
+
+A: 检查前端的代理配置,确保 `vite.config.ts` 中配置了正确的代理。
+
+### Q: DocReader 服务需要重新构建怎么办?
+
+A: DocReader 仍然使用 Docker 镜像,如果需要修改,需要重新构建:
+
+```bash
+sh scripts/build_images.sh -d
+make dev-restart
+```
+
+## 总结
+
+- **日常开发**:使用 `make dev-*` 命令,快速迭代
+- **测试集成**:使用 `sh scripts/start_all.sh --no-pull` 测试完整环境
+- **生产部署**:使用 `sh scripts/build_images.sh` + `sh scripts/start_all.sh`
+
diff --git a/docs/快速开发模式说明.md b/docs/快速开发模式说明.md
new file mode 100644
index 00000000..68b61efc
--- /dev/null
+++ b/docs/快速开发模式说明.md
@@ -0,0 +1,255 @@
+# 快速开发模式说明
+
+## 🎯 问题背景
+
+之前的开发流程中,每次修改 `app`(后端)或 `frontend`(前端)代码后,都需要:
+
+```bash
+# 重新构建 Docker 镜像(耗时 2-5 分钟)
+sh scripts/build_images.sh -p # 构建后端
+sh scripts/build_images.sh -f # 构建前端
+
+# 重启容器
+sh scripts/start_all.sh --no-pull
+```
+
+这个流程非常耗时,严重影响开发效率。
+
+## ✨ 解决方案
+
+现在提供了**快速开发模式**,可以直接在本地运行应用和前端,只在 Docker 中启动基础设施服务(数据库、缓存等),实现:
+
+- ✅ **前端热重载**:修改代码自动刷新,无需重启
+- ✅ **后端快速重启**:修改代码后 5-10 秒即可重启
+- ✅ **无需构建镜像**:跳过耗时的镜像构建过程
+- ✅ **支持调试**:可以使用 IDE 断点调试
+
+## 📦 新增文件
+
+### 1. 核心配置文件
+
+- **`docker-compose.dev.yml`** - 开发环境 Docker Compose 配置
+ - 只启动基础设施服务(postgres, redis, minio, neo4j, docreader, jaeger)
+ - 不启动 app 和 frontend 容器
+
+### 2. 开发脚本
+
+- **`scripts/dev.sh`** - 开发环境管理脚本
+ - 支持启动/停止基础设施
+ - 支持本地运行 app 和 frontend
+ - 自动检测并使用 Air(Go 热重载工具)
+
+- **`scripts/quick-dev.sh`** - 一键启动开发环境脚本
+ - 交互式启动所有服务
+ - 自动创建日志目录
+
+### 3. 配置文件
+
+- **`.air.toml`** - Air 热重载配置
+ - 监控 Go 文件变化
+ - 自动重新编译和重启
+
+- **`frontend/vite.config.ts`** - 更新的 Vite 配置
+ - 添加 API 代理配置
+ - 避免 CORS 问题
+
+### 4. 文档
+
+- **`DEVELOPMENT.md`** - 开发环境快速入门指南
+- **`docs/开发指南.md`** - 完整开发指南
+- **`docs/快速开发模式说明.md`** - 本文档
+
+### 5. Makefile 更新
+
+新增的 Make 命令:
+- `make dev-start` - 启动开发环境基础设施
+- `make dev-stop` - 停止开发环境
+- `make dev-restart` - 重启开发环境
+- `make dev-logs` - 查看服务日志
+- `make dev-status` - 查看服务状态
+- `make dev-app` - 启动后端应用(本地)
+- `make dev-frontend` - 启动前端(本地)
+
+## 🚀 使用方法
+
+### 方式 1:使用 Make 命令(推荐)
+
+```bash
+# 终端 1:启动基础设施
+make dev-start
+
+# 终端 2:启动后端
+make dev-app
+
+# 终端 3:启动前端
+make dev-frontend
+```
+
+### 方式 2:使用开发脚本
+
+```bash
+# 终端 1
+./scripts/dev.sh start
+
+# 终端 2
+./scripts/dev.sh app
+
+# 终端 3
+./scripts/dev.sh frontend
+```
+
+### 方式 3:一键启动(交互式)
+
+```bash
+./scripts/quick-dev.sh
+```
+
+## 📊 效率对比
+
+### 旧方式(慢)
+
+| 操作 | 耗时 |
+|------|------|
+| 修改代码 | - |
+| 重新构建镜像 | 2-5 分钟 |
+| 重启容器 | 30-60 秒 |
+| **总计** | **2.5-6 分钟** |
+
+### 新方式(快)
+
+| 操作 | 耗时 |
+|------|------|
+| 首次启动基础设施 | 1-2 分钟(仅一次) |
+| **后续修改后端** | **5-10 秒** |
+| **后续修改前端** | **实时热重载** |
+
+**效率提升:20-60 倍!**
+
+## 🎓 高级功能
+
+### 使用 Air 实现后端热重载
+
+安装 Air 后,后端代码修改会自动重新编译和重启:
+
+```bash
+# 安装 Air
+go install github.com/cosmtrek/air@latest
+
+# 确保在 PATH 中
+export PATH=$PATH:$(go env GOPATH)/bin
+
+# 使用 Air 启动(自动检测)
+make dev-app
+```
+
+### VS Code 调试配置
+
+创建 `.vscode/launch.json`:
+
+```json
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Launch WeKnora Server",
+ "type": "go",
+ "request": "launch",
+ "mode": "auto",
+ "program": "${workspaceFolder}/cmd/server",
+ "env": {
+ "DB_HOST": "localhost",
+ "DOCREADER_ADDR": "localhost:50051",
+ "MINIO_ENDPOINT": "localhost:9000",
+ "REDIS_ADDR": "localhost:6379",
+ "OTEL_EXPORTER_OTLP_ENDPOINT": "localhost:4317",
+ "NEO4J_URI": "bolt://localhost:7687"
+ }
+ }
+ ]
+}
+```
+
+## 🔄 架构说明
+
+### 开发模式架构
+
+```
+┌─────────────────────────────────────────────────────────┐
+│ 本地开发环境 │
+├─────────────────────────────────────────────────────────┤
+│ │
+│ ┌──────────┐ ┌──────────┐ │
+│ │ 后端 App │◄────────┤ 前端 UI │ │
+│ │ (本地运行)│ │ (本地运行)│ │
+│ │ :8080 │ │ :5173 │ │
+│ └────┬─────┘ └──────────┘ │
+│ │ │
+│ │ 连接基础设施服务 │
+│ ▼ │
+│ ┌─────────────────────────────────────────────────┐ │
+│ │ Docker 基础设施容器 │ │
+│ ├─────────────────────────────────────────────────┤ │
+│ │ PostgreSQL │ Redis │ MinIO │ Neo4j │ DocReader │ │
+│ │ :5432 │ :6379 │ :9000 │ :7687 │ :50051 │ │
+│ └─────────────────────────────────────────────────┘ │
+│ │
+└─────────────────────────────────────────────────────────┘
+```
+
+### 生产模式架构
+
+```
+┌─────────────────────────────────────────────────────────┐
+│ Docker Compose 环境 │
+├─────────────────────────────────────────────────────────┤
+│ │
+│ ┌──────────┐ ┌──────────┐ │
+│ │ 后端 App │◄────────┤ 前端 UI │ │
+│ │ (容器运行)│ │ (容器运行)│ │
+│ │ :8080 │ │ :80 │ │
+│ └────┬─────┘ └──────────┘ │
+│ │ │
+│ ▼ │
+│ ┌─────────────────────────────────────────────────┐ │
+│ │ 基础设施容器 │ │
+│ ├─────────────────────────────────────────────────┤ │
+│ │ PostgreSQL │ Redis │ MinIO │ Neo4j │ DocReader │ │
+│ └─────────────────────────────────────────────────┘ │
+│ │
+└─────────────────────────────────────────────────────────┘
+```
+
+## 🎯 使用场景
+
+### 日常开发
+
+使用**开发模式**(`make dev-*`),享受快速迭代。
+
+### 集成测试
+
+使用**生产模式**(`make start-all`),测试完整环境。
+
+### 生产部署
+
+```bash
+# 构建镜像
+make build-images
+
+# 启动服务
+make start-all
+```
+
+## 🔧 故障排除
+
+详见 [DEVELOPMENT.md](../DEVELOPMENT.md) 的故障排除部分。
+
+## 📚 相关文档
+
+- [开发环境快速入门](../DEVELOPMENT.md)
+- [完整开发指南](./开发指南.md)
+- [API 文档](./API.md)
+
+## 💬 反馈与建议
+
+如有问题或建议,请提交 [Issue](https://github.com/Tencent/WeKnora/issues)。
+
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 87b6c68b..ba50d559 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -16,7 +16,7 @@
"pagefind": "^1.1.1",
"pinia": "^3.0.1",
"tdesign-icons-vue-next": "^0.4.1",
- "tdesign-vue-next": "^1.11.5",
+ "tdesign-vue-next": "^1.17.2",
"vue": "^3.5.13",
"vue-router": "^4.5.0",
"webpack": "^5.94.0"
@@ -3413,9 +3413,10 @@
}
},
"node_modules/tdesign-vue-next": {
- "version": "1.14.2",
- "resolved": "https://mirrors.tencent.com/npm/tdesign-vue-next/-/tdesign-vue-next-1.14.2.tgz",
- "integrity": "sha512-Wcg0SjT4W7JmMhwCI4/rnk8bJzcmjed/YAemVDaOY33U8PqUsU3DxxP/vJ2gbLH7NkFgLGVNpeAdTqYdmHnV4w==",
+ "version": "1.17.2",
+ "resolved": "https://mirrors.tencent.com/npm/tdesign-vue-next/-/tdesign-vue-next-1.17.2.tgz",
+ "integrity": "sha512-nTofPSKGQguOS+Lb62fDiLS1Rs3Ngzb8rE8sPEjfMdDjdFZ5kEUgvVp+J+8GKfgwVRAZRjv//qSzZVt6cqgXxA==",
+ "license": "MIT",
"dependencies": {
"@babel/runtime": "^7.22.6",
"@popperjs/core": "^2.11.8",
@@ -3423,11 +3424,11 @@
"@types/sortablejs": "^1.15.1",
"@types/tinycolor2": "^1.4.3",
"@types/validator": "^13.7.17",
- "dayjs": "1.11.10",
+ "dayjs": "^1.11.10",
"lodash-es": "^4.17.21",
"mitt": "^3.0.1",
"sortablejs": "^1.15.0",
- "tdesign-icons-vue-next": "^0.3.6",
+ "tdesign-icons-vue-next": "~0.4.1",
"tinycolor2": "^1.6.0",
"validator": "^13.9.0"
},
@@ -3438,18 +3439,6 @@
"vue": ">=3.1.0"
}
},
- "node_modules/tdesign-vue-next/node_modules/tdesign-icons-vue-next": {
- "version": "0.3.7",
- "resolved": "https://mirrors.tencent.com/npm/tdesign-icons-vue-next/-/tdesign-icons-vue-next-0.3.7.tgz",
- "integrity": "sha512-Q5ebVty/TCqhBa0l/17kkhjC0pBAOGvn7C35MAt1xS+johKVM9QEDOy9R6XEl332AiwQ37MwqioczqjYC30ckw==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.16.3"
- },
- "peerDependencies": {
- "vue": "^3.0.0"
- }
- },
"node_modules/terser": {
"version": "5.43.1",
"resolved": "https://mirrors.tencent.com/npm/terser/-/terser-5.43.1.tgz",
diff --git a/frontend/src/components/Input-field.vue b/frontend/src/components/Input-field.vue
index 7b049b1a..665bfbbf 100644
--- a/frontend/src/components/Input-field.vue
+++ b/frontend/src/components/Input-field.vue
@@ -1,14 +1,17 @@
diff --git a/frontend/src/components/ModelEditorDialog.vue b/frontend/src/components/ModelEditorDialog.vue
index f69ac66c..152bee1c 100644
--- a/frontend/src/components/ModelEditorDialog.vue
+++ b/frontend/src/components/ModelEditorDialog.vue
@@ -31,64 +31,66 @@
@@ -153,12 +155,31 @@
@@ -231,6 +252,9 @@ const checking = ref(false)
const remoteChecked = ref(false)
const remoteAvailable = ref(false)
const remoteMessage = ref('')
+const dimensionChecked = ref(false)
+const dimensionSuccess = ref(false)
+const dimensionMessage = ref('')
// Ollama 模型状态
const ollamaModelList = ref([])
@@ -248,7 +272,7 @@ const formData = ref({
modelName: '',
baseUrl: '',
apiKey: '',
- dimension: 1536,
+ dimension: undefined,
interfaceType: 'ollama',
isDefault: false
})
@@ -338,7 +362,7 @@ const resetForm = () => {
modelName: '',
baseUrl: '',
apiKey: '',
- dimension: props.modelType === 'embedding' ? 1536 : undefined,
+ dimension: undefined, // 默认不填,让用户手动输入或通过检测按钮获取
interfaceType: undefined,
isDefault: false
}
@@ -347,6 +371,9 @@ const resetForm = () => {
remoteChecked.value = false
remoteAvailable.value = false
remoteMessage.value = ''
+ dimensionChecked.value = false
+ dimensionSuccess.value = false
+ dimensionMessage.value = ''
}
// 监听来源变化,重置校验状态(已合并到下面的 watch)
@@ -436,6 +463,45 @@ const checkModelStatus = async () => {
}
}
+// 检查 Ollama 本地 Embedding 模型维度
+const checkOllamaDimension = async () => {
+ if (!formData.value.modelName || formData.value.source !== 'local' || props.modelType !== 'embedding') {
+ return
+ }
+
+ checking.value = true
+ dimensionChecked.value = false
+ dimensionMessage.value = ''
+
+ try {
+ const result = await testEmbeddingModel({
+ source: 'local',
+ modelName: formData.value.modelName,
+ dimension: formData.value.dimension
+ })
+
+ dimensionChecked.value = true
+ dimensionSuccess.value = result.available || false
+
+ if (result.available && result.dimension) {
+ formData.value.dimension = result.dimension
+ dimensionMessage.value = `检测成功,向量维度:${result.dimension}`
+ MessagePlugin.success(dimensionMessage.value)
+ } else {
+ dimensionMessage.value = result.message || '检测失败,请手动输入维度'
+ MessagePlugin.warning(dimensionMessage.value)
+ }
+ } catch (error: any) {
+ console.error('检测 Ollama 模型维度失败:', error)
+ dimensionChecked.value = true
+ dimensionSuccess.value = false
+ dimensionMessage.value = error.message || '检测失败,请手动输入维度'
+ MessagePlugin.error(dimensionMessage.value)
+ } finally {
+ checking.value = false
+ }
+}
+
// 检查 Remote API 连接(根据模型类型调用不同的接口)
const checkRemoteAPI = async () => {
if (!formData.value.modelName || !formData.value.baseUrl) {
@@ -570,18 +636,31 @@ const handleConfirm = async () => {
}
}
-// 监听模型选择变化(处理下载逻辑)
-watch(() => formData.value.modelName, async (newValue) => {
- if (!newValue || !newValue.startsWith('__download__')) return
+// 监听模型选择变化(处理下载逻辑和自动维度检测提示)
+watch(() => formData.value.modelName, async (newValue, oldValue) => {
+ if (!newValue) return
- // 提取模型名称
- const modelName = newValue.replace('__download__', '')
+ // 处理下载逻辑
+ if (newValue.startsWith('__download__')) {
+ // 提取模型名称
+ const modelName = newValue.replace('__download__', '')
+
+ // 重置选择(避免显示 __download__ 前缀)
+ formData.value.modelName = ''
+
+ // 开始下载
+ await startDownload(modelName)
+ return
+ }
- // 重置选择(避免显示 __download__ 前缀)
- formData.value.modelName = ''
-
- // 开始下载
- await startDownload(modelName)
+ // 如果是 embedding 模型且选择的是 Ollama 本地模型,且模型名称发生了实际变化
+ if (props.modelType === 'embedding' &&
+ formData.value.source === 'local' &&
+ newValue !== oldValue &&
+ oldValue !== '') {
+ // 提示用户可以检测维度
+ MessagePlugin.info('模型已选择,点击"检测维度"按钮自动获取向量维度')
+ }
})
// 开始下载模型
@@ -658,6 +737,9 @@ watch(() => formData.value.source, () => {
remoteChecked.value = false
remoteAvailable.value = false
remoteMessage.value = ''
+ dimensionChecked.value = false
+ dimensionSuccess.value = false
+ dimensionMessage.value = ''
// 清理下载状态
searchKeyword.value = ''
@@ -670,6 +752,13 @@ watch(() => formData.value.source, () => {
currentDownloadModel.value = ''
})
+// 监听模型名称变化,清理维度检测状态
+watch(() => formData.value.modelName, () => {
+ dimensionChecked.value = false
+ dimensionSuccess.value = false
+ dimensionMessage.value = ''
+})
+
// 取消
const handleCancel = () => {
dialogVisible.value = false
@@ -1000,10 +1089,21 @@ const handleCancel = () => {
}
}
+.model-select-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+
+ .t-select {
+ flex: 1;
+ }
+}
+
.refresh-btn {
- margin-top: 4px;
+ margin-top: 0;
font-size: 12px;
color: #666666;
+ flex-shrink: 0;
&:hover {
color: #07C05F;
@@ -1014,5 +1114,32 @@ const handleCancel = () => {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
+
+// 维度控制样式
+.dimension-control {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+
+ :deep(.t-input) {
+ flex: 1;
+ }
+}
+
+.dimension-check-btn {
+ flex-shrink: 0;
+ font-size: 12px;
+}
+
+.dimension-hint {
+ margin: 8px 0 0 0;
+ font-size: 13px;
+ line-height: 1.5;
+ color: #e34d59;
+
+ &.success {
+ color: #07C05F;
+ }
+}
diff --git a/frontend/src/stores/settings.ts b/frontend/src/stores/settings.ts
index 1bd537ae..6ca82f81 100644
--- a/frontend/src/stores/settings.ts
+++ b/frontend/src/stores/settings.ts
@@ -84,6 +84,14 @@ export const useSettingsStore = defineStore("settings", {
// Agent 是否启用
isAgentEnabled: (state) => state.settings.isAgentEnabled || false,
+ // Agent 是否就绪(配置完整)
+ isAgentReady: (state) => {
+ const config = state.settings.agentConfig || defaultSettings.agentConfig
+ return config.thinkingModelId !== '' &&
+ config.rerankModelId !== '' &&
+ config.allowedTools.length > 0
+ },
+
// 获取 Agent 配置
agentConfig: (state) => state.settings.agentConfig || defaultSettings.agentConfig,
diff --git a/frontend/src/views/settings/AgentSettings.vue b/frontend/src/views/settings/AgentSettings.vue
index 34552c1b..59826838 100644
--- a/frontend/src/views/settings/AgentSettings.vue
+++ b/frontend/src/views/settings/AgentSettings.vue
@@ -3,23 +3,40 @@
-
-
-
-
-
启用后,AI 将能够使用工具进行多步推理和跨知识库搜索
-
-
-
-
-
@@ -35,8 +52,7 @@
:max="30"
:step="1"
:marks="{ 1: '1', 5: '5', 10: '10', 15: '15', 20: '20', 25: '25', 30: '30' }"
- @change="handleMaxIterationsChange"
- :disabled="!localAgentEnabled"
+ @change="handleMaxIterationsChangeDebounced"
style="width: 200px;"
/>
{{ localMaxIterations }}
@@ -141,7 +157,6 @@
:step="0.1"
:marks="{ 0: '0', 0.5: '0.5', 1: '1' }"
@change="handleTemperatureChange"
- :disabled="!localAgentEnabled"
style="width: 200px;"
/>
{{ localTemperature.toFixed(1) }}
@@ -161,7 +176,6 @@
multiple
placeholder="请选择工具..."
@change="handleAllowedToolsChange"
- :disabled="!localAgentEnabled"
style="width: 400px;"
>
-
diff --git a/frontend/src/views/settings/OllamaSettings.vue b/frontend/src/views/settings/OllamaSettings.vue
index dd566ca9..4facf0a0 100644
--- a/frontend/src/views/settings/OllamaSettings.vue
+++ b/frontend/src/views/settings/OllamaSettings.vue
@@ -6,59 +6,78 @@
-
+
-
-
启用后可以使用本地 Ollama 模型
+
+
自动检测本地 Ollama 服务是否可用。如果服务未运行或地址配置错误,将显示"不可用"状态
-
+
+
+
+ 检测中
+
+
+
+ 可用
+
+
+
+ 不可用
+
+
+
+ 未检测
+
+
+
+ 重新检测
+
+
-
-
本地 Ollama 服务的 API 地址
+
+
本地 Ollama 服务的 API 地址,由系统自动检测。如需修改,请在 .env 配置文件中设置
@@ -66,8 +85,51 @@
+
+
+
+
+
+
+
+
+ 下载
+
+
+
+
+
+ 正在下载: {{ downloadModelName }}
+ {{ downloadProgress.toFixed(2) }}%
+
+
+
+
+
+
-
+
-
-
-
-
-
-
-
-
-
- 下载
-
-
-
-
-
- 正在下载: {{ downloadModelName }}
- {{ downloadProgress.toFixed(2) }}%
-
-
-
-
-
-
推荐模型:
-
-
- {{ model }}
-
-
-
-
-
@@ -166,7 +175,6 @@ import { checkOllamaStatus, listOllamaModels, downloadOllamaModel, getDownloadPr
const settingsStore = useSettingsStore()
-const localEnabled = ref(settingsStore.settings.ollamaConfig?.enabled ?? true)
const localBaseUrl = ref(settingsStore.settings.ollamaConfig?.baseUrl ?? '')
const testing = ref(false)
@@ -177,32 +185,8 @@ const downloading = ref(false)
const downloadModelName = ref('')
const downloadProgress = ref(0)
-// 推荐的流行模型
-const popularModels = [
- 'qwen2.5:0.5b',
- 'qwen2.5:1.5b',
- 'llama3.2:1b',
- 'llama3.2:3b',
- 'gemma2:2b',
- 'phi3:mini'
-]
-
-// 处理启用/禁用
-const handleEnableToggle = (value: boolean) => {
- settingsStore.updateOllamaConfig({ enabled: value })
- MessagePlugin.success(value ? 'Ollama 已启用' : 'Ollama 已禁用')
-
- if (value) {
- testConnection()
- } else {
- connectionStatus.value = null
- }
-}
-
// 测试连接
const testConnection = async () => {
- if (!localEnabled.value) return
-
testing.value = true
connectionStatus.value = null
@@ -334,12 +318,6 @@ const downloadModel = async () => {
}
}
-// 快速下载推荐模型
-const quickDownload = (modelName: string) => {
- downloadModelName.value = modelName
- downloadModel()
-}
-
// 初始化 Ollama 服务地址
const initOllamaBaseUrl = async () => {
try {
@@ -356,12 +334,10 @@ const initOllamaBaseUrl = async () => {
localBaseUrl.value = 'http://localhost:11434'
}
- // 如果启用了,直接使用初始化时获取的状态,避免重复调用
- if (localEnabled.value) {
+ // 直接使用初始化时获取的状态,避免重复调用
connectionStatus.value = result.available
if (result.available) {
refreshModels()
- }
}
return result
@@ -425,8 +401,7 @@ onMounted(async () => {
.setting-info {
flex: 1;
- max-width: 65%;
- padding-right: 24px;
+ padding-right: 32px;
label {
font-size: 15px;
@@ -440,53 +415,46 @@ onMounted(async () => {
font-size: 13px;
color: #666666;
margin: 0;
- line-height: 1.5;
+ line-height: 1.6;
}
}
.setting-control {
flex-shrink: 0;
- min-width: 420px;
+ min-width: 360px;
+ max-width: 360px;
display: flex;
flex-direction: column;
align-items: flex-end;
}
+.status-display {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+
+ .status-icon.spinning {
+ animation: spin 1s linear infinite;
+ }
+}
+
.url-control-group {
width: 100%;
display: flex;
align-items: center;
gap: 8px;
-
- .status-indicator {
- display: flex;
- align-items: center;
- gap: 4px;
-
- .status-icon {
- font-size: 18px;
-
- &.success {
- color: #07C05F;
- }
-
- &.error {
- color: #e34d59;
- }
-
- &.spinning {
- animation: spin 1s linear infinite;
- }
- }
- }
}
.model-category-section {
- margin-bottom: 24px;
- border: 1px solid #e5e7eb;
- border-radius: 8px;
- padding: 24px;
- background: #ffffff;
+ margin-top: 32px;
+ margin-bottom: 32px;
+ padding-top: 32px;
+ border-top: 1px solid #e5e7eb;
+
+ &:first-of-type {
+ margin-top: 24px;
+ padding-top: 24px;
+ }
&:last-child {
margin-bottom: 0;
@@ -497,24 +465,43 @@ onMounted(async () => {
display: flex;
align-items: flex-start;
justify-content: space-between;
- margin-bottom: 20px;
+ margin-bottom: 24px;
.header-info {
flex: 1;
h3 {
- font-size: 16px;
+ font-size: 17px;
font-weight: 600;
color: #333333;
- margin: 0 0 4px 0;
+ margin: 0 0 6px 0;
}
p {
- font-size: 14px;
- color: #666666;
+ font-size: 13px;
+ color: #999999;
margin: 0;
line-height: 1.5;
}
+
+ .model-link {
+ color: #07C05F;
+ text-decoration: none;
+ font-weight: 500;
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ transition: all 0.2s ease;
+
+ &:hover {
+ color: #05a04f;
+ text-decoration: underline;
+ }
+
+ .link-icon {
+ font-size: 12px;
+ }
+ }
}
}
@@ -523,8 +510,8 @@ onMounted(async () => {
align-items: center;
justify-content: center;
gap: 8px;
- padding: 60px;
- color: #666666;
+ padding: 48px 0;
+ color: #999999;
font-size: 14px;
}
@@ -582,52 +569,28 @@ onMounted(async () => {
.input-group {
display: flex;
gap: 8px;
+ align-items: center;
}
.download-progress {
- padding: 12px;
- background: #f5f7fa;
- border-radius: 6px;
+ padding: 16px;
+ background: #f8f9fa;
+ border-radius: 8px;
+ border: 1px solid #e5e7eb;
.progress-info {
display: flex;
justify-content: space-between;
- margin-bottom: 8px;
+ margin-bottom: 10px;
font-size: 13px;
color: #333333;
- }
- }
-
- .recommended-models {
- .recommended-label {
- font-size: 13px;
- color: #666666;
- margin: 0 0 10px 0;
font-weight: 500;
}
-
- .model-tags {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
-
- .model-tag {
- cursor: pointer;
- transition: all 0.2s;
- font-size: 12px;
-
- &:hover {
- background: #07C05F;
- color: #ffffff;
- border-color: #07C05F;
- }
- }
- }
}
}
.empty-state {
- padding: 80px 0;
+ padding: 48px 0;
text-align: center;
.empty-text {
diff --git a/frontend/src/views/settings/Settings.vue b/frontend/src/views/settings/Settings.vue
index de1ca9f5..9b89b3ff 100644
--- a/frontend/src/views/settings/Settings.vue
+++ b/frontend/src/views/settings/Settings.vue
@@ -73,11 +73,6 @@
-
-
-
-
-
@@ -116,7 +111,6 @@ import TenantInfo from './TenantInfo.vue'
import ApiInfo from './ApiInfo.vue'
import GeneralSettings from './GeneralSettings.vue'
import ModelSettings from './ModelSettings.vue'
-import KnowledgeBaseSettings from './KnowledgeBaseSettings.vue'
import OllamaSettings from './OllamaSettings.vue'
const route = useRoute()
@@ -141,7 +135,6 @@ const navItems = [
]
},
{ key: 'ollama', icon: 'server', label: 'Ollama' },
- { key: 'knowledge', icon: 'folder-open', label: '知识库' },
{ key: 'agent', icon: 'chat', label: 'Agent 配置' },
{ key: 'system', icon: 'info-circle', label: '系统信息' },
{ key: 'tenant', icon: 'user-circle', label: '租户信息' },
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
index 758d2a0a..47647ad5 100644
--- a/frontend/vite.config.ts
+++ b/frontend/vite.config.ts
@@ -12,5 +12,17 @@ export default defineConfig({
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
+ },
+ server: {
+ port: 5173,
+ host: true,
+ // 代理配置,用于开发环境
+ proxy: {
+ '/api': {
+ target: 'http://localhost:8080',
+ changeOrigin: true,
+ secure: false,
+ }
+ }
}
})
diff --git a/internal/models/utils/ollama/ollama.go b/internal/models/utils/ollama/ollama.go
index 4cbe0229..4a5047b7 100644
--- a/internal/models/utils/ollama/ollama.go
+++ b/internal/models/utils/ollama/ollama.go
@@ -2,6 +2,7 @@ package ollama
import (
"context"
+ "encoding/json"
"fmt"
"net/http"
"net/url"
@@ -273,6 +274,11 @@ func (s *OllamaService) ListModelsDetailed(ctx context.Context) ([]OllamaModelIn
if err != nil {
return nil, fmt.Errorf("failed to get model list: %w", err)
}
+ jsonData, err := json.Marshal(listResp.Models)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal model list: %w", err)
+ }
+ logger.GetLogger(ctx).Infof("List models detailed: %s", string(jsonData))
models := make([]OllamaModelInfo, len(listResp.Models))
for i, model := range listResp.Models {
diff --git a/scripts/check-env.sh b/scripts/check-env.sh
new file mode 100755
index 00000000..8a4b163f
--- /dev/null
+++ b/scripts/check-env.sh
@@ -0,0 +1,196 @@
+#!/bin/bash
+# 检查开发环境配置
+
+# 设置颜色
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+RED='\033[0;31m'
+BLUE='\033[0;34m'
+NC='\033[0m' # 无颜色
+
+# 获取项目根目录
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+PROJECT_ROOT="$( cd "$SCRIPT_DIR/.." && pwd )"
+
+log_info() {
+ printf "%b\n" "${BLUE}[INFO]${NC} $1"
+}
+
+log_success() {
+ printf "%b\n" "${GREEN}[✓]${NC} $1"
+}
+
+log_error() {
+ printf "%b\n" "${RED}[✗]${NC} $1"
+}
+
+log_warning() {
+ printf "%b\n" "${YELLOW}[!]${NC} $1"
+}
+
+echo ""
+printf "%b\n" "${GREEN}========================================${NC}"
+printf "%b\n" "${GREEN} WeKnora 开发环境配置检查${NC}"
+printf "%b\n" "${GREEN}========================================${NC}"
+echo ""
+
+cd "$PROJECT_ROOT"
+
+# 检查 .env 文件
+log_info "检查 .env 文件..."
+if [ -f ".env" ]; then
+ log_success ".env 文件存在"
+else
+ log_error ".env 文件不存在"
+ echo ""
+ log_info "解决方法:"
+ echo " 1. 复制示例文件: cp .env.example .env"
+ echo " 2. 编辑 .env 文件并配置必要的环境变量"
+ exit 1
+fi
+
+echo ""
+log_info "检查必要的环境变量..."
+
+# 加载 .env 文件
+set -a
+source .env
+set +a
+
+# 检查必要的环境变量
+errors=0
+
+check_var() {
+ local var_name=$1
+ local var_value="${!var_name}"
+
+ if [ -z "$var_value" ]; then
+ log_error "$var_name 未设置"
+ errors=$((errors + 1))
+ else
+ log_success "$var_name = $var_value"
+ fi
+}
+
+# 数据库配置
+log_info "数据库配置:"
+check_var "DB_DRIVER"
+check_var "DB_HOST"
+check_var "DB_PORT"
+check_var "DB_USER"
+check_var "DB_PASSWORD"
+check_var "DB_NAME"
+
+echo ""
+log_info "存储配置:"
+check_var "STORAGE_TYPE"
+
+if [ "$STORAGE_TYPE" = "minio" ]; then
+ check_var "MINIO_BUCKET_NAME"
+fi
+
+echo ""
+log_info "Redis 配置:"
+check_var "REDIS_ADDR"
+
+echo ""
+log_info "Ollama 配置:"
+check_var "OLLAMA_BASE_URL"
+
+echo ""
+log_info "模型配置:"
+if [ -n "$INIT_LLM_MODEL_NAME" ]; then
+ log_success "INIT_LLM_MODEL_NAME = $INIT_LLM_MODEL_NAME"
+else
+ log_warning "INIT_LLM_MODEL_NAME 未设置(可选)"
+fi
+
+if [ -n "$INIT_EMBEDDING_MODEL_NAME" ]; then
+ log_success "INIT_EMBEDDING_MODEL_NAME = $INIT_EMBEDDING_MODEL_NAME"
+else
+ log_warning "INIT_EMBEDDING_MODEL_NAME 未设置(可选)"
+fi
+
+# 检查 Go 环境
+echo ""
+log_info "检查 Go 环境..."
+if command -v go &> /dev/null; then
+ go_version=$(go version)
+ log_success "Go 已安装: $go_version"
+else
+ log_error "Go 未安装"
+ errors=$((errors + 1))
+fi
+
+# 检查 Air
+if command -v air &> /dev/null; then
+ log_success "Air 已安装(支持热重载)"
+else
+ log_warning "Air 未安装(可选,用于热重载)"
+ log_info "安装命令: go install github.com/cosmtrek/air@latest"
+fi
+
+# 检查 npm
+echo ""
+log_info "检查 Node.js 环境..."
+if command -v npm &> /dev/null; then
+ npm_version=$(npm --version)
+ log_success "npm 已安装: $npm_version"
+else
+ log_error "npm 未安装"
+ errors=$((errors + 1))
+fi
+
+# 检查 Docker
+echo ""
+log_info "检查 Docker 环境..."
+if command -v docker &> /dev/null; then
+ docker_version=$(docker --version)
+ log_success "Docker 已安装: $docker_version"
+
+ if docker info &> /dev/null; then
+ log_success "Docker 服务正在运行"
+ else
+ log_error "Docker 服务未运行"
+ errors=$((errors + 1))
+ fi
+else
+ log_error "Docker 未安装"
+ errors=$((errors + 1))
+fi
+
+# 检查 Docker Compose
+if docker compose version &> /dev/null; then
+ compose_version=$(docker compose version)
+ log_success "Docker Compose 已安装: $compose_version"
+elif command -v docker-compose &> /dev/null; then
+ compose_version=$(docker-compose --version)
+ log_success "docker-compose 已安装: $compose_version"
+else
+ log_error "Docker Compose 未安装"
+ errors=$((errors + 1))
+fi
+
+# 总结
+echo ""
+printf "%b\n" "${GREEN}========================================${NC}"
+if [ $errors -eq 0 ]; then
+ log_success "所有检查通过!环境配置正常"
+ echo ""
+ log_info "下一步:"
+ echo " 1. 启动开发环境: make dev-start"
+ echo " 2. 启动后端: make dev-app"
+ echo " 3. 启动前端: make dev-frontend"
+else
+ log_error "发现 $errors 个问题,请修复后再启动开发环境"
+ echo ""
+ log_info "常见问题:"
+ echo " - 如果 .env 文件不存在,请复制 .env.example"
+ echo " - 确保 DB_DRIVER 设置为 'postgres' 或 'mysql'"
+ echo " - 确保 Docker 服务正在运行"
+fi
+printf "%b\n" "${GREEN}========================================${NC}"
+echo ""
+
+exit $errors
+
diff --git a/scripts/dev.sh b/scripts/dev.sh
new file mode 100755
index 00000000..899ca60f
--- /dev/null
+++ b/scripts/dev.sh
@@ -0,0 +1,287 @@
+#!/bin/bash
+# 开发环境启动脚本 - 只启动基础设施,app 和 frontend 需要手动在本地运行
+
+# 设置颜色
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+RED='\033[0;31m'
+BLUE='\033[0;34m'
+NC='\033[0m' # 无颜色
+
+# 获取项目根目录
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+PROJECT_ROOT="$( cd "$SCRIPT_DIR/.." && pwd )"
+
+# 日志函数
+log_info() {
+ printf "%b\n" "${BLUE}[INFO]${NC} $1"
+}
+
+log_success() {
+ printf "%b\n" "${GREEN}[SUCCESS]${NC} $1"
+}
+
+log_error() {
+ printf "%b\n" "${RED}[ERROR]${NC} $1"
+}
+
+log_warning() {
+ printf "%b\n" "${YELLOW}[WARNING]${NC} $1"
+}
+
+# 选择可用的 Docker Compose 命令
+DOCKER_COMPOSE_BIN=""
+DOCKER_COMPOSE_SUBCMD=""
+
+detect_compose_cmd() {
+ if docker compose version &> /dev/null; then
+ DOCKER_COMPOSE_BIN="docker"
+ DOCKER_COMPOSE_SUBCMD="compose"
+ return 0
+ fi
+ if command -v docker-compose &> /dev/null; then
+ if docker-compose version &> /dev/null; then
+ DOCKER_COMPOSE_BIN="docker-compose"
+ DOCKER_COMPOSE_SUBCMD=""
+ return 0
+ fi
+ fi
+ return 1
+}
+
+# 显示帮助信息
+show_help() {
+ printf "%b\n" "${GREEN}WeKnora 开发环境脚本${NC}"
+ echo "用法: $0 [命令]"
+ echo ""
+ echo "命令:"
+ echo " start 启动基础设施服务(postgres, redis, minio, neo4j, docreader, jaeger)"
+ echo " stop 停止所有服务"
+ echo " restart 重启所有服务"
+ echo " logs 查看服务日志"
+ echo " status 查看服务状态"
+ echo " app 启动后端应用(本地运行)"
+ echo " frontend 启动前端开发服务器(本地运行)"
+ echo " help 显示此帮助信息"
+ echo ""
+ echo "示例:"
+ echo " $0 start # 启动所有基础设施"
+ echo " $0 app # 在另一个终端启动后端"
+ echo " $0 frontend # 在另一个终端启动前端"
+}
+
+# 检查 Docker
+check_docker() {
+ if ! command -v docker &> /dev/null; then
+ log_error "未安装Docker,请先安装Docker"
+ return 1
+ fi
+
+ if ! detect_compose_cmd; then
+ log_error "未检测到 Docker Compose"
+ return 1
+ fi
+
+ if ! docker info &> /dev/null; then
+ log_error "Docker服务未运行"
+ return 1
+ fi
+
+ return 0
+}
+
+# 启动基础设施服务
+start_services() {
+ log_info "启动开发环境基础设施服务..."
+
+ check_docker
+ if [ $? -ne 0 ]; then
+ return 1
+ fi
+
+ cd "$PROJECT_ROOT"
+
+ # 检查 .env 文件
+ if [ ! -f ".env" ]; then
+ log_error ".env 文件不存在,请先创建"
+ return 1
+ fi
+
+ # 启动服务
+ "$DOCKER_COMPOSE_BIN" $DOCKER_COMPOSE_SUBCMD -f docker-compose.dev.yml up -d
+
+ if [ $? -eq 0 ]; then
+ log_success "基础设施服务已启动"
+ echo ""
+ log_info "服务访问地址:"
+ echo " - PostgreSQL: localhost:5432"
+ echo " - Redis: localhost:6379"
+ echo " - MinIO: localhost:9000 (Console: localhost:9001)"
+ echo " - Neo4j: localhost:7474 (Bolt: localhost:7687)"
+ echo " - DocReader: localhost:50051"
+ echo " - Jaeger: localhost:16686"
+ echo ""
+ log_info "接下来的步骤:"
+ printf "%b\n" "${YELLOW}1. 在新终端运行后端:${NC} cd $PROJECT_ROOT && ./scripts/dev.sh app"
+ printf "%b\n" "${YELLOW}2. 在新终端运行前端:${NC} cd $PROJECT_ROOT && ./scripts/dev.sh frontend"
+ return 0
+ else
+ log_error "服务启动失败"
+ return 1
+ fi
+}
+
+# 停止服务
+stop_services() {
+ log_info "停止开发环境服务..."
+
+ check_docker
+ if [ $? -ne 0 ]; then
+ return 1
+ fi
+
+ cd "$PROJECT_ROOT"
+ "$DOCKER_COMPOSE_BIN" $DOCKER_COMPOSE_SUBCMD -f docker-compose.dev.yml down
+
+ if [ $? -eq 0 ]; then
+ log_success "所有服务已停止"
+ return 0
+ else
+ log_error "服务停止失败"
+ return 1
+ fi
+}
+
+# 重启服务
+restart_services() {
+ stop_services
+ sleep 2
+ start_services
+}
+
+# 查看日志
+show_logs() {
+ cd "$PROJECT_ROOT"
+ "$DOCKER_COMPOSE_BIN" $DOCKER_COMPOSE_SUBCMD -f docker-compose.dev.yml logs -f
+}
+
+# 查看状态
+show_status() {
+ cd "$PROJECT_ROOT"
+ "$DOCKER_COMPOSE_BIN" $DOCKER_COMPOSE_SUBCMD -f docker-compose.dev.yml ps
+}
+
+# 启动后端应用(本地)
+start_app() {
+ log_info "启动后端应用(本地开发模式)..."
+
+ cd "$PROJECT_ROOT"
+
+ # 检查 Go 是否安装
+ if ! command -v go &> /dev/null; then
+ log_error "Go 未安装"
+ return 1
+ fi
+
+ # 加载环境变量(使用 set -a 确保所有变量都被导出)
+ if [ -f ".env" ]; then
+ log_info "加载 .env 文件..."
+ set -a
+ source .env
+ set +a
+ else
+ log_error ".env 文件不存在,请先创建配置文件"
+ return 1
+ fi
+
+ # 设置本地开发环境变量(覆盖 Docker 容器地址)
+ export DB_HOST=localhost
+ export DOCREADER_ADDR=localhost:50051
+ export MINIO_ENDPOINT=localhost:9000
+ export REDIS_ADDR=localhost:6379
+ export OTEL_EXPORTER_OTLP_ENDPOINT=localhost:4317
+ export NEO4J_URI=bolt://localhost:7687
+
+ # 确保必要的环境变量已设置
+ if [ -z "$DB_DRIVER" ]; then
+ log_error "DB_DRIVER 环境变量未设置,请检查 .env 文件"
+ return 1
+ fi
+
+ log_info "环境变量已设置,启动应用..."
+ log_info "数据库地址: $DB_HOST:${DB_PORT:-5432}"
+
+ # 检查是否安装了 Air(热重载工具)
+ if command -v air &> /dev/null; then
+ log_success "检测到 Air,使用热重载模式启动..."
+ log_info "修改 Go 代码后将自动重新编译和重启"
+ air
+ else
+ log_info "未检测到 Air,使用普通模式启动"
+ log_warning "提示: 安装 Air 可以实现代码修改后自动重启"
+ log_info "安装命令: go install github.com/cosmtrek/air@latest"
+ # 运行应用
+ go run cmd/server/main.go
+ fi
+}
+
+# 启动前端(本地)
+start_frontend() {
+ log_info "启动前端开发服务器..."
+
+ cd "$PROJECT_ROOT/frontend"
+
+ # 检查 npm 是否安装
+ if ! command -v npm &> /dev/null; then
+ log_error "npm 未安装"
+ return 1
+ fi
+
+ # 检查依赖是否已安装
+ if [ ! -d "node_modules" ]; then
+ log_warning "node_modules 不存在,正在安装依赖..."
+ npm install
+ fi
+
+ log_info "启动 Vite 开发服务器..."
+ log_info "前端将运行在 http://localhost:5173"
+
+ # 运行开发服务器
+ npm run dev
+}
+
+# 解析命令
+case "${1:-help}" in
+ start)
+ start_services
+ ;;
+ stop)
+ stop_services
+ ;;
+ restart)
+ restart_services
+ ;;
+ logs)
+ show_logs
+ ;;
+ status)
+ show_status
+ ;;
+ app)
+ start_app
+ ;;
+ frontend)
+ start_frontend
+ ;;
+ help|--help|-h)
+ show_help
+ ;;
+ *)
+ log_error "未知命令: $1"
+ show_help
+ exit 1
+ ;;
+esac
+
+exit 0
+
diff --git a/scripts/quick-dev.sh b/scripts/quick-dev.sh
new file mode 100755
index 00000000..93aebd3c
--- /dev/null
+++ b/scripts/quick-dev.sh
@@ -0,0 +1,124 @@
+#!/bin/bash
+# 快速启动开发环境的一键脚本
+# 此脚本会在一个终端中启动所有必需的服务
+
+# 设置颜色
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+RED='\033[0;31m'
+BLUE='\033[0;34m'
+NC='\033[0m' # 无颜色
+
+# 获取项目根目录
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+PROJECT_ROOT="$( cd "$SCRIPT_DIR/.." && pwd )"
+
+log_info() {
+ printf "%b\n" "${BLUE}[INFO]${NC} $1"
+}
+
+log_success() {
+ printf "%b\n" "${GREEN}[SUCCESS]${NC} $1"
+}
+
+log_error() {
+ printf "%b\n" "${RED}[ERROR]${NC} $1"
+}
+
+log_warning() {
+ printf "%b\n" "${YELLOW}[WARNING]${NC} $1"
+}
+
+echo ""
+printf "%b\n" "${GREEN}========================================${NC}"
+printf "%b\n" "${GREEN} WeKnora 快速开发环境启动${NC}"
+printf "%b\n" "${GREEN}========================================${NC}"
+echo ""
+
+# 检查是否在项目根目录
+cd "$PROJECT_ROOT"
+
+# 1. 启动基础设施
+log_info "步骤 1/3: 启动基础设施服务..."
+./scripts/dev.sh start
+if [ $? -ne 0 ]; then
+ log_error "基础设施启动失败"
+ exit 1
+fi
+
+# 等待服务就绪
+log_info "等待服务启动完成..."
+sleep 5
+
+# 2. 询问是否启动后端
+echo ""
+log_info "步骤 2/3: 启动后端应用"
+printf "%b" "${YELLOW}是否在当前终端启动后端? (y/N): ${NC}"
+read -r start_backend
+
+if [ "$start_backend" = "y" ] || [ "$start_backend" = "Y" ]; then
+ log_info "启动后端..."
+ # 在后台启动后端
+ nohup bash -c 'cd "'$PROJECT_ROOT'" && ./scripts/dev.sh app' > "$PROJECT_ROOT/logs/backend.log" 2>&1 &
+ BACKEND_PID=$!
+ echo $BACKEND_PID > "$PROJECT_ROOT/tmp/backend.pid"
+ log_success "后端已在后台启动 (PID: $BACKEND_PID)"
+ log_info "查看后端日志: tail -f $PROJECT_ROOT/logs/backend.log"
+else
+ log_warning "跳过后端启动"
+ log_info "稍后在新终端运行: make dev-app 或 ./scripts/dev.sh app"
+fi
+
+# 3. 询问是否启动前端
+echo ""
+log_info "步骤 3/3: 启动前端应用"
+printf "%b" "${YELLOW}是否在当前终端启动前端? (y/N): ${NC}"
+read -r start_frontend
+
+if [ "$start_frontend" = "y" ] || [ "$start_frontend" = "Y" ]; then
+ log_info "启动前端..."
+ # 在后台启动前端
+ nohup bash -c 'cd "'$PROJECT_ROOT'/frontend" && npm run dev' > "$PROJECT_ROOT/logs/frontend.log" 2>&1 &
+ FRONTEND_PID=$!
+ echo $FRONTEND_PID > "$PROJECT_ROOT/tmp/frontend.pid"
+ log_success "前端已在后台启动 (PID: $FRONTEND_PID)"
+ log_info "查看前端日志: tail -f $PROJECT_ROOT/logs/frontend.log"
+else
+ log_warning "跳过前端启动"
+ log_info "稍后在新终端运行: make dev-frontend 或 ./scripts/dev.sh frontend"
+fi
+
+# 显示总结
+echo ""
+printf "%b\n" "${GREEN}========================================${NC}"
+printf "%b\n" "${GREEN} 启动完成!${NC}"
+printf "%b\n" "${GREEN}========================================${NC}"
+echo ""
+
+log_info "访问地址:"
+echo " - 前端: http://localhost:5173"
+echo " - 后端 API: http://localhost:8080"
+echo " - MinIO Console: http://localhost:9001"
+echo " - Jaeger UI: http://localhost:16686"
+echo ""
+
+log_info "管理命令:"
+echo " - 查看服务状态: make dev-status"
+echo " - 查看日志: make dev-logs"
+echo " - 停止所有服务: make dev-stop"
+echo ""
+
+if [ -f "$PROJECT_ROOT/tmp/backend.pid" ] || [ -f "$PROJECT_ROOT/tmp/frontend.pid" ]; then
+ log_warning "停止后台进程:"
+ if [ -f "$PROJECT_ROOT/tmp/backend.pid" ]; then
+ echo " - 停止后端: kill \$(cat $PROJECT_ROOT/tmp/backend.pid)"
+ fi
+ if [ -f "$PROJECT_ROOT/tmp/frontend.pid" ]; then
+ echo " - 停止前端: kill \$(cat $PROJECT_ROOT/tmp/frontend.pid)"
+ fi
+fi
+
+echo ""
+log_success "开发环境已就绪,开始编码吧!"
+echo ""
+