feat: 合并 Docker Compose、标注表格优化与部署文档

将 platform + CVAT 合并为单文件 docker-compose.yml,完善 .env 与 init/dev_up 脚本;
新增 docs/DEPLOY.md 与更新 README 以支持新机器部署;含数据湖示例、车队地图、
紧凑表格 UI、ADAS det_7cls 路径与批次台账等近期改动。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-16 17:06:31 +08:00
parent 0b8ade048e
commit 483e027482
117 changed files with 5933 additions and 1499 deletions

View File

@@ -45,6 +45,45 @@ class DeliveryPatchBody(BaseModel):
owner_name: str | None = None
@router.get("/scan")
def api_scan_deliveries(
_user: Annotated[User, Depends(require_any_permission("read:deliveries", "read:pending", "*"))],
projects: str | None = Query(None, description="逗号分隔: dms,adas,lane"),
) -> dict[str, Any]:
from as_platform.deliveries.scan import scan_delivery_sources
projs = [p.strip() for p in projects.split(",") if p.strip()] if projects else None
return scan_delivery_sources(projects=projs)
class ScanRegisterBody(BaseModel):
items: list[dict[str, Any]] = Field(default_factory=list)
sync_workbench: bool = True
@router.post("/scan/register")
def api_register_scanned_deliveries(
body: ScanRegisterBody,
user: Annotated[User, Depends(require_any_permission("write:delivery_submit", "*"))],
) -> dict[str, Any]:
from as_platform.deliveries.scan import register_scanned_to_ledger
return register_scanned_to_ledger(body.items, user, sync_workbench=body.sync_workbench)
@router.post("/{delivery_id}/sync-workbench")
def api_sync_delivery_workbench(
delivery_id: str,
_user: Annotated[User, Depends(require_any_permission("write:delivery_submit", "*"))],
) -> dict[str, Any]:
from as_platform.deliveries.scan import bridge_delivery_to_workbench
try:
return bridge_delivery_to_workbench(delivery_id)
except ValueError as e:
raise HTTPException(400, str(e)) from e
@router.get("")
def api_list_deliveries(
_user: Annotated[User, Depends(require_any_permission("read:deliveries", "read:pending", "*"))],

View File

@@ -128,10 +128,40 @@ def api_assign_campaign(
def api_labeling_batches(
_user: Annotated[User, Depends(require_permission("read:pending"))],
stage: str | None = Query(None),
stages: str | None = Query(None, description="逗号分隔多阶段,一次扫描返回"),
offset: int = Query(0, ge=0),
limit: int = Query(20, ge=1, le=100),
refresh: bool = Query(False, description="true 时先重建索引再返回"),
q: str | None = Query(None, description="搜索批次名/任务/项目"),
) -> dict[str, Any]:
return list_labeling_batches(stage=stage, offset=offset, limit=limit)
stage_list = [s.strip() for s in stages.split(",")] if stages else None
return list_labeling_batches(
stage=stage, stages=stage_list, offset=offset, limit=limit, refresh=refresh, q=q,
)
@router.post("/api/v1/labeling/batches/rebuild-index")
def api_rebuild_batch_index(
_user: Annotated[User, Depends(require_permission("write:labeling_assign"))],
) -> dict[str, Any]:
from as_platform.labeling.batch_index import rebuild_batch_index
return rebuild_batch_index()
@router.post("/api/v1/labeling/batches/{campaign_id}/archive")
def api_archive_batch(
campaign_id: str,
_user: Annotated[User, Depends(require_permission("write:labeling_assign"))],
) -> dict[str, Any]:
from as_platform.labeling.batch_index import archive_batch
try:
return archive_batch(campaign_id)
except FileNotFoundError:
raise HTTPException(404, "batch not found") from None
except ValueError as e:
raise HTTPException(400, str(e)) from e
@router.post("/api/v1/labeling/campaigns/open")
@@ -488,23 +518,7 @@ def api_review_image(
from as_platform.audit.review import get_review_image
import tempfile
try:
# Get class names from registry
import yaml
from as_platform.data.core import load_wf, proj_root
wf = load_wf()
root = proj_root(wf, "dms")
reg = yaml.safe_load((root / wf["projects"]["dms"]["registry"]).read_text())
# Build class_names dict from campaign scope
class_names: dict[int, str] = {}
# Try to get from the specific task
tasks = reg.get("tasks", {})
for task_cfg in tasks.values():
names = task_cfg.get("names")
if isinstance(names, list):
for i, n in enumerate(names):
class_names[i] = n
data = get_review_image(campaign_id, path, class_names)
data = get_review_image(campaign_id, path)
tmp = tempfile.NamedTemporaryFile(suffix=".jpg", delete=False)
tmp.write(data)
tmp.close()
@@ -528,6 +542,16 @@ def api_review_submit(
def api_review_progress(
campaign_id: str,
_user: Annotated[User, Depends(require_permission("read:pending"))],
) -> dict[str, int]:
) -> dict[str, Any]:
from as_platform.audit.review import review_progress
return review_progress(campaign_id)
@router.get("/api/v1/labeling/review-progress")
def api_review_progress_batch(
_user: Annotated[User, Depends(require_permission("read:pending"))],
campaign_ids: str = Query(..., description="逗号分隔 campaign id最多 50 个"),
) -> dict[str, Any]:
from as_platform.audit.review import review_progress_batch
ids = [x.strip() for x in campaign_ids.split(",") if x.strip()]
return review_progress_batch(ids)

View File

@@ -693,7 +693,8 @@ def api_scan_inbox(
project: str = Query("dms"),
) -> dict[str, Any]:
"""扫描 inbox 目录,返回未登记的新批次。"""
from as_platform.data.core import get_pending_report, load_wf, proj_root
from as_platform.data.core import load_wf, proj_root
from as_platform.labeling.batch_index import index_is_empty, rebuild_batch_index
wf = load_wf()
root = proj_root(wf, project)
@@ -701,8 +702,20 @@ def api_scan_inbox(
if not inbox.is_dir():
return {"project": project, "items": [], "inbox_path": str(inbox)}
report = get_pending_report()
registered = {b.get("batch", "") for b in report.get("batches", [])}
if index_is_empty():
rebuild_batch_index(wf)
from as_platform.db.engine import session_scope
from as_platform.db.models import BatchIndex
with session_scope() as db:
registered = {
(r.task or "", r.batch)
for r in db.query(BatchIndex).filter(
BatchIndex.project == project,
BatchIndex.archived.is_(False),
).all()
}
items: list[dict[str, Any]] = []
for task_dir in sorted(inbox.iterdir()):
@@ -713,7 +726,7 @@ def api_scan_inbox(
continue
batch_name = batch_dir.name
task_name = task_dir.name
if batch_name in registered:
if (task_name, batch_name) in registered:
continue # 已登记
# Count images (含 images/ 子目录)