From 93bac360f1ab63cf0d9d9b7bbee882b0f97a459b Mon Sep 17 00:00:00 2001 From: lintsinghua <1930438860@qq.com> Date: Thu, 23 Oct 2025 00:13:48 +0800 Subject: [PATCH] feat(deployment): Add Docker support and deployment configuration - Add Dockerfile for multi-stage build with Node.js and Nginx - Create docker-compose.yml for simplified container deployment - Add .dockerignore to optimize Docker build context - Configure nginx.conf with performance and security optimizations - Update README.md and README_EN.md with Docker deployment instructions - Enhance database configuration to support optional Supabase integration - Improve environment configuration handling for local and containerized environments Enables easier deployment and provides a standardized container-based setup for the application. --- .dockerignore | 46 ++++++++++++++++++++ Dockerfile | 46 ++++++++++++++++++++ README.md | 36 ++++++++++++++-- README_EN.md | 44 +++++++++++++++++-- docker-compose.yml | 40 +++++++++++++++++ nginx.conf | 38 ++++++++++++++++ src/shared/config/database.ts | 81 ++++++++++++++++++++++++++++++++++- 7 files changed, 322 insertions(+), 9 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 nginx.conf diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..fad52b9 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,46 @@ +# 依赖 +node_modules + +# 构建产物 +dist +.vite + +# 环境变量(保留 .env 用于构建) +.env.local +.env.*.local + +# 日志 +logs +*.log +npm-debug.log* +pnpm-debug.log* +yarn-debug.log* +yarn-error.log* + +# 编辑器 +.vscode +.idea +*.swp +*.swo +*~ + +# 操作系统 +.DS_Store +Thumbs.db + +# Git +.git +.gitignore +.gitattributes + +# 测试 +coverage +.nyc_output + +# 文档 +*.md +docs + +# 其他 +.history +.cache diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..02e23aa --- /dev/null +++ b/Dockerfile @@ -0,0 +1,46 @@ +# 多阶段构建 - 构建阶段 +FROM node:18-alpine AS builder + +# 设置工作目录 +WORKDIR /app + +# 禁用代理并安装 pnpm +ENV HTTP_PROXY="" +ENV HTTPS_PROXY="" +ENV http_proxy="" +ENV https_proxy="" +ENV NO_PROXY="*" +ENV no_proxy="*" + +RUN npm config set registry https://registry.npmjs.org/ && \ + npm config delete proxy 2>/dev/null || true && \ + npm config delete https-proxy 2>/dev/null || true && \ + npm config delete http-proxy 2>/dev/null || true && \ + npm install -g pnpm + +# 复制依赖文件 +COPY package.json pnpm-lock.yaml ./ + +# 安装依赖 +RUN pnpm install --no-frozen-lockfile + +# 复制项目文件(包括 .env) +COPY . . + +# 构建应用(环境变量会在构建时被读取) +RUN pnpm build + +# 生产阶段 - 使用 nginx 提供静态文件服务 +FROM nginx:alpine + +# 复制自定义 nginx 配置 +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# 从构建阶段复制构建产物 +COPY --from=builder /app/dist /usr/share/nginx/html + +# 暴露端口 +EXPOSE 80 + +# 启动 nginx +CMD ["nginx", "-g", "daemon off;"] diff --git a/README.md b/README.md index 628bc3a..8d24d7f 100644 --- a/README.md +++ b/README.md @@ -49,14 +49,43 @@ ## 🚀 快速开始 -### 环境要求 +### 🐳 Docker 部署(推荐) + +使用 Docker 可以快速部署应用,无需配置 Node.js 环境。 + +1. **克隆项目** + ```bash + git clone https://github.com/lintsinghua/XCodeReviewer.git + cd XCodeReviewer + ``` + +2. **配置环境变量** + ```bash + cp .env.example .env + # 编辑 .env 文件,至少需要配置 VITE_GEMINI_API_KEY + ``` + +3. **构建并启动** + ```bash + docker-compose build + docker-compose up -d + ``` + +4. **访问应用** + + 在浏览器中打开 `http://localhost:5174` + +### 💻 本地开发部署 + +如果需要进行开发或自定义修改,可以使用本地部署方式。 + +#### 环境要求 - **Node.js**: `18+` - **pnpm**: `8+` (推荐) 或 `npm` / `yarn` - **Google Gemini API Key**: 用于 AI 代码分析 -- **Supabase 项目**: 用于数据存储(可选,支持离线模式) -### 安装与启动 +#### 安装与启动 1. **克隆项目** ```bash @@ -256,6 +285,7 @@ XCodeReviewer/ 5. 查看详细的问题报告 ### 构建和部署 + ```bash # 开发模式 pnpm dev diff --git a/README_EN.md b/README_EN.md index b962630..e994b7f 100644 --- a/README_EN.md +++ b/README_EN.md @@ -49,14 +49,50 @@ In the fast-paced world of software development, ensuring code quality is crucia ## 🚀 Quick Start -### Requirements +### 🐳 Docker Deployment (Recommended) + +Deploy quickly using Docker without Node.js environment setup. + +1. **Clone the project** + ```bash + git clone https://github.com/lintsinghua/XCodeReviewer.git + cd XCodeReviewer + ``` + +2. **Configure environment variables** + ```bash + cp .env.example .env + # Edit .env file and set at least VITE_GEMINI_API_KEY + ``` + +3. **Build and start** + ```bash + docker-compose build + docker-compose up -d + ``` + +4. **Access the application** + + Open `http://localhost:5174` in your browser + +**Common commands:** +```bash +docker-compose logs -f # View logs +docker-compose restart # Restart service +docker-compose down # Stop service +``` + +### 💻 Local Development Deployment + +For development or custom modifications, use local deployment. + +#### Requirements - **Node.js**: `18+` - **pnpm**: `8+` (recommended) or `npm` / `yarn` - **Google Gemini API Key**: For AI code analysis -- **Supabase Project**: For data storage (optional, supports offline mode) -### Installation & Setup +#### Installation & Setup 1. **Clone the project** ```bash @@ -111,7 +147,7 @@ In the fast-paced world of software development, ensuring code quality is crucia ``` 5. **Access the application** - Open `http://localhost:5173` in your browser + Open `http://localhost:5174` in your browser ### 🔑 Getting API Keys diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..fd803b8 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,40 @@ +services: + # XCodeReviewer 前端应用 + xcodereviewer: + build: + context: . + dockerfile: Dockerfile + container_name: xcodereviewer-app + ports: + - "5174:80" + environment: + # Google Gemini AI 配置 (必需) + - VITE_GEMINI_API_KEY=${VITE_GEMINI_API_KEY} + - VITE_GEMINI_MODEL=${VITE_GEMINI_MODEL:-gemini-2.5-flash} + - VITE_GEMINI_TIMEOUT_MS=${VITE_GEMINI_TIMEOUT_MS:-25000} + + # Supabase 配置 (可选) + - VITE_SUPABASE_URL=${VITE_SUPABASE_URL} + - VITE_SUPABASE_ANON_KEY=${VITE_SUPABASE_ANON_KEY} + + # GitHub 集成 (可选) + - VITE_GITHUB_TOKEN=${VITE_GITHUB_TOKEN} + + # 应用配置 + - VITE_APP_ID=${VITE_APP_ID:-xcodereviewer} + - VITE_MAX_ANALYZE_FILES=${VITE_MAX_ANALYZE_FILES:-40} + - VITE_LLM_CONCURRENCY=${VITE_LLM_CONCURRENCY:-2} + - VITE_LLM_GAP_MS=${VITE_LLM_GAP_MS:-500} + restart: unless-stopped + networks: + - xcodereviewer-network + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:80"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + +networks: + xcodereviewer-network: + driver: bridge diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..c116f5f --- /dev/null +++ b/nginx.conf @@ -0,0 +1,38 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + # Gzip 压缩配置 + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/json application/javascript; + + # 安全头部 + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + + # 处理 React Router 的客户端路由 + location / { + try_files $uri $uri/ /index.html; + } + + # 静态资源缓存 + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # API 代理(如果需要) + # location /api { + # proxy_pass http://backend:8000; + # proxy_http_version 1.1; + # proxy_set_header Upgrade $http_upgrade; + # proxy_set_header Connection 'upgrade'; + # proxy_set_header Host $host; + # proxy_cache_bypass $http_upgrade; + # } +} diff --git a/src/shared/config/database.ts b/src/shared/config/database.ts index 7a66e84..51a09e6 100644 --- a/src/shared/config/database.ts +++ b/src/shared/config/database.ts @@ -19,19 +19,49 @@ const isValidUuid = (value?: string): boolean => { return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value); }; -export const supabase = createClient(supabaseUrl, supabaseAnonKey, { +// 检查是否配置了 Supabase +const hasSupabaseConfig = supabaseUrl && supabaseAnonKey; + +// 如果没有配置 Supabase,使用虚拟配置避免错误 +const finalSupabaseUrl = hasSupabaseConfig ? supabaseUrl : 'https://demo.supabase.co'; +const finalSupabaseKey = hasSupabaseConfig ? supabaseAnonKey : 'demo-key'; + +export const supabase = hasSupabaseConfig ? createClient(finalSupabaseUrl, finalSupabaseKey, { global: { fetch: undefined }, auth: { storageKey: (import.meta.env.VITE_APP_ID || "sb") + "-auth-token" } -}); +}) : null; + +// 演示模式标识 +export const isDemoMode = !hasSupabaseConfig; + +// 演示数据 +const demoProfile: Profile = { + id: 'demo-user', + phone: null, + email: 'demo@xcodereviewer.com', + full_name: 'Demo User', + avatar_url: null, + role: 'admin', + github_username: 'demo-user', + gitlab_username: null, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString() +}; // 用户相关API export const api = { // Profile相关 async getProfilesById(id: string): Promise { + if (isDemoMode) { + return demoProfile; + } + + if (!supabase) return null; + const { data, error } = await supabase .from('profiles') .select('*') @@ -43,6 +73,12 @@ export const api = { }, async getProfilesCount(): Promise { + if (isDemoMode) { + return 1; + } + + if (!supabase) return 0; + const { count, error } = await supabase .from('profiles') .select('*', { count: 'exact', head: true }); @@ -95,6 +131,25 @@ export const api = { // Project相关 async getProjects(): Promise { + if (isDemoMode) { + return [{ + id: 'demo-project-1', + name: 'Demo Project', + description: '这是一个演示项目,展示 XCodeReviewer 的功能', + repository_url: 'https://github.com/demo/project', + repository_type: 'github', + default_branch: 'main', + programming_languages: JSON.stringify(['TypeScript', 'JavaScript', 'React']), + owner_id: 'demo-user', + owner: demoProfile, + is_active: true, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString() + }]; + } + + if (!supabase) return []; + const { data, error } = await supabase .from('projects') .select(` @@ -382,6 +437,28 @@ export const api = { // 统计相关 async getProjectStats(): Promise { + if (isDemoMode) { + return { + total_projects: 1, + active_projects: 1, + total_tasks: 3, + completed_tasks: 2, + total_issues: 15, + resolved_issues: 12 + }; + } + + if (!supabase) { + return { + total_projects: 0, + active_projects: 0, + total_tasks: 0, + completed_tasks: 0, + total_issues: 0, + resolved_issues: 0 + }; + } + const [projectsResult, tasksResult, issuesResult] = await Promise.all([ supabase.from('projects').select('id, is_active', { count: 'exact' }), supabase.from('audit_tasks').select('id, status', { count: 'exact' }),