iOSBuildServer/backend/routers/tasks.py
shen 75363ee276 feat: 版本号管理、构建日志持久化、App_Store 构建号自动递增
- 新增版本号配置 API(GET/PUT /api/config/versions)和前端管理界面
- 构建日志收集与保存:任务完成/取消/失败时写入 build.log
- 新增任务日志查询接口 GET /api/tasks/{task_id}/log
- App_Store 打包时自动递增 Build_Ver 第四位版本号
- 从 Provisioning Profile 自动提取 TEAM_ID 和 PROVISIONING_NAME
- 前端完成后支持查看完整构建日志
2026-06-08 11:23:15 +08:00

189 lines
6.0 KiB
Python

"""任务 API"""
import os
import shutil
import tempfile
import uuid
from datetime import datetime
from typing import List
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import FileResponse
from starlette.background import BackgroundTask
from sqlalchemy.orm import Session
from ..database import get_db
from ..models import Task
from ..schemas import TaskCreate, TaskResponse
router = APIRouter(prefix="/api/tasks", tags=["tasks"])
@router.post("", response_model=TaskResponse)
async def create_task(task: TaskCreate, db: Session = Depends(get_db)):
"""创建打包任务"""
from .config import load_config
config = load_config()
apps = config.get("apps", {})
schemes = config.get("schemes", {})
if task.app_id not in apps:
raise HTTPException(status_code=400, detail="App 不存在")
if task.scheme_id not in schemes:
raise HTTPException(status_code=400, detail="Scheme 不存在")
app = apps[task.app_id]
scheme = schemes[task.scheme_id]
task_id = str(uuid.uuid4())
db_task = Task(
id=task_id,
app_id=task.app_id,
app_name=app.get("name", ""),
build_type=task.build_type,
scheme_id=task.scheme_id,
scheme_name=scheme.get("displayName") or scheme.get("name", ""),
obfuscation=task.obfuscation,
branch=task.branch,
status="pending",
)
db.add(db_task)
db.commit()
db.refresh(db_task)
# 启动打包任务
from ..services.build_queue import build_queue
await build_queue.submit(task_id)
return db_task
@router.get("", response_model=List[TaskResponse])
async def list_tasks(
status: str = None,
limit: int = 20,
offset: int = 0,
db: Session = Depends(get_db)
):
"""获取任务列表"""
query = db.query(Task)
if status:
query = query.filter(Task.status == status)
tasks = query.order_by(Task.created_at.desc()).offset(offset).limit(limit).all()
return tasks
@router.get("/{task_id}", response_model=TaskResponse)
async def get_task(task_id: str, db: Session = Depends(get_db)):
"""获取任务详情"""
task = db.query(Task).filter(Task.id == task_id).first()
if not task:
raise HTTPException(status_code=404, detail="任务不存在")
return task
@router.delete("/{task_id}")
async def cancel_task(task_id: str, db: Session = Depends(get_db)):
"""取消任务"""
task = db.query(Task).filter(Task.id == task_id).first()
if not task:
raise HTTPException(status_code=404, detail="任务不存在")
if task.status in ("completed", "failed", "cancelled"):
raise HTTPException(status_code=400, detail="任务已完成或已取消")
task.status = "cancelled"
db.commit()
# 尝试取消运行中的任务
from ..services.build_queue import build_queue
build_queue.cancel(task_id)
return {"message": "任务已取消"}
@router.get("/{task_id}/log")
async def get_build_log(task_id: str, db: Session = Depends(get_db)):
"""获取打包日志"""
task = db.query(Task).filter(Task.id == task_id).first()
if not task:
raise HTTPException(status_code=404, detail="任务不存在")
from ..services.log_streamer import log_streamer
# 先从内存中获取(任务运行中)
lines = log_streamer._log_lines.get(task_id)
if lines:
content = ""
for entry in lines:
ts = entry["timestamp"]
level = entry["level"].upper()
msg = entry["message"]
content += f"[{ts}] [{level}] {msg}\n"
return {"log": content}
# 从文件获取(任务已完成)
if task.build_dir:
from pathlib import Path
log_path = Path(task.build_dir) / "build.log"
if log_path.exists():
return {"log": log_path.read_text(encoding="utf-8")}
return {"log": ""}
@router.get("/{task_id}/dsym")
async def download_dsym(task_id: str, db: Session = Depends(get_db)):
"""下载 dSYM 文件"""
task = db.query(Task).filter(Task.id == task_id).first()
if not task:
raise HTTPException(status_code=404, detail="任务不存在")
if not task.dsym_path:
raise HTTPException(status_code=404, detail="dSYM 文件不存在")
if not os.path.exists(task.dsym_path):
raise HTTPException(status_code=404, detail="dSYM 文件已被清理")
return FileResponse(task.dsym_path, filename=os.path.basename(task.dsym_path))
@router.get("/{task_id}/obfuscation-maps")
async def download_obfuscation_maps(task_id: str, db: Session = Depends(get_db)):
"""下载混淆映射表"""
task = db.query(Task).filter(Task.id == task_id).first()
if not task:
raise HTTPException(status_code=404, detail="任务不存在")
if not task.obfuscation_maps_path:
raise HTTPException(status_code=404, detail="混淆映射表不存在")
maps_path = task.obfuscation_maps_path
if not os.path.exists(maps_path):
raise HTTPException(status_code=404, detail="混淆映射表已被清理")
# 如果是目录,打包成 zip
if os.path.isdir(maps_path):
fd, zip_path = tempfile.mkstemp(suffix=".zip")
os.close(fd)
shutil.make_archive(zip_path.replace(".zip", ""), "zip", maps_path)
return FileResponse(
zip_path,
filename=f"{task.app_name}_混淆映射.zip",
background=BackgroundTask(os.remove, zip_path),
)
return FileResponse(maps_path, filename=os.path.basename(maps_path))
@router.get("/{task_id}/qrcode")
async def download_qrcode(task_id: str, db: Session = Depends(get_db)):
"""下载二维码图片"""
task = db.query(Task).filter(Task.id == task_id).first()
if not task:
raise HTTPException(status_code=404, detail="任务不存在")
if not task.qr_code_path:
raise HTTPException(status_code=404, detail="二维码不存在")
if not os.path.exists(task.qr_code_path):
raise HTTPException(status_code=404, detail="二维码文件已被清理")
return FileResponse(task.qr_code_path, media_type="image/png")