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 @@
- - - + -
- - {{ model.name }} - {{ formatModelSize(model.size) }} -
-
+ + +
+ + {{ model.name }} + {{ formatModelSize(model.size) }} +
+
+ + + +
+ + 下载: {{ searchKeyword }} +
+
+ + + +
- - + -
- - 下载: {{ searchKeyword }} -
-
- - - - - - - - - 刷新列表 - + + 刷新列表 + +
@@ -153,12 +155,31 @@
- +
+ + + + + 检测维度 + +
+

+ {{ dimensionMessage }} +

@@ -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 @@

Agent 配置

配置 AI Agent 的默认行为和参数,这些设置将应用于所有启用 Agent 模式的对话

+ + +
+
+ +
+
+
+ + + + {{ isAgentReady ? '可用' : '未就绪' }} + +
+ + {{ agentStatusMessage }} + +

+ + 配置完成后,Agent 状态将自动变为"可用",此时可在对话界面开启 Agent 模式 +

+
+
- -
-
- -

启用后,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 @@
+ +
+
+
+

下载新模型

+

+ 输入模型名称下载, + + 浏览 Ollama 模型库 + + +

+
+
+ +
+
+ + + 下载 + +
+ +
+
+ 正在下载: {{ downloadModelName }} + {{ downloadProgress.toFixed(2) }}% +
+ +
+
+
+ -
+

已下载的模型

@@ -102,59 +164,6 @@

暂无已下载的模型

- - -
-
-
-

下载新模型

-

输入模型名称下载,或点击推荐模型快速下载

-
-
- -
-
- - - 下载 - -
- -
-
- 正在下载: {{ downloadModelName }} - {{ downloadProgress.toFixed(2) }}% -
- -
- - -
-
@@ -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 "" +