diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6efd355 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,48 @@ +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# Git +.git/ +.gitignore +.gitattributes + +# Node +web/node_modules/ +web/dist/ +web/electron/ +web/.nuxt/ +web/.output/ + +# Python +app/__pycache__/ +app/src/__pycache__/ +app/.venv/ +app/.uv/ +**/*.pyc +**/*.pyo +**/__pycache__/ +**/.pytest_cache/ +**/.ruff_cache/ + +# Docker +docker/data/ +docker/postgresql-16/data/ +docker/redis/data/ +docker/minio/data/ + +# Docs +docs/ +/*.md +*.drawio + +# Tests +app/tests/ +web/src/**/*.test.ts +web/src/**/*.spec.ts + +# Misc +.claude/ +.superpowers/ diff --git a/.gitignore b/.gitignore index d3abd03..799b46b 100644 --- a/.gitignore +++ b/.gitignore @@ -38,4 +38,4 @@ app/artifacts/ .playwright* .snow/ -/node_modules/ \ No newline at end of file +/node_modules/docker/.env.production diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..b0f7091 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,113 @@ +#!/bin/bash +# FileFlash 一键部署脚本 +# 用法: 将整个项目上传到服务器后,在项目根目录执行 bash deploy.sh +# bash deploy.sh --fresh # 强制全量重建镜像(首次部署或大版本更新) + +set -e + +echo "========================================" +echo " FileFlash 生产环境部署" +echo "========================================" + +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m' + +# ---------- 1. 检查环境 ---------- +echo -e "\n${YELLOW}[1/5] 检查环境...${NC}" + +if ! command -v docker &>/dev/null; then + echo -e "${RED}未安装 Docker,请先安装${NC}" + exit 1 +fi + +if ! docker compose version &>/dev/null; then + echo -e "${RED}需要 Docker Compose v2+${NC}" + exit 1 +fi + +# ---------- 2. 生成密钥 ---------- +echo -e "\n${YELLOW}[2/5] 生成安全密钥...${NC}" + +JWT_SECRET=$(openssl rand -hex 32 2>/dev/null || python3 -c "import secrets;print(secrets.token_hex(32))" 2>/dev/null) +ADMIN_PASSWORD=$(openssl rand -hex 32 2>/dev/null || python3 -c "import secrets;print(secrets.token_hex(32))" 2>/dev/null) +DB_PASSWORD=$(openssl rand -hex 16 2>/dev/null || python3 -c "import secrets;print(secrets.token_hex(16))" 2>/dev/null) +MINIO_PASSWORD=$(openssl rand -hex 16 2>/dev/null || python3 -c "import secrets;print(secrets.token_hex(16))" 2>/dev/null) + +if [ -z "$JWT_SECRET" ] || [ -z "$ADMIN_PASSWORD" ] || [ -z "$DB_PASSWORD" ] || [ -z "$MINIO_PASSWORD" ]; then + echo -e "${RED}无法生成密钥,请手动修改 docker/.env.production${NC}" + exit 1 +fi + +# 替换 .env.production +sed -i "s|^JWT_SECRET_KEY=.*|JWT_SECRET_KEY=$JWT_SECRET|" docker/.env.production +sed -i "s|^DEFAULT_ADMIN_PASSWORD=.*|DEFAULT_ADMIN_PASSWORD=$ADMIN_PASSWORD|" docker/.env.production +sed -i "s|^OBJECT_STORAGE_SECRET_KEY=.*|OBJECT_STORAGE_SECRET_KEY=$MINIO_PASSWORD|" docker/.env.production +sed -i "s|:psgl-ff-db@|:$DB_PASSWORD@|" docker/.env.production + +# 替换 docker-compose.prod.yml +sed -i "s|POSTGRES_PASSWORD: psgl-ff-db|POSTGRES_PASSWORD: $DB_PASSWORD|" docker/docker-compose.prod.yml +sed -i "s|MINIO_ROOT_PASSWORD: minio-admin|MINIO_ROOT_PASSWORD: $MINIO_PASSWORD|" docker/docker-compose.prod.yml +sed -i "s|-password=psgl-ff-db|-password=$DB_PASSWORD|" docker/docker-compose.prod.yml + +echo -e "${GREEN}JWT_SECRET_KEY 已生成${NC}" +echo -e "${GREEN}DEFAULT_ADMIN_PASSWORD 已生成${NC}" +echo -e "${GREEN}数据库密码 已生成${NC}" +echo -e "${GREEN}MinIO 密码 已生成${NC}" + +# ---------- 3. 构建镜像 ---------- +echo -e "\n${YELLOW}[3/5] 构建 Docker 镜像...${NC}" + +BUILD_FLAGS="" +if [ "$1" = "--fresh" ]; then + BUILD_FLAGS="--no-cache" + echo -e "${YELLOW} 首次部署模式: 强制全量构建${NC}" +else + echo " 增量构建 (使用层缓存,加 --fresh 可强制全量)" +fi + +docker compose -f docker/docker-compose.prod.yml build $BUILD_FLAGS + +echo -e "${GREEN}镜像构建完成${NC}" + +# ---------- 4. 启动服务 ---------- +echo -e "\n${YELLOW}[4/5] 启动服务...${NC}" + +docker compose -f docker/docker-compose.prod.yml up -d + +# ---------- 5. 等待就绪 ---------- +echo -e "\n${YELLOW}[5/5] 等待服务就绪...${NC}" + +echo -n "等待服务启动" +for i in $(seq 1 60); do + if curl -s -o /dev/null http://localhost/health 2>/dev/null; then + echo "" + echo -e "${GREEN}服务已就绪${NC}" + break + fi + echo -n "." + sleep 3 +done + +echo "" +echo "========================================" +echo -e "${GREEN} 部署完成!${NC}" +echo "========================================" +echo "" +echo "管理员账号:" +echo " 用户名: administrator" +echo " 密码: $ADMIN_PASSWORD" +echo "" +echo "数据库密码: $DB_PASSWORD" +echo "MinIO 密码: $MINIO_PASSWORD" +echo "" +echo "访问 http://$(hostname -I | awk '{print $1}')" +echo "" +echo "常用命令:" +echo " docker compose -f docker/docker-compose.prod.yml logs -f # 查看日志" +echo " docker compose -f docker/docker-compose.prod.yml down # 停止" +echo " docker compose -f docker/docker-compose.prod.yml restart # 重启" +echo " docker compose -f docker/docker-compose.prod.yml ps # 状态" +echo "" +echo "如需开启 AI Agent:" +echo " 1. 修改 docker/.env.production 中 AGENT_ENABLED=true" +echo " 2. 配置 AGENT_LLM_BASE_URL 和 AGENT_LLM_API_KEY" +echo " 3. docker compose -f docker/docker-compose.prod.yml --profile agent up -d" diff --git a/docker/Dockerfile.backend b/docker/Dockerfile.backend new file mode 100644 index 0000000..5a82bdf --- /dev/null +++ b/docker/Dockerfile.backend @@ -0,0 +1,29 @@ +FROM docker.m.daocloud.io/library/python:3.12-slim + +WORKDIR /app + +# 换成阿里云 debian 镜像源 +RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list.d/debian.sources + +# 再安装 ffmpeg +RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/* + +# pip 走阿里云 +RUN pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/ +RUN pip install --no-cache-dir uv + +# uv 走阿里云 +ENV UV_INDEX_URL=https://mirrors.aliyun.com/pypi/simple/ + +# Layer 1: 仅依赖(除非 pyproject.toml / uv.lock 变动,否则缓存命中) +COPY app/pyproject.toml app/uv.lock ./ +RUN uv sync --no-dev --frozen --no-install-project + +# Layer 2: 源码 + 安装项目本身 +COPY app/src ./src +COPY app/README.md ./README.md +RUN uv sync --no-dev --frozen + +EXPOSE 8080 + +CMD ["uv", "run", "uvicorn", "fileflash.main:app", "--host", "0.0.0.0", "--port", "8080"] diff --git a/docker/Dockerfile.frontend b/docker/Dockerfile.frontend new file mode 100644 index 0000000..4eec7cf --- /dev/null +++ b/docker/Dockerfile.frontend @@ -0,0 +1,35 @@ +FROM docker.m.daocloud.io/library/node:22-alpine AS builder + +WORKDIR /build + +RUN npm config set registry https://registry.npmmirror.com +ENV BUN_CONFIG_REGISTRY=https://registry.npmmirror.com + +RUN npm install -g bun + +COPY web/package.json web/bun.lock* ./ + +# Docker 里只需要打包 Vue,不需要 Electron 二进制 +ENV ELECTRON_SKIP_BINARY_DOWNLOAD=1 + +RUN bun install --frozen-lockfile +COPY web ./ + +ARG VITE_BASE_URL=/api/v1 +ENV VITE_BASE_URL=$VITE_BASE_URL + +ARG VITE_ENABLE_MOCKS=false +ENV VITE_ENABLE_MOCKS=$VITE_ENABLE_MOCKS + +# 跳过 vue-tsc 类型检查(CI 已做),仅生产构建 +RUN bun x vite build + +# Stage 2: Serve static files with nginx +FROM docker.m.daocloud.io/library/nginx:1.27-alpine + +COPY --from=builder /build/dist /usr/share/nginx/html +COPY docker/nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml new file mode 100644 index 0000000..8b60388 --- /dev/null +++ b/docker/docker-compose.prod.yml @@ -0,0 +1,159 @@ +services: + # ========== PostgreSQL + Flyway 迁移 ========== + postgres: + image: docker.m.daocloud.io/library/postgres:16 + container_name: ff-postgres + restart: always + environment: + POSTGRES_USER: admin + POSTGRES_PASSWORD: psgl-ff-db + POSTGRES_DB: fileflash + TZ: Asia/Shanghai + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U admin -d fileflash"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - fileflash + + flyway: + image: docker.m.daocloud.io/flyway/flyway:latest + container_name: ff-flyway + command: > + -url=jdbc:postgresql://postgres:5432/fileflash + -user=admin + -password=psgl-ff-db + -connectRetries=15 + migrate + volumes: + - ./flyway/migrations:/flyway/sql + depends_on: + postgres: + condition: service_healthy + networks: + - fileflash + + # ========== Redis ========== + redis: + image: docker.m.daocloud.io/library/redis:7-alpine + container_name: ff-redis + restart: always + command: redis-server --appendonly yes + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - fileflash + + # ========== MinIO 对象存储 ========== + minio: + image: quay.io/minio/minio:latest + container_name: ff-minio + restart: always + environment: + MINIO_ROOT_USER: admin + MINIO_ROOT_PASSWORD: minio-admin + volumes: + - minio_data:/data + command: server /data + healthcheck: + test: ["CMD", "curl" ,"-f","http://localhost:9000/minio/health/live"] + interval: 30s + timeout: 20s + retries: 3 + networks: + - fileflash + + # ========== FastAPI 后端 ========== + backend: + build: + context: .. + dockerfile: docker/Dockerfile.backend + container_name: ff-backend + restart: always + env_file: + - .env.production + depends_on: + flyway: + condition: service_completed_successfully + redis: + condition: service_healthy + minio: + condition: service_healthy + networks: + - fileflash + + # ========== Worker 后台任务(转码/扫描等) ========== + worker: + build: + context: .. + dockerfile: docker/Dockerfile.backend + container_name: ff-worker + restart: always + env_file: + - .env.production + command: ["uv", "run", "python", "-m", "fileflash.workers.consumer"] + depends_on: + flyway: + condition: service_completed_successfully + redis: + condition: service_healthy + minio: + condition: service_healthy + networks: + - fileflash + + # ========== Agent Worker (开启 AGENT_ENABLED=true 时需要) ========== + agent-worker: + build: + context: .. + dockerfile: docker/Dockerfile.backend + container_name: ff-agent-worker + restart: always + env_file: + - .env.production + command: ["uv", "run", "python", "-m", "fileflash.agents.worker"] + depends_on: + flyway: + condition: service_completed_successfully + redis: + condition: service_healthy + minio: + condition: service_healthy + networks: + - fileflash + profiles: + - agent + + # ========== Vue 前端 + nginx ========== + frontend: + build: + context: .. + dockerfile: docker/Dockerfile.frontend + args: + VITE_BASE_URL: /api/v1 + VITE_ENABLE_MOCKS: 'false' + container_name: ff-frontend + restart: always + ports: + - "80:80" + depends_on: + - backend + networks: + - fileflash + +volumes: + postgres_data: + redis_data: + minio_data: + +networks: + fileflash: + driver: bridge diff --git a/docker/nginx.conf b/docker/nginx.conf new file mode 100644 index 0000000..2cbd2d5 --- /dev/null +++ b/docker/nginx.conf @@ -0,0 +1,39 @@ +server { + listen 80; + server_name _; + + # Gzip compression + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml; + + # Frontend SPA + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } + + # Proxy API requests to backend + location /api/ { + proxy_pass http://backend:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # Upload support + client_max_body_size 5G; + proxy_read_timeout 900s; + proxy_send_timeout 900s; + + # Disable buffering for streaming (video/audio) + proxy_buffering off; + proxy_request_buffering off; + } + + # Health check + location /health { + proxy_pass http://backend:8080/health; + } +} diff --git a/web/src/components/molecules/Select.vue b/web/src/components/molecules/Select.vue index 324e53b..282285b 100644 --- a/web/src/components/molecules/Select.vue +++ b/web/src/components/molecules/Select.vue @@ -1,218 +1,170 @@ - - - - - - - {{ selectedLabel || placeholder }} - - - - - - {{ opt.label }} - - - - - - + + + + + + + {{ selectedLabel || placeholder }} + + + + + + {{ opt.label }} + + + + + +