fully commit

This commit is contained in:
root
2025-11-21 23:18:13 +08:00
parent 39ae577965
commit 1fb1d92b4e
18 changed files with 1146 additions and 86 deletions
+24
View File
@@ -0,0 +1,24 @@
# OmniVerse Production Environment Configuration
# Application
NODE_ENV=production
# News Crawler Service
PORT=3001
API_KEY=6b212515bedb521bc700e125a8d43ae53907a5160296abdab2064aaa3ed86526
ENABLE_SCHEDULER=true
CRAWL_INTERVAL_MINUTES=30
MAX_NEWS_AGE_DAYS=7
# Database
DATABASE_PATH=./data/news.db
# Logging
LOG_LEVEL=info
LOG_FILE=./logs/app.log
# Cache
CACHE_TTL_SECONDS=300
# CORS (允许的前端域名)
CORS_ORIGIN=https://www.omniverseai.net,https://omniverseai.net,http://localhost:3000
+24
View File
@@ -0,0 +1,24 @@
# OmniVerse Production Environment Configuration
# Application
NODE_ENV=production
# News Crawler Service
PORT=3001
API_KEY=your-secure-api-key-here-change-this
ENABLE_SCHEDULER=true
CRAWL_INTERVAL_MINUTES=30
MAX_NEWS_AGE_DAYS=7
# Database
DATABASE_PATH=./data/news.db
# Logging
LOG_LEVEL=info
LOG_FILE=./logs/app.log
# Cache
CACHE_TTL_SECONDS=300
# CORS (允许的前端域名)
CORS_ORIGIN=https://www.omniverseai.net,https://omniverseai.net,http://localhost:3000
+96
View File
@@ -0,0 +1,96 @@
name: Deploy Website to Production
on:
push:
branches:
- main
paths-ignore:
- 'README.md'
- 'docs/**'
- '.gitignore'
workflow_dispatch:
env:
HARBOR_REGISTRY: harbor.omniverseai.net
HARBOR_PROJECT: omniverse
IMAGE_NAME: website
KUBECONFIG: /etc/rancher/k3s/k3s.yaml
jobs:
build-and-deploy:
runs-on: [self-hosted, Linux, X64]
steps:
# ========================================
# 1. Checkout Code
# ========================================
- name: Checkout code
uses: actions/checkout@v4
# ========================================
# 2. Set up Docker Buildx
# ========================================
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# ========================================
# 3. Login to Harbor
# ========================================
- name: Login to Harbor
run: |
echo "${{ secrets.HARBOR_PASSWORD }}" | docker login ${{ env.HARBOR_REGISTRY }} -u ${{ secrets.HARBOR_USERNAME }} --password-stdin
# ========================================
# 4. Build and Push Docker Image
# ========================================
- name: Build and push Docker image
run: |
IMAGE_TAG="${{ env.HARBOR_REGISTRY }}/${{ env.HARBOR_PROJECT }}/${{ env.IMAGE_NAME }}:latest"
IMAGE_SHA="${{ env.HARBOR_REGISTRY }}/${{ env.HARBOR_PROJECT }}/${{ env.IMAGE_NAME }}:${{ github.sha }}"
docker build -f Dockerfile.prod -t $IMAGE_TAG -t $IMAGE_SHA .
docker push $IMAGE_TAG
docker push $IMAGE_SHA
echo "Built and pushed: $IMAGE_TAG"
echo "Built and pushed: $IMAGE_SHA"
# ========================================
# 5. Deploy to k3s
# ========================================
- name: Deploy to k3s
env:
KUBECONFIG: ${{ env.KUBECONFIG }}
run: |
# Apply Kubernetes manifests
kubectl apply -f k8s/production.yaml
# Force pull latest image and restart
kubectl set image deployment/website website=${{ env.HARBOR_REGISTRY }}/${{ env.HARBOR_PROJECT }}/${{ env.IMAGE_NAME }}:${{ github.sha }} -n prod
kubectl rollout restart deployment/website -n prod
# Wait for rollout to complete
kubectl rollout status deployment/website -n prod --timeout=300s
# ========================================
# 6. Verify Deployment
# ========================================
- name: Verify deployment
run: |
echo "==================================="
echo "Website Deployment Summary"
echo "==================================="
echo ""
echo "Image: ${{ env.HARBOR_REGISTRY }}/${{ env.HARBOR_PROJECT }}/${{ env.IMAGE_NAME }}:latest"
echo "Commit: ${{ github.sha }}"
echo "Namespace: prod"
echo ""
echo "Pods:"
kubectl get pods -n prod -l app=website
echo ""
echo "Service:"
kubectl get svc -n prod website-svc
echo ""
echo "Ingress:"
kubectl get ingress -n prod website-ingress
echo "==================================="
+95
View File
@@ -0,0 +1,95 @@
# 网站空白页面问题修复指南
## 问题诊断
您的网站已成功部署在服务器上,但通过 https://www.omniverseai.net 访问时看到空白页面。
经过检查:
- ✅ 服务器正常运行
- ✅ HTML 文件正确返回
- ✅ JS/CSS 资源文件可以正常访问
- ⚠️ **问题原因:Cloudflare CDN 缓存**
## 解决方案
### 方案一:清除 Cloudflare 缓存(推荐)
1. 登录 Cloudflare 控制台:https://dash.cloudflare.com
2. 选择域名 `omniverseai.net`
3. 进入 **"缓存" (Caching)** 菜单
4. 点击 **"清除所有文件" (Purge Everything)**
5. 确认清除
6. 等待 30 秒后,刷新浏览器(按 Ctrl+Shift+R 强制刷新)
### 方案二:临时禁用 Cloudflare 代理
如果清除缓存后仍有问题,可以临时禁用 Cloudflare:
1. 登录 Cloudflare 控制台
2. 进入 **DNS 设置**
3. 找到 `www` 和 `@` 记录
4. 点击橙色云图标,将其变为灰色(DNS Only 模式)
5. 等待 DNS 传播(约 5-10 分钟)
6. 刷新浏览器测试
### 方案三:配置 Cloudflare 缓存规则
为了避免未来出现类似问题:
1. 进入 Cloudflare 的 **"规则" (Rules)** → **"页面规则" (Page Rules)**
2. 创建新规则:
- URL: `www.omniverseai.net/*`
- 设置:
- 缓存级别:Standard
- Browser Cache TTL: 4 hours
- Always Online: ON
3. 保存规则
## 验证步骤
1. 打开浏览器开发者工具(F12)
2. 切换到 "Console" 标签
3. 访问 https://www.omniverseai.net
4. 检查是否有错误信息
5. 切换到 "Network" 标签
6. 确认 JS 和 CSS 文件是否成功加载(状态码 200)
## 本地测试(绕过 Cloudflare)
如果需要直接测试服务器(绕过 Cloudflare):
```bash
# 在您的本地计算机添加 hosts 文件
# Windows: C:\Windows\System32\drivers\etc\hosts
# Mac/Linux: /etc/hosts
154.38.168.199 www.omniverseai.net
```
然后访问 http://www.omniverseai.net(注意是 HTTP)
## 检查 Cloudflare SSL 设置
确保 Cloudflare SSL/TLS 设置正确:
1. 进入 **SSL/TLS** 设置
2. 加密模式应设置为:
- **Flexible**(如果服务器只有 HTTP)
- **Full**(如果服务器有自签名证书)
- **Full (strict)**(如果服务器有有效的 SSL 证书)
**当前服务器配置**:仅 HTTP,因此建议使用 **Flexible** 模式
## 技术细节
- 服务器 IP: 154.38.168.199
- HTTP 端口: 80
- HTTPS 端口: 443(通过 Cloudflare 代理)
- 容器状态: 所有服务正常运行
## 需要帮助?
如果以上方法都不能解决问题,请提供:
1. 浏览器控制台的错误截图
2. Network 标签的资源加载状态
3. 当前的 Cloudflare SSL/TLS 设置模式
+13 -36
View File
@@ -1,64 +1,41 @@
# Node.js 应用 Dockerfile - 自动优化版本
# 由 docker-builder 生成
# OmniVerse Web Application Dockerfile
# Production build for US server
# 构建阶段
# Build stage
FROM node:18-alpine AS builder
# 清除代理配置,避免网络冲突
ENV HTTP_PROXY=""
ENV HTTPS_PROXY=""
ENV http_proxy=""
ENV https_proxy=""
WORKDIR /app
# 使用国内镜像源加速
RUN npm config delete proxy && \
npm config delete https-proxy && \
npm config set registry https://registry.npmmirror.com && \
npm config set cache /tmp/.npm --global
# 复制 package 文件
# Copy package files
COPY package*.json ./
# 安装依赖
# Install dependencies
RUN npm install --frozen-lockfile
# 复制源代码
# Copy source code
COPY . .
# Build the application
RUN npm run build -- --base=/
RUN npm run build
# 生产阶段
# Production stage
FROM node:18-alpine AS production
# 清除代理配置
ENV HTTP_PROXY=""
ENV HTTPS_PROXY=""
ENV http_proxy=""
ENV https_proxy=""
WORKDIR /app
# 配置 npm 使用国内镜像源
RUN npm config delete proxy && \
npm config delete https-proxy && \
npm config set registry https://registry.npmmirror.com
# 只安装运行时工具
# Install serve for static file serving
RUN npm install -g serve
# 复制构建产物
# Copy build artifacts
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./
# 暴露端口
# Expose port
EXPOSE 3000
# 健康检查
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000 || exit 1
# 启动应用
# Start application
CMD ["serve", "-s", "dist", "-l", "3000"]
+40
View File
@@ -0,0 +1,40 @@
# ============================================================================
# OmniVerse Website Production Dockerfile
# Multi-stage build for optimized image size
# ============================================================================
# Stage 1: Build
FROM node:18-alpine AS builder
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production --ignore-scripts || npm install
# Copy source code
COPY . .
# Build application
RUN npm run build
# Stage 2: Production
FROM nginx:1.25-alpine
# Copy built files
COPY --from=builder /app/dist /usr/share/nginx/html
# Copy nginx configuration
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Expose port
EXPOSE 80
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost/health || exit 1
# Start nginx
CMD ["nginx", "-g", "daemon off;"]
+298
View File
@@ -0,0 +1,298 @@
# OmniVerse 官网生产环境部署指南
## 部署架构
```
Internet (80/443)
↓
Nginx Reverse Proxy (Docker)
↓
├─→ Frontend App (Port 3000, Docker)
└─→ News Crawler API (Port 3001, Docker)
```
## 服务组件
1. **Nginx** - 反向代理和SSL终止 (80/443端口)
2. **Web Frontend** - React应用 (内部3000端口)
3. **News Crawler** - 新闻爬虫服务 (内部3001端口)
4. **Certbot** - SSL证书自动续期
## 部署前准备
### 1. 检查域名DNS解析
确保域名已正确解析到服务器:
```bash
nslookup www.omniverseai.net
nslookup omniverseai.net
```
### 2. 检查必要端口
确保以下端口可用:
```bash
# 检查端口占用
netstat -tlnp | grep -E ':(80|443) '
# 如有其他服务占用,需要先停止
```
### 3. 安装Docker和Docker Compose
```bash
# 检查Docker版本
docker --version
docker-compose --version
# 如未安装,请先安装Docker
```
### 4. 配置环境变量
编辑 `.env.production` 文件,修改必要的配置:
```bash
nano .env.production
```
重要配置项:
- `API_KEY`: 修改为强密码
- `CORS_ORIGIN`: 确认域名正确
## 部署步骤
### 第一步:初始部署(HTTP)
首次部署使用HTTP模式,用于SSL证书申请:
```bash
# 运行部署脚本
./deploy-prod.sh
```
部署脚本会:
1. 停止旧容器
2. 构建Docker镜像
3. 启动所有服务
4. 显示服务状态
### 第二步:验证HTTP访问
访问 http://www.omniverseai.net 确认网站可以正常访问。
```bash
# 检查服务状态
docker-compose -f docker-compose.prod.yml ps
# 查看日志
docker-compose -f docker-compose.prod.yml logs -f web
docker-compose -f docker-compose.prod.yml logs -f news-crawler
docker-compose -f docker-compose.prod.yml logs -f nginx
```
### 第三步:申请SSL证书
确认HTTP访问正常后,运行SSL初始化脚本:
```bash
# 申请证书(首次)
./init-ssl.sh
```
### 第四步:启用HTTPS
证书申请成功后,需要更新Nginx配置:
1. 编辑 `nginx/default.conf`:
```bash
nano nginx/default.conf
```
2. 进行以下修改:
- 在HTTP服务器块中,注释掉临时的 `proxy_pass` 配置
- 取消注释 HTTP到HTTPS重定向配置
- 取消注释整个HTTPS服务器配置块
3. 重启Nginx:
```bash
docker-compose -f docker-compose.prod.yml restart nginx
```
4. 验证HTTPS访问:
访问 https://www.omniverseai.net
## 管理命令
### 查看服务状态
```bash
docker-compose -f docker-compose.prod.yml ps
```
### 查看日志
```bash
# 查看所有服务日志
docker-compose -f docker-compose.prod.yml logs -f
# 查看特定服务日志
docker-compose -f docker-compose.prod.yml logs -f web
docker-compose -f docker-compose.prod.yml logs -f nginx
docker-compose -f docker-compose.prod.yml logs -f news-crawler
```
### 重启服务
```bash
# 重启所有服务
docker-compose -f docker-compose.prod.yml restart
# 重启特定服务
docker-compose -f docker-compose.prod.yml restart web
docker-compose -f docker-compose.prod.yml restart nginx
```
### 停止服务
```bash
docker-compose -f docker-compose.prod.yml down
```
### 更新部署
```bash
# 拉取最新代码
git pull
# 重新部署
./deploy-prod.sh
```
### SSL证书续期
Certbot容器会自动续期证书(每12小时检查一次)。也可以手动续期:
```bash
docker-compose -f docker-compose.prod.yml run --rm certbot renew
docker-compose -f docker-compose.prod.yml restart nginx
```
## 监控和维护
### 健康检查
```bash
# Web服务健康检查
curl http://localhost:3000
# News爬虫服务健康检查
curl http://localhost:3001/health
# Nginx状态
curl -I http://www.omniverseai.net
```
### 数据备份
新闻数据库和日志存储在Docker volumes中:
```bash
# 查看volumes
docker volume ls | grep omniverse
# 备份新闻数据库
docker run --rm -v website_news_data:/data -v $(pwd):/backup alpine tar czf /backup/news_data_backup.tar.gz -C /data .
# 备份日志
docker run --rm -v website_news_logs:/logs -v $(pwd):/backup alpine tar czf /backup/news_logs_backup.tar.gz -C /logs .
```
### 清理旧数据
新闻爬虫服务会自动清理7天前的旧新闻(可在 `.env.production` 中配置)。
## 故障排查
### 网站无法访问
1. 检查服务是否运行:
```bash
docker-compose -f docker-compose.prod.yml ps
```
2. 检查Nginx日志:
```bash
docker-compose -f docker-compose.prod.yml logs nginx
```
3. 检查端口是否监听:
```bash
netstat -tlnp | grep -E ':(80|443) '
```
### SSL证书问题
1. 检查证书文件:
```bash
ls -la ./certbot/conf/live/www.omniverseai.net/
```
2. 测试证书:
```bash
openssl s_client -connect www.omniverseai.net:443 -servername www.omniverseai.net
```
3. 手动续期:
```bash
docker-compose -f docker-compose.prod.yml run --rm certbot renew --dry-run
```
### 新闻服务问题
1. 检查服务日志:
```bash
docker-compose -f docker-compose.prod.yml logs news-crawler
```
2. 手动触发爬虫:
```bash
curl -X POST http://localhost:3001/api/crawl \
-H "X-API-Key: your-api-key"
```
3. 检查数据库:
```bash
docker exec -it omniverse-news-crawler ls -la /app/data
```
## 性能优化
### Nginx缓存
可以在 `nginx/default.conf` 中添加缓存配置:
```nginx
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m max_size=1g inactive=60m;
proxy_cache my_cache;
```
### 资源限制
可以在 `docker-compose.prod.yml` 中添加资源限制:
```yaml
services:
web:
deploy:
resources:
limits:
cpus: '1'
memory: 512M
```
## 安全建议
1. 定期更新Docker镜像
2. 使用强密码作为API_KEY
3. 启用防火墙,只开放80和443端口
4. 定期备份数据
5. 监控日志中的异常访问
6. 配置fail2ban防止暴力攻击
## 联系支持
如遇到问题,请查看:
- 应用日志:`docker-compose logs`
- Nginx日志:`/var/log/nginx/`(在容器内)
- 新闻服务日志:`./news-crawler-service/logs/`
Executable
+78
View File
@@ -0,0 +1,78 @@
#!/bin/bash
# 生产环境部署脚本
# 用于构建和启动OmniVerse官网
set -e
echo "=========================================="
echo " OmniVerse 官网生产环境部署"
echo "=========================================="
echo ""
# 检查必要文件
if [ ! -f ".env.production" ]; then
echo "错误:找不到 .env.production 文件"
echo "请复制 .env.production.example 并配置环境变量"
exit 1
fi
# 停止旧容器
echo "停止旧容器..."
docker-compose -f docker-compose.prod.yml down
# 清理旧镜像(可选)
read -p "是否清理旧的Docker镜像?(y/N) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
echo "清理旧镜像..."
docker system prune -f
fi
# 构建镜像
echo ""
echo "构建Docker镜像..."
docker-compose -f docker-compose.prod.yml build --no-cache
# 启动服务
echo ""
echo "启动服务..."
docker-compose -f docker-compose.prod.yml up -d
# 显示服务状态
echo ""
echo "等待服务启动..."
sleep 10
echo ""
echo "服务状态:"
docker-compose -f docker-compose.prod.yml ps
# 检查服务健康状态
echo ""
echo "检查服务健康状态..."
sleep 5
echo ""
echo "Web服务健康检查:"
docker inspect --format='{{json .State.Health}}' omniverse-web | python3 -m json.tool || echo "健康检查未就绪"
echo ""
echo "News爬虫服务健康检查:"
docker inspect --format='{{json .State.Health}}' omniverse-news-crawler | python3 -m json.tool || echo "健康检查未就绪"
echo ""
echo "=========================================="
echo " 部署完成!"
echo "=========================================="
echo ""
echo "访问地址:"
echo " HTTP: http://www.omniverseai.net"
echo " HTTPS: https://www.omniverseai.net (需要先申请SSL证书)"
echo ""
echo "查看日志:"
echo " docker-compose -f docker-compose.prod.yml logs -f"
echo ""
echo "如需申请SSL证书,请运行:"
echo " ./init-ssl.sh"
echo ""
+85
View File
@@ -0,0 +1,85 @@
version: '3.8'
services:
# Nginx反向代理
nginx:
image: nginx:alpine
container_name: omniverse-nginx
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
- ./certbot/conf:/etc/letsencrypt:ro
- ./certbot/www:/var/www/certbot:ro
- nginx_logs:/var/log/nginx
depends_on:
- web
- news-crawler
restart: unless-stopped
networks:
- omniverse-network
# 前端应用
web:
build:
context: .
dockerfile: Dockerfile
container_name: omniverse-web
environment:
- NODE_ENV=production
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
networks:
- omniverse-network
# 新闻爬虫服务
news-crawler:
build:
context: ./news-crawler-service
dockerfile: Dockerfile
container_name: omniverse-news-crawler
environment:
- NODE_ENV=production
- PORT=3001
volumes:
- news_data:/app/data
- news_logs:/app/logs
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:3001/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
networks:
- omniverse-network
# Certbot用于SSL证书申请和续期
certbot:
image: certbot/certbot:latest
container_name: omniverse-certbot
volumes:
- ./certbot/conf:/etc/letsencrypt
- ./certbot/www:/var/www/certbot
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
networks:
- omniverse-network
networks:
omniverse-network:
driver: bridge
volumes:
news_data:
driver: local
news_logs:
driver: local
nginx_logs:
driver: local
Executable
+49
View File
@@ -0,0 +1,49 @@
#!/bin/bash
# SSL证书初始化脚本
# 用于首次申请Let's Encrypt证书
set -e
DOMAIN="www.omniverseai.net"
EMAIL="admin@omniverseai.net" # 请修改为您的实际邮箱
echo "正在为域名 ${DOMAIN} 申请SSL证书..."
# 创建必要的目录
mkdir -p ./certbot/conf
mkdir -p ./certbot/www
# 启动nginx和certbot服务
docker-compose -f docker-compose.prod.yml up -d nginx
# 等待nginx启动
echo "等待Nginx启动..."
sleep 5
# 申请证书
echo "申请SSL证书..."
docker-compose -f docker-compose.prod.yml run --rm certbot certonly \
--webroot \
--webroot-path=/var/www/certbot \
--email ${EMAIL} \
--agree-tos \
--no-eff-email \
-d ${DOMAIN} \
-d omniverseai.net
if [ $? -eq 0 ]; then
echo "SSL证书申请成功!"
echo ""
echo "接下来的步骤:"
echo "1. 编辑 nginx/default.conf"
echo "2. 取消注释HTTPS服务器配置块"
echo "3. 注释或删除HTTP配置中的临时proxy_pass"
echo "4. 取消注释HTTP到HTTPS重定向"
echo "5. 运行: docker-compose -f docker-compose.prod.yml restart nginx"
else
echo "SSL证书申请失败,请检查:"
echo "1. 域名DNS是否正确解析到本服务器"
echo "2. 80端口是否可以从外网访问"
echo "3. 邮箱地址是否正确"
fi
+124
View File
@@ -0,0 +1,124 @@
# ============================================================================
# OmniVerse Website - Kubernetes Deployment
# Namespace: prod
# Domain: www.omniverseai.net
# ============================================================================
---
# Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: website
namespace: prod
labels:
app: website
environment: production
spec:
replicas: 2
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: website
template:
metadata:
labels:
app: website
environment: production
spec:
containers:
- name: website
image: harbor.omniverseai.net/omniverse/website:latest
imagePullPolicy: Always
ports:
- name: http
containerPort: 80
protocol: TCP
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
readinessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
imagePullSecrets:
- name: harbor-secret
---
# Service
apiVersion: v1
kind: Service
metadata:
name: website-svc
namespace: prod
labels:
app: website
spec:
type: ClusterIP
selector:
app: website
ports:
- name: http
port: 80
targetPort: 80
protocol: TCP
---
# Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: website-ingress
namespace: prod
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/proxy-body-size: "50m"
nginx.ingress.kubernetes.io/ssl-redirect: "false"
spec:
ingressClassName: nginx
rules:
- host: www.omniverseai.net
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: website-svc
port:
number: 80
- host: omniverseai.net
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: website-svc
port:
number: 80
+30
View File
@@ -0,0 +1,30 @@
# OmniVerse News Crawler Service Dockerfile
FROM node:20-alpine
# Set working directory
WORKDIR /app
# Install build dependencies
RUN apk add --no-cache python3 make g++
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm install --production
# Copy source code
COPY . .
# Create data and log directories
RUN mkdir -p /app/data /app/logs
# Expose port
EXPOSE 3001
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3001/health || exit 1
# Start application
CMD ["npm", "start"]
+36
View File
@@ -0,0 +1,36 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Gzip compression
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
gzip_vary on;
gzip_min_length 1024;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# SPA fallback
location / {
try_files $uri $uri/ /index.html;
}
# Cache static assets
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Health check endpoint
location /health {
access_log off;
return 200 "OK\n";
add_header Content-Type text/plain;
}
}
+84
View File
@@ -0,0 +1,84 @@
# OmniVerse官网 - HTTP配置(用于SSL证书申请和重定向)
server {
listen 80;
server_name www.omniverseai.net omniverseai.net;
# Let's Encrypt验证目录
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
# 其他请求重定向到HTTPS(证书配置后启用)
location / {
# 临时允许HTTP访问,用于测试
proxy_pass http://web:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 支持 Cloudflare HTTPS 代理
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
proxy_cache_bypass $http_upgrade;
}
# News API代理
location /api/news {
proxy_pass http://news-crawler:3001;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# HTTPS配置(获得证书后启用)
# server {
# listen 443 ssl http2;
# server_name www.omniverseai.net omniverseai.net;
#
# # SSL证书配置
# ssl_certificate /etc/letsencrypt/live/www.omniverseai.net/fullchain.pem;
# ssl_certificate_key /etc/letsencrypt/live/www.omniverseai.net/privkey.pem;
# ssl_trusted_certificate /etc/letsencrypt/live/www.omniverseai.net/chain.pem;
#
# # SSL安全配置
# ssl_protocols TLSv1.2 TLSv1.3;
# ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384';
# ssl_prefer_server_ciphers off;
# ssl_session_cache shared:SSL:10m;
# ssl_session_timeout 10m;
# ssl_stapling on;
# ssl_stapling_verify on;
#
# # 安全头
# add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# add_header X-Frame-Options "SAMEORIGIN" always;
# add_header X-Content-Type-Options "nosniff" always;
# add_header X-XSS-Protection "1; mode=block" always;
#
# # 前端应用
# location / {
# proxy_pass http://web:3000;
# proxy_http_version 1.1;
# proxy_set_header Upgrade $http_upgrade;
# proxy_set_header Connection 'upgrade';
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header X-Forwarded-Proto $scheme;
# proxy_cache_bypass $http_upgrade;
# }
#
# # News API代理
# location /api/news {
# proxy_pass http://news-crawler:3001;
# proxy_http_version 1.1;
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header X-Forwarded-Proto $scheme;
# }
# }
+40
View File
@@ -0,0 +1,40 @@
# Nginx配置 - OmniVerse官网生产环境
# 主配置
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
client_max_body_size 20M;
# 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/javascript application/json;
# 包含站点配置
include /etc/nginx/conf.d/*.conf;
}
+28 -48
View File
@@ -181,31 +181,30 @@ const Navigation: React.FC<NavigationProps> = ({ onOpenAuthModal }) => {
: 'bg-transparent'
}`}
>
<div className="container mx-auto px-4">
<div className="flex items-center justify-between h-20">
<div className="container mx-auto px-4 lg:px-6">
<div className="flex items-center justify-between h-20 lg:h-24">
{/* Logo */}
<Link to="/" className="block">
<motion.div
whileHover={{ scale: 1.05 }}
className="flex items-center gap-3 cursor-pointer"
className="flex items-center gap-3 lg:gap-4 cursor-pointer"
>
<div className="relative">
<div className="absolute inset-0 bg-cyan-500 blur-xl opacity-50"></div>
<div className="relative bg-gradient-to-br from-cyan-400 to-green-400 p-2 rounded-xl">
<Cpu size={28} className="text-black" />
<div className="relative bg-gradient-to-br from-cyan-400 to-green-400 p-2 lg:p-2.5 rounded-xl">
<Cpu size={28} className="text-black lg:w-8 lg:h-8" />
</div>
</div>
<div>
<h1 className="text-2xl font-bold bg-gradient-to-r from-cyan-400 to-green-400 bg-clip-text text-transparent">
<h1 className="text-xl lg:text-2xl font-bold bg-gradient-to-r from-cyan-400 to-green-400 bg-clip-text text-transparent">
{t('company.name')}
</h1>
<p className="text-xs text-gray-400">{t('company.tagline')}</p>
</div>
</motion.div>
</Link>
{/* Desktop Navigation */}
<div className="hidden lg:flex items-center gap-1">
<div className="hidden xl:flex items-center gap-1.5">
{navigationItems.map((item) => (
<div
key={item.id}
@@ -216,7 +215,7 @@ const Navigation: React.FC<NavigationProps> = ({ onOpenAuthModal }) => {
{item.href.startsWith('/blog') || item.href.startsWith('/articles') || item.href.startsWith('/docs') || item.href.startsWith('/industry-news') ? (
<Link
to={item.href}
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all duration-300 ${
className={`flex items-center gap-2 px-3 xl:px-4 py-2.5 rounded-lg text-sm font-medium whitespace-nowrap transition-all duration-300 ${
activeDropdown === item.id
? 'text-cyan-400 bg-cyan-500/10'
: 'text-gray-300 hover:text-cyan-400 hover:bg-gray-800/50'
@@ -237,7 +236,7 @@ const Navigation: React.FC<NavigationProps> = ({ onOpenAuthModal }) => {
) : item.href.startsWith('/about') || item.href.startsWith('/team') || item.href.startsWith('/history') || item.href.startsWith('/careers') || item.href.startsWith('/news') || item.href.startsWith('/investors') || item.href.startsWith('/csr') || item.href.startsWith('/contact') ? (
<Link
to={item.href}
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all duration-300 ${
className={`flex items-center gap-2 px-3 xl:px-4 py-2.5 rounded-lg text-sm font-medium whitespace-nowrap transition-all duration-300 ${
activeDropdown === item.id
? 'text-cyan-400 bg-cyan-500/10'
: 'text-gray-300 hover:text-cyan-400 hover:bg-gray-800/50'
@@ -260,7 +259,7 @@ const Navigation: React.FC<NavigationProps> = ({ onOpenAuthModal }) => {
href={item.href}
onClick={(e) => handleSmoothScroll(e, item.id)}
whileHover={{ y: -2 }}
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all duration-300 ${
className={`flex items-center gap-2 px-3 xl:px-4 py-2.5 rounded-lg text-sm font-medium whitespace-nowrap transition-all duration-300 ${
activeDropdown === item.id
? 'text-cyan-400 bg-cyan-500/10'
: 'text-gray-300 hover:text-cyan-400 hover:bg-gray-800/50'
@@ -369,7 +368,7 @@ const Navigation: React.FC<NavigationProps> = ({ onOpenAuthModal }) => {
</div>
{/* Right Side Actions */}
<div className="hidden lg:flex items-center gap-4">
<div className="hidden xl:flex items-center gap-4 lg:gap-6">
{/* Search Button */}
<motion.button
whileHover={{ scale: 1.05 }}
@@ -418,28 +417,18 @@ const Navigation: React.FC<NavigationProps> = ({ onOpenAuthModal }) => {
{isAuthenticated ? (
<UserMenu />
) : (
<div className="flex items-center gap-3">
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => onOpenAuthModal('login')}
className={`px-4 py-2 border rounded-lg transition-all duration-300 ${
effectiveTheme === 'dark'
? 'text-cyan-400 border-cyan-500/50 hover:bg-cyan-500/10'
: 'text-cyan-600 border-cyan-500/50 hover:bg-cyan-50'
}`}
>
{t('nav.login')}
</motion.button>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => onOpenAuthModal('register')}
className="px-4 py-2 bg-gradient-to-r from-cyan-500 to-green-500 text-black font-bold rounded-lg hover:shadow-lg hover:shadow-cyan-500/30 transition-all duration-300"
>
{t('nav.getStarted')}
</motion.button>
</div>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => onOpenAuthModal('login')}
className={`px-5 py-2.5 border rounded-lg text-sm font-medium transition-all duration-300 ${
effectiveTheme === 'dark'
? 'text-cyan-400 border-cyan-500/50 hover:bg-cyan-500/10'
: 'text-cyan-600 border-cyan-500/50 hover:bg-cyan-50'
}`}
>
{t('nav.login')}
</motion.button>
)}
</div>
@@ -448,7 +437,7 @@ const Navigation: React.FC<NavigationProps> = ({ onOpenAuthModal }) => {
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
className="lg:hidden p-2 text-gray-300 hover:text-cyan-400 transition-colors"
className="xl:hidden p-2 text-gray-300 hover:text-cyan-400 transition-colors"
>
{isMobileMenuOpen ? <X size={24} /> : <Menu size={24} />}
</motion.button>
@@ -465,9 +454,9 @@ const Navigation: React.FC<NavigationProps> = ({ onOpenAuthModal }) => {
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: '100%' }}
transition={{ type: 'tween', duration: 0.3 }}
className="fixed inset-0 z-40 bg-gray-900/95 backdrop-blur-xl lg:hidden"
className="fixed inset-0 z-40 bg-gray-900/95 backdrop-blur-xl xl:hidden"
>
<div className="flex flex-col h-full pt-20 pb-6 px-4 overflow-y-auto">
<div className="flex flex-col h-full pt-24 lg:pt-28 pb-6 px-4 overflow-y-auto">
{/* Search Bar */}
<div className="mb-6">
<motion.button
@@ -612,8 +601,8 @@ const Navigation: React.FC<NavigationProps> = ({ onOpenAuthModal }) => {
</div>
</div>
) : (
<div className="space-y-3 mb-6">
<button
<div className="mb-6">
<button
onClick={() => {
onOpenAuthModal('login');
setIsMobileMenuOpen(false);
@@ -622,15 +611,6 @@ const Navigation: React.FC<NavigationProps> = ({ onOpenAuthModal }) => {
>
{t('nav.login')}
</button>
<button
onClick={() => {
onOpenAuthModal('register');
setIsMobileMenuOpen(false);
}}
className="w-full px-4 py-3 bg-gradient-to-r from-cyan-500 to-green-500 text-black font-bold rounded-lg hover:shadow-lg hover:shadow-cyan-500/30 transition-all duration-300"
>
{t('nav.getStarted')}
</button>
</div>
)}
+1 -1
View File
@@ -9,7 +9,7 @@ import { SearchProvider } from './contexts/SearchContext'
import { AuthProvider } from './contexts/AuthContext'
import { FavoritesProvider } from './contexts/FavoritesContext'
const basename = import.meta.env.PROD ? '/website' : '';
const basename = '';
createRoot(document.getElementById('root')!).render(
<StrictMode>
+1 -1
View File
@@ -4,7 +4,7 @@ import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
base: process.env.NODE_ENV === 'production' ? '/website/' : '/',
base: '/',
server: {
port: 5174,
host: '0.0.0.0'