feat(deploy): fix docker deployment and add backend i18n
- Docker Deployment Fixes: - Switch base images to docker.m.daocloud.io to resolve registry 401 errors - Add Postgres and Redis services to docker-compose.traefik.yml - Fix frontend build: replace missing icons (Globe->Location, Chart->TrendCharts) - Fix frontend build: resolve pnpm CI/TTY issues and frozen lockfile errors - Add missing backend dependencies (sqlalchemy, psycopg2, redis-py, celery, docker-py) in pixi.toml - Ensure database tables are created on startup (lifespan event) - Backend Internationalization (i18n): - Add backend/app/core/i18n.py for locale handling - Update API endpoints (jobs, tasks, uploads, results) to return localized messages - Support 'Accept-Language' header (en/zh) - Documentation: - Update DOCKER_DEPLOYMENT.md with new architecture and troubleshooting - Update AGENTS.md with latest stack details and deployment steps - Update @fix_plan.md status Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ from ...models.job import Job, JobStatus
|
||||
from ...schemas.job import JobResponse
|
||||
from ...workers.tasks import run_bttoxin_analysis, update_queue_positions
|
||||
from ...config import settings
|
||||
from ...core.i18n import I18n, get_i18n
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -30,7 +31,8 @@ async def create_job(
|
||||
min_coverage: float = Form(0.6),
|
||||
allow_unknown_families: bool = Form(False),
|
||||
require_index_hit: bool = Form(True),
|
||||
db: Session = Depends(get_db)
|
||||
db: Session = Depends(get_db),
|
||||
i18n: I18n = Depends(get_i18n)
|
||||
):
|
||||
"""
|
||||
创建新分析任务
|
||||
@@ -52,14 +54,14 @@ async def create_job(
|
||||
if ext not in allowed_extensions:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Invalid file extension: {ext}. Allowed: {', '.join(allowed_extensions)}"
|
||||
detail=i18n.t("invalid_extension", ext=ext, allowed=', '.join(allowed_extensions))
|
||||
)
|
||||
|
||||
# 限制单文件上传
|
||||
if len(files) != 1:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Only one file allowed per task"
|
||||
detail=i18n.t("single_file_only")
|
||||
)
|
||||
|
||||
job_id = str(uuid.uuid4())
|
||||
@@ -114,20 +116,20 @@ async def create_job(
|
||||
|
||||
|
||||
@router.get("/{job_id}", response_model=JobResponse)
|
||||
async def get_job(job_id: str, db: Session = Depends(get_db)):
|
||||
async def get_job(job_id: str, db: Session = Depends(get_db), i18n: I18n = Depends(get_i18n)):
|
||||
"""获取任务详情"""
|
||||
job = db.query(Job).filter(Job.id == job_id).first()
|
||||
if not job:
|
||||
raise HTTPException(status_code=404, detail="Job not found")
|
||||
raise HTTPException(status_code=404, detail=i18n.t("job_not_found"))
|
||||
return job
|
||||
|
||||
|
||||
@router.get("/{job_id}/progress")
|
||||
async def get_job_progress(job_id: str, db: Session = Depends(get_db)):
|
||||
async def get_job_progress(job_id: str, db: Session = Depends(get_db), i18n: I18n = Depends(get_i18n)):
|
||||
"""获取任务进度"""
|
||||
job = db.query(Job).filter(Job.id == job_id).first()
|
||||
if not job:
|
||||
raise HTTPException(status_code=404, detail="Job not found")
|
||||
raise HTTPException(status_code=404, detail=i18n.t("job_not_found"))
|
||||
|
||||
result = {
|
||||
'job_id': job_id,
|
||||
@@ -148,7 +150,7 @@ async def get_job_progress(job_id: str, db: Session = Depends(get_db)):
|
||||
|
||||
|
||||
@router.post("/update-queue-positions")
|
||||
async def trigger_queue_update(db: Session = Depends(get_db)):
|
||||
async def trigger_queue_update(db: Session = Depends(get_db), i18n: I18n = Depends(get_i18n)):
|
||||
"""手动触发队列位置更新"""
|
||||
task = update_queue_positions.delay()
|
||||
return {"message": "Queue update triggered", "task_id": task.id}
|
||||
return {"message": i18n.t("queue_update_triggered"), "task_id": task.id}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""结果下载 API"""
|
||||
from fastapi import APIRouter, HTTPException, Response
|
||||
from fastapi import APIRouter, HTTPException, Response, Depends
|
||||
from fastapi.responses import FileResponse
|
||||
from sqlalchemy.orm import Session
|
||||
from pathlib import Path
|
||||
@@ -10,6 +10,7 @@ import shutil
|
||||
from ...database import get_db
|
||||
from ...models.job import Job, JobStatus
|
||||
from ...config import settings
|
||||
from ...core.i18n import I18n, get_i18n
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -17,21 +18,21 @@ RESULTS_DIR = Path(settings.RESULTS_DIR)
|
||||
|
||||
|
||||
@router.get("/{job_id}/download")
|
||||
async def download_results(job_id: str, db: Session = Depends(get_db)):
|
||||
async def download_results(job_id: str, db: Session = Depends(get_db), i18n: I18n = Depends(get_i18n)):
|
||||
"""下载任务结果(打包为 .tar.gz)"""
|
||||
job = db.query(Job).filter(Job.id == job_id).first()
|
||||
if not job:
|
||||
raise HTTPException(status_code=404, detail="Job not found")
|
||||
raise HTTPException(status_code=404, detail=i18n.t("job_not_found"))
|
||||
|
||||
if job.status != JobStatus.COMPLETED:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Job not completed. Current status: {job.status}"
|
||||
detail=i18n.t("job_not_completed", status=job.status)
|
||||
)
|
||||
|
||||
job_output_dir = RESULTS_DIR / job_id
|
||||
if not job_output_dir.exists():
|
||||
raise HTTPException(status_code=404, detail="Results not found on disk")
|
||||
raise HTTPException(status_code=404, detail=i18n.t("results_not_found"))
|
||||
|
||||
# 创建 tar.gz 文件到内存
|
||||
tar_buffer = io.BytesIO()
|
||||
@@ -53,11 +54,11 @@ async def download_results(job_id: str, db: Session = Depends(get_db)):
|
||||
|
||||
|
||||
@router.delete("/{job_id}")
|
||||
async def delete_job(job_id: str, db: Session = Depends(get_db)):
|
||||
async def delete_job(job_id: str, db: Session = Depends(get_db), i18n: I18n = Depends(get_i18n)):
|
||||
"""删除任务及其结果"""
|
||||
job = db.query(Job).filter(Job.id == job_id).first()
|
||||
if not job:
|
||||
raise HTTPException(status_code=404, detail="Job not found")
|
||||
raise HTTPException(status_code=404, detail=i18n.t("job_not_found"))
|
||||
|
||||
# 删除磁盘上的文件
|
||||
job_input_dir = Path(settings.UPLOAD_DIR) / job_id
|
||||
@@ -73,4 +74,4 @@ async def delete_job(job_id: str, db: Session = Depends(get_db)):
|
||||
db.delete(job)
|
||||
db.commit()
|
||||
|
||||
return {"message": f"Job {job_id} deleted successfully"}
|
||||
return {"message": i18n.t("job_deleted", job_id=job_id)}
|
||||
|
||||
@@ -9,6 +9,7 @@ from ...database import get_db
|
||||
from ...models.job import Job, JobStatus
|
||||
from ...schemas.job import JobResponse
|
||||
from ...config import settings
|
||||
from ...core.i18n import I18n, get_i18n
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -30,31 +31,31 @@ class QueuePosition(BaseModel):
|
||||
|
||||
|
||||
@router.post("/", response_model=JobResponse)
|
||||
async def create_task(request: TaskCreateRequest, db: Session = Depends(get_db)):
|
||||
async def create_task(request: TaskCreateRequest, db: Session = Depends(get_db), i18n: I18n = Depends(get_i18n)):
|
||||
"""创建新任务(兼容前端)"""
|
||||
# 暂时复用 jobs 逻辑
|
||||
# TODO: 实现完整的文件上传和处理
|
||||
raise HTTPException(status_code=501, detail="Use POST /api/v1/jobs/create for now")
|
||||
raise HTTPException(status_code=501, detail=i18n.t("use_create_endpoint"))
|
||||
|
||||
|
||||
@router.get("/{task_id}", response_model=JobResponse)
|
||||
async def get_task(task_id: str, db: Session = Depends(get_db)):
|
||||
async def get_task(task_id: str, db: Session = Depends(get_db), i18n: I18n = Depends(get_i18n)):
|
||||
"""获取任务状态"""
|
||||
job = db.query(Job).filter(Job.id == task_id).first()
|
||||
if not job:
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
raise HTTPException(status_code=404, detail=i18n.t("task_not_found"))
|
||||
return job
|
||||
|
||||
|
||||
@router.get("/{task_id}/queue")
|
||||
async def get_queue_position(task_id: str, db: Session = Depends(get_db)):
|
||||
async def get_queue_position(task_id: str, db: Session = Depends(get_db), i18n: I18n = Depends(get_i18n)):
|
||||
"""获取排队位置"""
|
||||
job = db.query(Job).filter(Job.id == task_id).first()
|
||||
if not job:
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
raise HTTPException(status_code=404, detail=i18n.t("task_not_found"))
|
||||
|
||||
if job.status not in [JobStatus.PENDING, JobStatus.QUEUED]:
|
||||
return {"position": 0, "message": "Task is not in queue"}
|
||||
return {"position": 0, "message": i18n.t("task_not_in_queue")}
|
||||
|
||||
# 计算排队位置
|
||||
ahead_jobs = db.query(Job).filter(
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"""文件上传 API"""
|
||||
from fastapi import APIRouter
|
||||
from fastapi import APIRouter, Depends
|
||||
from ...core.i18n import I18n, get_i18n
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/")
|
||||
async def upload_info():
|
||||
return {"message": "Upload endpoint"}
|
||||
async def upload_info(i18n: I18n = Depends(get_i18n)):
|
||||
return {"message": i18n.t("upload_endpoint")}
|
||||
|
||||
Reference in New Issue
Block a user