mirror of
https://github.com/fawney19/Aether.git
synced 2026-01-03 16:22:27 +08:00
Compare commits
6 Commits
v0.1.17
...
7553b0da80
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7553b0da80 | ||
|
|
8f30bf0bef | ||
|
|
8c12174521 | ||
|
|
6aa1876955 | ||
|
|
7f07122aea | ||
|
|
c2ddc6bd3c |
10
.env.example
10
.env.example
@@ -1,8 +1,16 @@
|
|||||||
# ==================== 必须配置(启动前) ====================
|
# ==================== 必须配置(启动前) ====================
|
||||||
# 以下配置项必须在项目启动前设置
|
# 以下配置项必须在项目启动前设置
|
||||||
|
|
||||||
# 数据库密码
|
# 数据库配置
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_USER=postgres
|
||||||
|
DB_NAME=aether
|
||||||
DB_PASSWORD=your_secure_password_here
|
DB_PASSWORD=your_secure_password_here
|
||||||
|
|
||||||
|
# Redis 配置
|
||||||
|
REDIS_HOST=localhost
|
||||||
|
REDIS_PORT=6379
|
||||||
REDIS_PASSWORD=your_redis_password_here
|
REDIS_PASSWORD=your_redis_password_here
|
||||||
|
|
||||||
# JWT密钥(使用 python generate_keys.py 生成)
|
# JWT密钥(使用 python generate_keys.py 生成)
|
||||||
|
|||||||
2
.github/workflows/docker-publish.yml
vendored
2
.github/workflows/docker-publish.yml
vendored
@@ -148,7 +148,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Update Dockerfile.app to use registry base image
|
- name: Update Dockerfile.app to use registry base image
|
||||||
run: |
|
run: |
|
||||||
sed -i "s|FROM aether-base:latest|FROM ${{ env.REGISTRY }}/${{ env.BASE_IMAGE_NAME }}:latest|g" Dockerfile.app
|
sed -i "s|FROM aether-base:latest AS builder|FROM ${{ env.REGISTRY }}/${{ env.BASE_IMAGE_NAME }}:latest AS builder|g" Dockerfile.app
|
||||||
|
|
||||||
- name: Build and push app image
|
- name: Build and push app image
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v5
|
||||||
|
|||||||
132
Dockerfile.app
132
Dockerfile.app
@@ -1,16 +1,134 @@
|
|||||||
# 应用镜像:基于基础镜像,只复制代码(秒级构建)
|
# 运行镜像:从 base 提取产物到精简运行时
|
||||||
# 构建命令: docker build -f Dockerfile.app -t aether-app:latest .
|
# 构建命令: docker build -f Dockerfile.app -t aether-app:latest .
|
||||||
FROM aether-base:latest
|
# 用于 GitHub Actions CI(官方源)
|
||||||
|
FROM aether-base:latest AS builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 复制前端源码并构建
|
||||||
|
COPY frontend/ ./frontend/
|
||||||
|
RUN cd frontend && npm run build
|
||||||
|
|
||||||
|
# ==================== 运行时镜像 ====================
|
||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 运行时依赖(无 gcc/nodejs/npm)
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
nginx \
|
||||||
|
supervisor \
|
||||||
|
libpq5 \
|
||||||
|
curl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# 从 base 镜像复制 Python 包
|
||||||
|
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
|
||||||
|
|
||||||
|
# 只复制需要的 Python 可执行文件
|
||||||
|
COPY --from=builder /usr/local/bin/gunicorn /usr/local/bin/
|
||||||
|
COPY --from=builder /usr/local/bin/uvicorn /usr/local/bin/
|
||||||
|
COPY --from=builder /usr/local/bin/alembic /usr/local/bin/
|
||||||
|
|
||||||
|
# 从 builder 阶段复制前端构建产物
|
||||||
|
COPY --from=builder /app/frontend/dist /usr/share/nginx/html
|
||||||
|
|
||||||
# 复制后端代码
|
# 复制后端代码
|
||||||
COPY src/ ./src/
|
COPY src/ ./src/
|
||||||
COPY alembic.ini ./
|
COPY alembic.ini ./
|
||||||
COPY alembic/ ./alembic/
|
COPY alembic/ ./alembic/
|
||||||
|
|
||||||
# 构建前端(使用基础镜像中已安装的 node_modules)
|
# Nginx 配置模板
|
||||||
COPY frontend/ /tmp/frontend/
|
RUN printf '%s\n' \
|
||||||
RUN cd /tmp/frontend && npm run build && \
|
'server {' \
|
||||||
cp -r dist/* /usr/share/nginx/html/ && \
|
' listen 80;' \
|
||||||
rm -rf /tmp/frontend
|
' server_name _;' \
|
||||||
|
' root /usr/share/nginx/html;' \
|
||||||
|
' index index.html;' \
|
||||||
|
' client_max_body_size 100M;' \
|
||||||
|
'' \
|
||||||
|
' location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {' \
|
||||||
|
' expires 1y;' \
|
||||||
|
' add_header Cache-Control "public, no-transform";' \
|
||||||
|
' try_files $uri =404;' \
|
||||||
|
' }' \
|
||||||
|
'' \
|
||||||
|
' location ~ ^/(src|node_modules)/ {' \
|
||||||
|
' deny all;' \
|
||||||
|
' return 404;' \
|
||||||
|
' }' \
|
||||||
|
'' \
|
||||||
|
' location ~ ^/(dashboard|admin|login)(/|$) {' \
|
||||||
|
' try_files $uri $uri/ /index.html;' \
|
||||||
|
' }' \
|
||||||
|
'' \
|
||||||
|
' location / {' \
|
||||||
|
' try_files $uri $uri/ @backend;' \
|
||||||
|
' }' \
|
||||||
|
'' \
|
||||||
|
' location @backend {' \
|
||||||
|
' proxy_pass http://127.0.0.1:PORT_PLACEHOLDER;' \
|
||||||
|
' 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;' \
|
||||||
|
' proxy_set_header Connection "";' \
|
||||||
|
' proxy_set_header Accept $http_accept;' \
|
||||||
|
' proxy_set_header Content-Type $content_type;' \
|
||||||
|
' proxy_set_header Authorization $http_authorization;' \
|
||||||
|
' proxy_set_header X-Api-Key $http_x_api_key;' \
|
||||||
|
' proxy_buffering off;' \
|
||||||
|
' proxy_cache off;' \
|
||||||
|
' proxy_request_buffering off;' \
|
||||||
|
' chunked_transfer_encoding on;' \
|
||||||
|
' gzip off;' \
|
||||||
|
' add_header X-Accel-Buffering no;' \
|
||||||
|
' proxy_connect_timeout 600s;' \
|
||||||
|
' proxy_send_timeout 600s;' \
|
||||||
|
' proxy_read_timeout 600s;' \
|
||||||
|
' }' \
|
||||||
|
'}' > /etc/nginx/sites-available/default.template
|
||||||
|
|
||||||
|
# Supervisor 配置
|
||||||
|
RUN printf '%s\n' \
|
||||||
|
'[supervisord]' \
|
||||||
|
'nodaemon=true' \
|
||||||
|
'logfile=/var/log/supervisor/supervisord.log' \
|
||||||
|
'pidfile=/var/run/supervisord.pid' \
|
||||||
|
'' \
|
||||||
|
'[program:nginx]' \
|
||||||
|
'command=/bin/bash -c "sed \"s/PORT_PLACEHOLDER/${PORT:-8084}/g\" /etc/nginx/sites-available/default.template > /etc/nginx/sites-available/default && /usr/sbin/nginx -g \"daemon off;\""' \
|
||||||
|
'autostart=true' \
|
||||||
|
'autorestart=true' \
|
||||||
|
'stdout_logfile=/var/log/nginx/access.log' \
|
||||||
|
'stderr_logfile=/var/log/nginx/error.log' \
|
||||||
|
'' \
|
||||||
|
'[program:app]' \
|
||||||
|
'command=gunicorn src.main:app -w %(ENV_GUNICORN_WORKERS)s -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:%(ENV_PORT)s --timeout 120 --access-logfile - --error-logfile - --log-level info' \
|
||||||
|
'directory=/app' \
|
||||||
|
'autostart=true' \
|
||||||
|
'autorestart=true' \
|
||||||
|
'stdout_logfile=/dev/stdout' \
|
||||||
|
'stdout_logfile_maxbytes=0' \
|
||||||
|
'stderr_logfile=/dev/stderr' \
|
||||||
|
'stderr_logfile_maxbytes=0' \
|
||||||
|
'environment=PYTHONUNBUFFERED=1,PYTHONIOENCODING=utf-8,LANG=C.UTF-8,LC_ALL=C.UTF-8,DOCKER_CONTAINER=true' > /etc/supervisor/conf.d/supervisord.conf
|
||||||
|
|
||||||
|
# 创建目录
|
||||||
|
RUN mkdir -p /var/log/supervisor /app/logs /app/data
|
||||||
|
|
||||||
|
# 环境变量
|
||||||
|
ENV PYTHONUNBUFFERED=1 \
|
||||||
|
PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PYTHONIOENCODING=utf-8 \
|
||||||
|
LANG=C.UTF-8 \
|
||||||
|
LC_ALL=C.UTF-8 \
|
||||||
|
PORT=8084
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||||
|
CMD curl -f http://localhost/health || exit 1
|
||||||
|
|
||||||
|
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
|
||||||
|
|||||||
135
Dockerfile.app.local
Normal file
135
Dockerfile.app.local
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
# 运行镜像:从 base 提取产物到精简运行时(国内镜像源版本)
|
||||||
|
# 构建命令: docker build -f Dockerfile.app.local -t aether-app:latest .
|
||||||
|
# 用于本地/国内服务器部署
|
||||||
|
FROM aether-base:latest AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 复制前端源码并构建
|
||||||
|
COPY frontend/ ./frontend/
|
||||||
|
RUN cd frontend && npm run build
|
||||||
|
|
||||||
|
# ==================== 运行时镜像 ====================
|
||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 运行时依赖(使用清华镜像源)
|
||||||
|
RUN sed -i 's/deb.debian.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list.d/debian.sources && \
|
||||||
|
apt-get update && apt-get install -y \
|
||||||
|
nginx \
|
||||||
|
supervisor \
|
||||||
|
libpq5 \
|
||||||
|
curl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# 从 base 镜像复制 Python 包
|
||||||
|
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
|
||||||
|
|
||||||
|
# 只复制需要的 Python 可执行文件
|
||||||
|
COPY --from=builder /usr/local/bin/gunicorn /usr/local/bin/
|
||||||
|
COPY --from=builder /usr/local/bin/uvicorn /usr/local/bin/
|
||||||
|
COPY --from=builder /usr/local/bin/alembic /usr/local/bin/
|
||||||
|
|
||||||
|
# 从 builder 阶段复制前端构建产物
|
||||||
|
COPY --from=builder /app/frontend/dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
# 复制后端代码
|
||||||
|
COPY src/ ./src/
|
||||||
|
COPY alembic.ini ./
|
||||||
|
COPY alembic/ ./alembic/
|
||||||
|
|
||||||
|
# Nginx 配置模板
|
||||||
|
RUN printf '%s\n' \
|
||||||
|
'server {' \
|
||||||
|
' listen 80;' \
|
||||||
|
' server_name _;' \
|
||||||
|
' root /usr/share/nginx/html;' \
|
||||||
|
' index index.html;' \
|
||||||
|
' client_max_body_size 100M;' \
|
||||||
|
'' \
|
||||||
|
' location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {' \
|
||||||
|
' expires 1y;' \
|
||||||
|
' add_header Cache-Control "public, no-transform";' \
|
||||||
|
' try_files $uri =404;' \
|
||||||
|
' }' \
|
||||||
|
'' \
|
||||||
|
' location ~ ^/(src|node_modules)/ {' \
|
||||||
|
' deny all;' \
|
||||||
|
' return 404;' \
|
||||||
|
' }' \
|
||||||
|
'' \
|
||||||
|
' location ~ ^/(dashboard|admin|login)(/|$) {' \
|
||||||
|
' try_files $uri $uri/ /index.html;' \
|
||||||
|
' }' \
|
||||||
|
'' \
|
||||||
|
' location / {' \
|
||||||
|
' try_files $uri $uri/ @backend;' \
|
||||||
|
' }' \
|
||||||
|
'' \
|
||||||
|
' location @backend {' \
|
||||||
|
' proxy_pass http://127.0.0.1:PORT_PLACEHOLDER;' \
|
||||||
|
' 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;' \
|
||||||
|
' proxy_set_header Connection "";' \
|
||||||
|
' proxy_set_header Accept $http_accept;' \
|
||||||
|
' proxy_set_header Content-Type $content_type;' \
|
||||||
|
' proxy_set_header Authorization $http_authorization;' \
|
||||||
|
' proxy_set_header X-Api-Key $http_x_api_key;' \
|
||||||
|
' proxy_buffering off;' \
|
||||||
|
' proxy_cache off;' \
|
||||||
|
' proxy_request_buffering off;' \
|
||||||
|
' chunked_transfer_encoding on;' \
|
||||||
|
' gzip off;' \
|
||||||
|
' add_header X-Accel-Buffering no;' \
|
||||||
|
' proxy_connect_timeout 600s;' \
|
||||||
|
' proxy_send_timeout 600s;' \
|
||||||
|
' proxy_read_timeout 600s;' \
|
||||||
|
' }' \
|
||||||
|
'}' > /etc/nginx/sites-available/default.template
|
||||||
|
|
||||||
|
# Supervisor 配置
|
||||||
|
RUN printf '%s\n' \
|
||||||
|
'[supervisord]' \
|
||||||
|
'nodaemon=true' \
|
||||||
|
'logfile=/var/log/supervisor/supervisord.log' \
|
||||||
|
'pidfile=/var/run/supervisord.pid' \
|
||||||
|
'' \
|
||||||
|
'[program:nginx]' \
|
||||||
|
'command=/bin/bash -c "sed \"s/PORT_PLACEHOLDER/${PORT:-8084}/g\" /etc/nginx/sites-available/default.template > /etc/nginx/sites-available/default && /usr/sbin/nginx -g \"daemon off;\""' \
|
||||||
|
'autostart=true' \
|
||||||
|
'autorestart=true' \
|
||||||
|
'stdout_logfile=/var/log/nginx/access.log' \
|
||||||
|
'stderr_logfile=/var/log/nginx/error.log' \
|
||||||
|
'' \
|
||||||
|
'[program:app]' \
|
||||||
|
'command=gunicorn src.main:app -w %(ENV_GUNICORN_WORKERS)s -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:%(ENV_PORT)s --timeout 120 --access-logfile - --error-logfile - --log-level info' \
|
||||||
|
'directory=/app' \
|
||||||
|
'autostart=true' \
|
||||||
|
'autorestart=true' \
|
||||||
|
'stdout_logfile=/dev/stdout' \
|
||||||
|
'stdout_logfile_maxbytes=0' \
|
||||||
|
'stderr_logfile=/dev/stderr' \
|
||||||
|
'stderr_logfile_maxbytes=0' \
|
||||||
|
'environment=PYTHONUNBUFFERED=1,PYTHONIOENCODING=utf-8,LANG=C.UTF-8,LC_ALL=C.UTF-8,DOCKER_CONTAINER=true' > /etc/supervisor/conf.d/supervisord.conf
|
||||||
|
|
||||||
|
# 创建目录
|
||||||
|
RUN mkdir -p /var/log/supervisor /app/logs /app/data
|
||||||
|
|
||||||
|
# 环境变量
|
||||||
|
ENV PYTHONUNBUFFERED=1 \
|
||||||
|
PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PYTHONIOENCODING=utf-8 \
|
||||||
|
LANG=C.UTF-8 \
|
||||||
|
LC_ALL=C.UTF-8 \
|
||||||
|
PORT=8084
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||||
|
CMD curl -f http://localhost/health || exit 1
|
||||||
|
|
||||||
|
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
|
||||||
119
Dockerfile.base
119
Dockerfile.base
@@ -1,124 +1,25 @@
|
|||||||
# 基础镜像:包含所有依赖,只在依赖变化时需要重建
|
# 构建镜像:编译环境 + 预编译的依赖
|
||||||
# 用于 GitHub Actions CI 构建(不使用国内镜像源)
|
# 用于 GitHub Actions CI 构建(不使用国内镜像源)
|
||||||
|
# 构建命令: docker build -f Dockerfile.base -t aether-base:latest .
|
||||||
|
# 只在 pyproject.toml 或 frontend/package*.json 变化时需要重建
|
||||||
FROM python:3.12-slim
|
FROM python:3.12-slim
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# 系统依赖
|
# 构建工具
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
nginx \
|
|
||||||
supervisor \
|
|
||||||
libpq-dev \
|
libpq-dev \
|
||||||
gcc \
|
gcc \
|
||||||
curl \
|
|
||||||
gettext-base \
|
|
||||||
nodejs \
|
nodejs \
|
||||||
npm \
|
npm \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Python 依赖(安装到系统,不用 -e 模式)
|
# Python 依赖
|
||||||
COPY pyproject.toml README.md ./
|
COPY pyproject.toml README.md ./
|
||||||
RUN mkdir -p src && touch src/__init__.py && \
|
RUN mkdir -p src && touch src/__init__.py && \
|
||||||
SETUPTOOLS_SCM_PRETEND_VERSION=0.1.0 pip install --no-cache-dir .
|
SETUPTOOLS_SCM_PRETEND_VERSION=0.1.0 pip install --no-cache-dir . && \
|
||||||
|
pip cache purge
|
||||||
|
|
||||||
# 前端依赖
|
# 前端依赖(只安装,不构建)
|
||||||
COPY frontend/package*.json /tmp/frontend/
|
COPY frontend/package*.json ./frontend/
|
||||||
WORKDIR /tmp/frontend
|
RUN cd frontend && npm ci
|
||||||
RUN npm ci
|
|
||||||
|
|
||||||
# Nginx 配置模板
|
|
||||||
RUN printf '%s\n' \
|
|
||||||
'server {' \
|
|
||||||
' listen 80;' \
|
|
||||||
' server_name _;' \
|
|
||||||
' root /usr/share/nginx/html;' \
|
|
||||||
' index index.html;' \
|
|
||||||
' client_max_body_size 100M;' \
|
|
||||||
'' \
|
|
||||||
' location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {' \
|
|
||||||
' expires 1y;' \
|
|
||||||
' add_header Cache-Control "public, no-transform";' \
|
|
||||||
' try_files $uri =404;' \
|
|
||||||
' }' \
|
|
||||||
'' \
|
|
||||||
' location ~ ^/(src|node_modules)/ {' \
|
|
||||||
' deny all;' \
|
|
||||||
' return 404;' \
|
|
||||||
' }' \
|
|
||||||
'' \
|
|
||||||
' location ~ ^/(dashboard|admin|login)(/|$) {' \
|
|
||||||
' try_files $uri $uri/ /index.html;' \
|
|
||||||
' }' \
|
|
||||||
'' \
|
|
||||||
' location / {' \
|
|
||||||
' try_files $uri $uri/ @backend;' \
|
|
||||||
' }' \
|
|
||||||
'' \
|
|
||||||
' location @backend {' \
|
|
||||||
' proxy_pass http://127.0.0.1:PORT_PLACEHOLDER;' \
|
|
||||||
' 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;' \
|
|
||||||
' proxy_set_header Connection "";' \
|
|
||||||
' proxy_set_header Accept $http_accept;' \
|
|
||||||
' proxy_set_header Content-Type $content_type;' \
|
|
||||||
' proxy_set_header Authorization $http_authorization;' \
|
|
||||||
' proxy_set_header X-Api-Key $http_x_api_key;' \
|
|
||||||
' proxy_buffering off;' \
|
|
||||||
' proxy_cache off;' \
|
|
||||||
' proxy_request_buffering off;' \
|
|
||||||
' chunked_transfer_encoding on;' \
|
|
||||||
' gzip off;' \
|
|
||||||
' add_header X-Accel-Buffering no;' \
|
|
||||||
' proxy_connect_timeout 600s;' \
|
|
||||||
' proxy_send_timeout 600s;' \
|
|
||||||
' proxy_read_timeout 600s;' \
|
|
||||||
' }' \
|
|
||||||
'}' > /etc/nginx/sites-available/default.template
|
|
||||||
|
|
||||||
# Supervisor 配置
|
|
||||||
RUN printf '%s\n' \
|
|
||||||
'[supervisord]' \
|
|
||||||
'nodaemon=true' \
|
|
||||||
'logfile=/var/log/supervisor/supervisord.log' \
|
|
||||||
'pidfile=/var/run/supervisord.pid' \
|
|
||||||
'' \
|
|
||||||
'[program:nginx]' \
|
|
||||||
'command=/bin/bash -c "sed \"s/PORT_PLACEHOLDER/${PORT:-8084}/g\" /etc/nginx/sites-available/default.template > /etc/nginx/sites-available/default && /usr/sbin/nginx -g \"daemon off;\""' \
|
|
||||||
'autostart=true' \
|
|
||||||
'autorestart=true' \
|
|
||||||
'stdout_logfile=/var/log/nginx/access.log' \
|
|
||||||
'stderr_logfile=/var/log/nginx/error.log' \
|
|
||||||
'' \
|
|
||||||
'[program:app]' \
|
|
||||||
'command=gunicorn src.main:app -w %(ENV_GUNICORN_WORKERS)s -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:%(ENV_PORT)s --timeout 120 --access-logfile - --error-logfile - --log-level info' \
|
|
||||||
'directory=/app' \
|
|
||||||
'autostart=true' \
|
|
||||||
'autorestart=true' \
|
|
||||||
'stdout_logfile=/dev/stdout' \
|
|
||||||
'stdout_logfile_maxbytes=0' \
|
|
||||||
'stderr_logfile=/dev/stderr' \
|
|
||||||
'stderr_logfile_maxbytes=0' \
|
|
||||||
'environment=PYTHONUNBUFFERED=1,PYTHONIOENCODING=utf-8,LANG=C.UTF-8,LC_ALL=C.UTF-8,DOCKER_CONTAINER=true' > /etc/supervisor/conf.d/supervisord.conf
|
|
||||||
|
|
||||||
# 创建目录
|
|
||||||
RUN mkdir -p /var/log/supervisor /app/logs /app/data /usr/share/nginx/html
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# 环境变量
|
|
||||||
ENV PYTHONUNBUFFERED=1 \
|
|
||||||
PYTHONDONTWRITEBYTECODE=1 \
|
|
||||||
PYTHONIOENCODING=utf-8 \
|
|
||||||
LANG=C.UTF-8 \
|
|
||||||
LC_ALL=C.UTF-8 \
|
|
||||||
PORT=8084
|
|
||||||
|
|
||||||
EXPOSE 80
|
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
|
||||||
CMD curl -f http://localhost/health || exit 1
|
|
||||||
|
|
||||||
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
|
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
# 基础镜像:包含所有依赖,只在依赖变化时需要重建
|
# 构建镜像:编译环境 + 预编译的依赖(国内镜像源版本)
|
||||||
# 构建命令: docker build -f Dockerfile.base -t aether-base:latest .
|
# 构建命令: docker build -f Dockerfile.base.local -t aether-base:latest .
|
||||||
|
# 只在 pyproject.toml 或 frontend/package*.json 变化时需要重建
|
||||||
FROM python:3.12-slim
|
FROM python:3.12-slim
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# 系统依赖
|
# 构建工具(使用清华镜像源)
|
||||||
RUN sed -i 's/deb.debian.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list.d/debian.sources && \
|
RUN sed -i 's/deb.debian.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list.d/debian.sources && \
|
||||||
apt-get update && apt-get install -y \
|
apt-get update && apt-get install -y \
|
||||||
nginx \
|
|
||||||
supervisor \
|
|
||||||
libpq-dev \
|
libpq-dev \
|
||||||
gcc \
|
gcc \
|
||||||
curl \
|
|
||||||
gettext-base \
|
|
||||||
nodejs \
|
nodejs \
|
||||||
npm \
|
npm \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
@@ -20,109 +17,12 @@ RUN sed -i 's/deb.debian.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.li
|
|||||||
# pip 镜像源
|
# pip 镜像源
|
||||||
RUN pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
|
RUN pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
|
||||||
|
|
||||||
# Python 依赖(安装到系统,不用 -e 模式)
|
# Python 依赖
|
||||||
COPY pyproject.toml README.md ./
|
COPY pyproject.toml README.md ./
|
||||||
RUN mkdir -p src && touch src/__init__.py && \
|
RUN mkdir -p src && touch src/__init__.py && \
|
||||||
SETUPTOOLS_SCM_PRETEND_VERSION=0.1.0 pip install --no-cache-dir .
|
SETUPTOOLS_SCM_PRETEND_VERSION=0.1.0 pip install --no-cache-dir . && \
|
||||||
|
pip cache purge
|
||||||
|
|
||||||
# 前端依赖
|
# 前端依赖(只安装,不构建,使用淘宝镜像源)
|
||||||
COPY frontend/package*.json /tmp/frontend/
|
COPY frontend/package*.json ./frontend/
|
||||||
WORKDIR /tmp/frontend
|
RUN cd frontend && npm config set registry https://registry.npmmirror.com && npm ci
|
||||||
RUN npm config set registry https://registry.npmmirror.com && npm ci
|
|
||||||
|
|
||||||
# Nginx 配置模板
|
|
||||||
RUN printf '%s\n' \
|
|
||||||
'server {' \
|
|
||||||
' listen 80;' \
|
|
||||||
' server_name _;' \
|
|
||||||
' root /usr/share/nginx/html;' \
|
|
||||||
' index index.html;' \
|
|
||||||
' client_max_body_size 100M;' \
|
|
||||||
'' \
|
|
||||||
' location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {' \
|
|
||||||
' expires 1y;' \
|
|
||||||
' add_header Cache-Control "public, no-transform";' \
|
|
||||||
' try_files $uri =404;' \
|
|
||||||
' }' \
|
|
||||||
'' \
|
|
||||||
' location ~ ^/(src|node_modules)/ {' \
|
|
||||||
' deny all;' \
|
|
||||||
' return 404;' \
|
|
||||||
' }' \
|
|
||||||
'' \
|
|
||||||
' location ~ ^/(dashboard|admin|login)(/|$) {' \
|
|
||||||
' try_files $uri $uri/ /index.html;' \
|
|
||||||
' }' \
|
|
||||||
'' \
|
|
||||||
' location / {' \
|
|
||||||
' try_files $uri $uri/ @backend;' \
|
|
||||||
' }' \
|
|
||||||
'' \
|
|
||||||
' location @backend {' \
|
|
||||||
' proxy_pass http://127.0.0.1:PORT_PLACEHOLDER;' \
|
|
||||||
' 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;' \
|
|
||||||
' proxy_set_header Connection "";' \
|
|
||||||
' proxy_set_header Accept $http_accept;' \
|
|
||||||
' proxy_set_header Content-Type $content_type;' \
|
|
||||||
' proxy_set_header Authorization $http_authorization;' \
|
|
||||||
' proxy_set_header X-Api-Key $http_x_api_key;' \
|
|
||||||
' proxy_buffering off;' \
|
|
||||||
' proxy_cache off;' \
|
|
||||||
' proxy_request_buffering off;' \
|
|
||||||
' chunked_transfer_encoding on;' \
|
|
||||||
' gzip off;' \
|
|
||||||
' add_header X-Accel-Buffering no;' \
|
|
||||||
' proxy_connect_timeout 600s;' \
|
|
||||||
' proxy_send_timeout 600s;' \
|
|
||||||
' proxy_read_timeout 600s;' \
|
|
||||||
' }' \
|
|
||||||
'}' > /etc/nginx/sites-available/default.template
|
|
||||||
|
|
||||||
# Supervisor 配置
|
|
||||||
RUN printf '%s\n' \
|
|
||||||
'[supervisord]' \
|
|
||||||
'nodaemon=true' \
|
|
||||||
'logfile=/var/log/supervisor/supervisord.log' \
|
|
||||||
'pidfile=/var/run/supervisord.pid' \
|
|
||||||
'' \
|
|
||||||
'[program:nginx]' \
|
|
||||||
'command=/bin/bash -c "sed \"s/PORT_PLACEHOLDER/${PORT:-8084}/g\" /etc/nginx/sites-available/default.template > /etc/nginx/sites-available/default && /usr/sbin/nginx -g \"daemon off;\""' \
|
|
||||||
'autostart=true' \
|
|
||||||
'autorestart=true' \
|
|
||||||
'stdout_logfile=/var/log/nginx/access.log' \
|
|
||||||
'stderr_logfile=/var/log/nginx/error.log' \
|
|
||||||
'' \
|
|
||||||
'[program:app]' \
|
|
||||||
'command=gunicorn src.main:app -w %(ENV_GUNICORN_WORKERS)s -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:%(ENV_PORT)s --timeout 120 --access-logfile - --error-logfile - --log-level info' \
|
|
||||||
'directory=/app' \
|
|
||||||
'autostart=true' \
|
|
||||||
'autorestart=true' \
|
|
||||||
'stdout_logfile=/dev/stdout' \
|
|
||||||
'stdout_logfile_maxbytes=0' \
|
|
||||||
'stderr_logfile=/dev/stderr' \
|
|
||||||
'stderr_logfile_maxbytes=0' \
|
|
||||||
'environment=PYTHONUNBUFFERED=1,PYTHONIOENCODING=utf-8,LANG=C.UTF-8,LC_ALL=C.UTF-8,DOCKER_CONTAINER=true' > /etc/supervisor/conf.d/supervisord.conf
|
|
||||||
|
|
||||||
# 创建目录
|
|
||||||
RUN mkdir -p /var/log/supervisor /app/logs /app/data /usr/share/nginx/html
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# 环境变量
|
|
||||||
ENV PYTHONUNBUFFERED=1 \
|
|
||||||
PYTHONDONTWRITEBYTECODE=1 \
|
|
||||||
PYTHONIOENCODING=utf-8 \
|
|
||||||
LANG=C.UTF-8 \
|
|
||||||
LC_ALL=C.UTF-8 \
|
|
||||||
PORT=8084
|
|
||||||
|
|
||||||
EXPOSE 80
|
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
|
||||||
CMD curl -f http://localhost/health || exit 1
|
|
||||||
|
|
||||||
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
|
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ build_base() {
|
|||||||
# 构建应用镜像
|
# 构建应用镜像
|
||||||
build_app() {
|
build_app() {
|
||||||
echo ">>> Building app image (code only)..."
|
echo ">>> Building app image (code only)..."
|
||||||
docker build -f Dockerfile.app -t aether-app:latest .
|
docker build -f Dockerfile.app.local -t aether-app:latest .
|
||||||
save_code_hash
|
save_code_hash
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
3
dev.sh
3
dev.sh
@@ -8,7 +8,8 @@ source .env
|
|||||||
set +a
|
set +a
|
||||||
|
|
||||||
# 构建 DATABASE_URL
|
# 构建 DATABASE_URL
|
||||||
export DATABASE_URL="postgresql://postgres:${DB_PASSWORD}@localhost:5432/aether"
|
export DATABASE_URL="postgresql://${DB_USER:-postgres}:${DB_PASSWORD}@${DB_HOST:-localhost}:${DB_PORT:-5432}/${DB_NAME:-aether}"
|
||||||
|
export REDIS_URL=redis://:${REDIS_PASSWORD}@${REDIS_HOST:-localhost}:${REDIS_PORT:-6379}/0
|
||||||
|
|
||||||
# 启动 uvicorn(热重载模式)
|
# 启动 uvicorn(热重载模式)
|
||||||
echo "🚀 启动本地开发服务器..."
|
echo "🚀 启动本地开发服务器..."
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ services:
|
|||||||
app:
|
app:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile.app
|
dockerfile: Dockerfile.app.local
|
||||||
image: aether-app:latest
|
image: aether-app:latest
|
||||||
container_name: aether-app
|
container_name: aether-app
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@@ -92,6 +92,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, useSlots, type Component } from 'vue'
|
import { computed, useSlots, type Component } from 'vue'
|
||||||
|
import { useEscapeKey } from '@/composables/useEscapeKey'
|
||||||
|
|
||||||
// Props 定义
|
// Props 定义
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -157,4 +158,14 @@ const maxWidthClass = computed(() => {
|
|||||||
const containerZIndex = computed(() => props.zIndex || 60)
|
const containerZIndex = computed(() => props.zIndex || 60)
|
||||||
const backdropZIndex = computed(() => props.zIndex || 60)
|
const backdropZIndex = computed(() => props.zIndex || 60)
|
||||||
const contentZIndex = computed(() => (props.zIndex || 60) + 10)
|
const contentZIndex = computed(() => (props.zIndex || 60) + 10)
|
||||||
|
|
||||||
|
// 添加 ESC 键监听
|
||||||
|
useEscapeKey(() => {
|
||||||
|
if (isOpen.value) {
|
||||||
|
handleClose()
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
disableOnInput: true,
|
||||||
|
once: false
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
80
frontend/src/composables/useEscapeKey.ts
Normal file
80
frontend/src/composables/useEscapeKey.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { onMounted, onUnmounted, ref } from 'vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ESC 键监听 Composable(简化版本,直接使用独立监听器)
|
||||||
|
* 用于按 ESC 键关闭弹窗或其他可关闭的组件
|
||||||
|
*
|
||||||
|
* @param callback - 按 ESC 键时执行的回调函数
|
||||||
|
* @param options - 配置选项
|
||||||
|
*/
|
||||||
|
export function useEscapeKey(
|
||||||
|
callback: () => void,
|
||||||
|
options: {
|
||||||
|
/** 是否在输入框获得焦点时禁用 ESC 键,默认 true */
|
||||||
|
disableOnInput?: boolean
|
||||||
|
/** 是否只监听一次,默认 false */
|
||||||
|
once?: boolean
|
||||||
|
} = {}
|
||||||
|
) {
|
||||||
|
const { disableOnInput = true, once = false } = options
|
||||||
|
const isActive = ref(true)
|
||||||
|
|
||||||
|
function handleKeyDown(event: KeyboardEvent) {
|
||||||
|
// 只处理 ESC 键
|
||||||
|
if (event.key !== 'Escape') return
|
||||||
|
|
||||||
|
// 检查组件是否还活跃
|
||||||
|
if (!isActive.value) return
|
||||||
|
|
||||||
|
// 如果配置了在输入框获得焦点时禁用,则检查当前焦点元素
|
||||||
|
if (disableOnInput) {
|
||||||
|
const activeElement = document.activeElement
|
||||||
|
const isInputElement = activeElement && (
|
||||||
|
activeElement.tagName === 'INPUT' ||
|
||||||
|
activeElement.tagName === 'TEXTAREA' ||
|
||||||
|
activeElement.tagName === 'SELECT' ||
|
||||||
|
activeElement.contentEditable === 'true' ||
|
||||||
|
activeElement.getAttribute('role') === 'textbox' ||
|
||||||
|
activeElement.getAttribute('role') === 'combobox'
|
||||||
|
)
|
||||||
|
|
||||||
|
// 如果焦点在输入框中,不处理 ESC 键
|
||||||
|
if (isInputElement) return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行回调
|
||||||
|
callback()
|
||||||
|
|
||||||
|
// 移除当前元素的焦点,避免残留样式
|
||||||
|
if (document.activeElement instanceof HTMLElement) {
|
||||||
|
document.activeElement.blur()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果只监听一次,则移除监听器
|
||||||
|
if (once) {
|
||||||
|
removeEventListener()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addEventListener() {
|
||||||
|
document.addEventListener('keydown', handleKeyDown)
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeEventListener() {
|
||||||
|
document.removeEventListener('keydown', handleKeyDown)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
addEventListener()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
isActive.value = false
|
||||||
|
removeEventListener()
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
addEventListener,
|
||||||
|
removeEventListener
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -698,6 +698,7 @@ import {
|
|||||||
Layers,
|
Layers,
|
||||||
BarChart3
|
BarChart3
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
|
import { useEscapeKey } from '@/composables/useEscapeKey'
|
||||||
import { useToast } from '@/composables/useToast'
|
import { useToast } from '@/composables/useToast'
|
||||||
import Card from '@/components/ui/card.vue'
|
import Card from '@/components/ui/card.vue'
|
||||||
import Badge from '@/components/ui/badge.vue'
|
import Badge from '@/components/ui/badge.vue'
|
||||||
@@ -833,6 +834,16 @@ watch(() => props.open, (newOpen) => {
|
|||||||
detailTab.value = 'basic'
|
detailTab.value = 'basic'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 添加 ESC 键监听
|
||||||
|
useEscapeKey(() => {
|
||||||
|
if (props.open) {
|
||||||
|
handleClose()
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
disableOnInput: true,
|
||||||
|
once: false
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -655,6 +655,7 @@ import {
|
|||||||
GripVertical,
|
GripVertical,
|
||||||
Copy
|
Copy
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
|
import { useEscapeKey } from '@/composables/useEscapeKey'
|
||||||
import Button from '@/components/ui/button.vue'
|
import Button from '@/components/ui/button.vue'
|
||||||
import Badge from '@/components/ui/badge.vue'
|
import Badge from '@/components/ui/badge.vue'
|
||||||
import Card from '@/components/ui/card.vue'
|
import Card from '@/components/ui/card.vue'
|
||||||
@@ -1296,6 +1297,16 @@ async function loadEndpoints() {
|
|||||||
showError(err.response?.data?.detail || '加载端点失败', '错误')
|
showError(err.response?.data?.detail || '加载端点失败', '错误')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加 ESC 键监听
|
||||||
|
useEscapeKey(() => {
|
||||||
|
if (props.open) {
|
||||||
|
handleClose()
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
disableOnInput: true,
|
||||||
|
once: false
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -472,6 +472,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch, computed } from 'vue'
|
import { ref, watch, computed } from 'vue'
|
||||||
import Button from '@/components/ui/button.vue'
|
import Button from '@/components/ui/button.vue'
|
||||||
|
import { useEscapeKey } from '@/composables/useEscapeKey'
|
||||||
import Card from '@/components/ui/card.vue'
|
import Card from '@/components/ui/card.vue'
|
||||||
import Badge from '@/components/ui/badge.vue'
|
import Badge from '@/components/ui/badge.vue'
|
||||||
import Separator from '@/components/ui/separator.vue'
|
import Separator from '@/components/ui/separator.vue'
|
||||||
@@ -897,6 +898,16 @@ const providerHeadersWithDiff = computed(() => {
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 添加 ESC 键监听
|
||||||
|
useEscapeKey(() => {
|
||||||
|
if (props.isOpen) {
|
||||||
|
handleClose()
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
disableOnInput: true,
|
||||||
|
once: false
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -136,11 +136,20 @@
|
|||||||
<!-- 分隔线 -->
|
<!-- 分隔线 -->
|
||||||
<div class="hidden sm:block h-4 w-px bg-border" />
|
<div class="hidden sm:block h-4 w-px bg-border" />
|
||||||
|
|
||||||
<!-- 刷新按钮 -->
|
<!-- 自动刷新按钮 -->
|
||||||
<RefreshButton
|
<Button
|
||||||
:loading="loading"
|
variant="ghost"
|
||||||
@click="$emit('refresh')"
|
size="icon"
|
||||||
|
class="h-8 w-8"
|
||||||
|
:class="autoRefresh ? 'text-primary' : ''"
|
||||||
|
:title="autoRefresh ? '点击关闭自动刷新' : '点击开启自动刷新(每10秒刷新)'"
|
||||||
|
@click="$emit('update:autoRefresh', !autoRefresh)"
|
||||||
|
>
|
||||||
|
<RefreshCcw
|
||||||
|
class="w-3.5 h-3.5"
|
||||||
|
:class="autoRefresh ? 'animate-spin' : ''"
|
||||||
/>
|
/>
|
||||||
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<Table>
|
<Table>
|
||||||
@@ -408,6 +417,7 @@ import { ref, computed, onUnmounted, watch } from 'vue'
|
|||||||
import {
|
import {
|
||||||
TableCard,
|
TableCard,
|
||||||
Badge,
|
Badge,
|
||||||
|
Button,
|
||||||
Select,
|
Select,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
@@ -420,8 +430,8 @@ import {
|
|||||||
TableHead,
|
TableHead,
|
||||||
TableCell,
|
TableCell,
|
||||||
Pagination,
|
Pagination,
|
||||||
RefreshButton,
|
|
||||||
} from '@/components/ui'
|
} from '@/components/ui'
|
||||||
|
import { RefreshCcw } from 'lucide-vue-next'
|
||||||
import { formatTokens, formatCurrency } from '@/utils/format'
|
import { formatTokens, formatCurrency } from '@/utils/format'
|
||||||
import { formatDateTime } from '../composables'
|
import { formatDateTime } from '../composables'
|
||||||
import { useRowClick } from '@/composables/useRowClick'
|
import { useRowClick } from '@/composables/useRowClick'
|
||||||
@@ -453,6 +463,8 @@ const props = defineProps<{
|
|||||||
pageSize: number
|
pageSize: number
|
||||||
totalRecords: number
|
totalRecords: number
|
||||||
pageSizeOptions: number[]
|
pageSizeOptions: number[]
|
||||||
|
// 自动刷新
|
||||||
|
autoRefresh: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -463,6 +475,7 @@ const emit = defineEmits<{
|
|||||||
'update:filterStatus': [value: string]
|
'update:filterStatus': [value: string]
|
||||||
'update:currentPage': [value: number]
|
'update:currentPage': [value: number]
|
||||||
'update:pageSize': [value: number]
|
'update:pageSize': [value: number]
|
||||||
|
'update:autoRefresh': [value: boolean]
|
||||||
'refresh': []
|
'refresh': []
|
||||||
'showDetail': [id: string]
|
'showDetail': [id: string]
|
||||||
}>()
|
}>()
|
||||||
|
|||||||
@@ -65,6 +65,7 @@
|
|||||||
:page-size="pageSize"
|
:page-size="pageSize"
|
||||||
:total-records="totalRecords"
|
:total-records="totalRecords"
|
||||||
:page-size-options="pageSizeOptions"
|
:page-size-options="pageSizeOptions"
|
||||||
|
:auto-refresh="globalAutoRefresh"
|
||||||
@update:selected-period="handlePeriodChange"
|
@update:selected-period="handlePeriodChange"
|
||||||
@update:filter-user="handleFilterUserChange"
|
@update:filter-user="handleFilterUserChange"
|
||||||
@update:filter-model="handleFilterModelChange"
|
@update:filter-model="handleFilterModelChange"
|
||||||
@@ -72,6 +73,7 @@
|
|||||||
@update:filter-status="handleFilterStatusChange"
|
@update:filter-status="handleFilterStatusChange"
|
||||||
@update:current-page="handlePageChange"
|
@update:current-page="handlePageChange"
|
||||||
@update:page-size="handlePageSizeChange"
|
@update:page-size="handlePageSizeChange"
|
||||||
|
@update:auto-refresh="handleAutoRefreshChange"
|
||||||
@refresh="refreshData"
|
@refresh="refreshData"
|
||||||
@export="exportData"
|
@export="exportData"
|
||||||
@show-detail="showRequestDetail"
|
@show-detail="showRequestDetail"
|
||||||
@@ -214,7 +216,10 @@ const hasActiveRequests = computed(() => activeRequestIds.value.length > 0)
|
|||||||
|
|
||||||
// 自动刷新定时器
|
// 自动刷新定时器
|
||||||
let autoRefreshTimer: ReturnType<typeof setInterval> | null = null
|
let autoRefreshTimer: ReturnType<typeof setInterval> | null = null
|
||||||
const AUTO_REFRESH_INTERVAL = 1000 // 1秒刷新一次
|
let globalAutoRefreshTimer: ReturnType<typeof setInterval> | null = null
|
||||||
|
const AUTO_REFRESH_INTERVAL = 1000 // 1秒刷新一次(用于活跃请求)
|
||||||
|
const GLOBAL_AUTO_REFRESH_INTERVAL = 10000 // 10秒刷新一次(全局自动刷新)
|
||||||
|
const globalAutoRefresh = ref(false) // 全局自动刷新开关
|
||||||
|
|
||||||
// 轮询活跃请求状态(轻量级,只更新状态变化的记录)
|
// 轮询活跃请求状态(轻量级,只更新状态变化的记录)
|
||||||
async function pollActiveRequests() {
|
async function pollActiveRequests() {
|
||||||
@@ -278,9 +283,34 @@ watch(hasActiveRequests, (hasActive) => {
|
|||||||
}
|
}
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
|
||||||
|
// 启动全局自动刷新
|
||||||
|
function startGlobalAutoRefresh() {
|
||||||
|
if (globalAutoRefreshTimer) return
|
||||||
|
globalAutoRefreshTimer = setInterval(refreshData, GLOBAL_AUTO_REFRESH_INTERVAL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止全局自动刷新
|
||||||
|
function stopGlobalAutoRefresh() {
|
||||||
|
if (globalAutoRefreshTimer) {
|
||||||
|
clearInterval(globalAutoRefreshTimer)
|
||||||
|
globalAutoRefreshTimer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理自动刷新开关变化
|
||||||
|
function handleAutoRefreshChange(value: boolean) {
|
||||||
|
globalAutoRefresh.value = value
|
||||||
|
if (value) {
|
||||||
|
startGlobalAutoRefresh()
|
||||||
|
} else {
|
||||||
|
stopGlobalAutoRefresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 组件卸载时清理定时器
|
// 组件卸载时清理定时器
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
stopAutoRefresh()
|
stopAutoRefresh()
|
||||||
|
stopGlobalAutoRefresh()
|
||||||
})
|
})
|
||||||
|
|
||||||
// 用户页面的前端分页
|
// 用户页面的前端分页
|
||||||
|
|||||||
@@ -350,6 +350,7 @@ import {
|
|||||||
Layers,
|
Layers,
|
||||||
Image as ImageIcon
|
Image as ImageIcon
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
|
import { useEscapeKey } from '@/composables/useEscapeKey'
|
||||||
import { useToast } from '@/composables/useToast'
|
import { useToast } from '@/composables/useToast'
|
||||||
import Card from '@/components/ui/card.vue'
|
import Card from '@/components/ui/card.vue'
|
||||||
import Badge from '@/components/ui/badge.vue'
|
import Badge from '@/components/ui/badge.vue'
|
||||||
@@ -453,6 +454,16 @@ function getFirst1hCachePrice(tieredPricing: TieredPricingConfig | undefined | n
|
|||||||
if (!tieredPricing?.tiers?.length) return '-'
|
if (!tieredPricing?.tiers?.length) return '-'
|
||||||
return get1hCachePrice(tieredPricing.tiers[0])
|
return get1hCachePrice(tieredPricing.tiers[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加 ESC 键监听
|
||||||
|
useEscapeKey(() => {
|
||||||
|
if (props.open) {
|
||||||
|
handleClose()
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
disableOnInput: true,
|
||||||
|
once: false
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ Provider Query API 端点
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import os
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
@@ -45,7 +46,11 @@ async def _fetch_openai_models(
|
|||||||
Returns:
|
Returns:
|
||||||
tuple[list, Optional[str]]: (模型列表, 错误信息)
|
tuple[list, Optional[str]]: (模型列表, 错误信息)
|
||||||
"""
|
"""
|
||||||
headers = {"Authorization": f"Bearer {api_key}"}
|
useragent = os.getenv("OPENAI_USER_AGENT") or "codex_cli_rs/0.73.0 (Mac OS 14.8.4; x86_64) Apple_Terminal/453"
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {api_key}",
|
||||||
|
"User-Agent": useragent,
|
||||||
|
}
|
||||||
if extra_headers:
|
if extra_headers:
|
||||||
# 防止 extra_headers 覆盖 Authorization
|
# 防止 extra_headers 覆盖 Authorization
|
||||||
safe_headers = {k: v for k, v in extra_headers.items() if k.lower() != "authorization"}
|
safe_headers = {k: v for k, v in extra_headers.items() if k.lower() != "authorization"}
|
||||||
@@ -91,10 +96,12 @@ async def _fetch_claude_models(
|
|||||||
Returns:
|
Returns:
|
||||||
tuple[list, Optional[str]]: (模型列表, 错误信息)
|
tuple[list, Optional[str]]: (模型列表, 错误信息)
|
||||||
"""
|
"""
|
||||||
|
useragent = os.getenv("CLAUDE_USER_AGENT") or "claude-cli/2.0.62 (external, cli)"
|
||||||
headers = {
|
headers = {
|
||||||
"x-api-key": api_key,
|
"x-api-key": api_key,
|
||||||
"Authorization": f"Bearer {api_key}",
|
"Authorization": f"Bearer {api_key}",
|
||||||
"anthropic-version": "2023-06-01",
|
"anthropic-version": "2023-06-01",
|
||||||
|
"User-Agent": useragent,
|
||||||
}
|
}
|
||||||
|
|
||||||
# 构建 /v1/models URL
|
# 构建 /v1/models URL
|
||||||
@@ -142,9 +149,12 @@ async def _fetch_gemini_models(
|
|||||||
models_url = f"{base_url_clean}/models?key={api_key}"
|
models_url = f"{base_url_clean}/models?key={api_key}"
|
||||||
else:
|
else:
|
||||||
models_url = f"{base_url_clean}/v1beta/models?key={api_key}"
|
models_url = f"{base_url_clean}/v1beta/models?key={api_key}"
|
||||||
|
useragent = os.getenv("GEMINI_USER_AGENT") or "gemini-cli/0.1.0 (external, cli)"
|
||||||
|
headers = {
|
||||||
|
"User-Agent": useragent,
|
||||||
|
}
|
||||||
try:
|
try:
|
||||||
response = await client.get(models_url)
|
response = await client.get(models_url, headers=headers)
|
||||||
logger.debug(f"Gemini models request to {models_url}: status={response.status_code}")
|
logger.debug(f"Gemini models request to {models_url}: status={response.status_code}")
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|||||||
Reference in New Issue
Block a user