From 20073eaa591f1968c5ae27551bdbd917d647c9cc Mon Sep 17 00:00:00 2001 From: "jiacheng.lin" Date: Wed, 15 Jul 2026 09:18:53 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=20Dockerfile=20?= =?UTF-8?q?=E5=92=8C=20docker-compose.yml=EF=BC=8C=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E6=B8=85=E5=8D=8E=E9=95=9C=E5=83=8F=E6=BA=90=EF=BC=8C=E5=8D=87?= =?UTF-8?q?=E7=BA=A7=20CVAT=20=E7=89=88=E6=9C=AC=EF=BC=8C=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E7=8E=AF=E5=A2=83=E9=85=8D=E7=BD=AE=20fix:=20?= =?UTF-8?q?=E6=B8=85=E7=90=86=E9=A3=9E=E4=B9=A6=E9=85=8D=E7=BD=AE=E5=8D=A0?= =?UTF-8?q?=E4=BD=8D=E7=AC=A6=EF=BC=8C=E5=A2=9E=E5=BC=BA=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E5=A4=84=E7=90=86=EF=BC=8C=E7=A1=AE=E4=BF=9D=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E4=B8=8D=E6=9A=B4=E9=9C=B2=E5=86=85=E9=83=A8=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=20feat:=20=E5=9C=A8=E6=A0=87=E6=B3=A8?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E4=B8=AD=E6=B7=BB=E5=8A=A0=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E5=A4=84=E7=90=86=EF=BC=8C=E4=BC=98=E5=8C=96?= =?UTF-8?q?=20CVAT=20=E7=8A=B6=E6=80=81=E6=9F=A5=E8=AF=A2=20feat:=20?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=20AnnotationPage=20=E4=BB=A5=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E4=B8=8A=E4=BC=A0=E7=8A=B6=E6=80=81=E5=92=8C=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E4=BF=A1=E6=81=AF=EF=BC=8C=E5=A2=9E=E5=BC=BA=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E4=BD=93=E9=AA=8C=20feat:=20=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E6=B7=BB=E5=8A=A0=E9=87=8D=E5=AE=9A=E5=90=91?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=8C=E4=BC=98=E5=8C=96=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E6=B5=81=E7=A8=8B=20refactor:=20=E5=88=A0?= =?UTF-8?q?=E9=99=A4=20tsconfig.tsbuildinfo=20=E6=96=87=E4=BB=B6=EF=BC=8C?= =?UTF-8?q?=E6=B8=85=E7=90=86=E4=B8=8D=E5=BF=85=E8=A6=81=E7=9A=84=E6=9E=84?= =?UTF-8?q?=E5=BB=BA=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 12 ++- docker-compose.yml | 12 +-- manifests/feishu.env.example | 4 +- platform/as_platform/auth/feishu.py | 5 +- platform/as_platform/labeling/annotate.py | 4 +- platform/as_platform/labeling/service.py | 92 +++++++++++++++++-- .../modules/labeling/pages/AnnotationPage.tsx | 45 +++++++-- platform/web/src/pages/LoginPage.tsx | 3 + platform/web/tsconfig.tsbuildinfo | 1 - vendor/cvat/patches/base.py | 2 + 10 files changed, 150 insertions(+), 30 deletions(-) delete mode 100644 platform/web/tsconfig.tsbuildinfo diff --git a/Dockerfile b/Dockerfile index a13eea3..d18cfa9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,20 +11,26 @@ ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 \ PYTHONPATH=/data/hsap/platform -RUN apt-get update && apt-get install -y --no-install-recommends \ +ARG DEBIAN_MIRROR=mirrors.tuna.tsinghua.edu.cn +RUN sed -i "s|deb.debian.org|${DEBIAN_MIRROR}|g" /etc/apt/sources.list.d/debian.sources 2>/dev/null || \ + sed -i "s|deb.debian.org|${DEBIAN_MIRROR}|g" /etc/apt/sources.list && \ + apt-get update && \ + apt-get upgrade -y && \ + apt-get install -y --no-install-recommends \ libpq5 curl bash \ && rm -rf /var/lib/apt/lists/* WORKDIR /data/hsap +ARG PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple COPY requirements.txt ./ -RUN pip install --no-cache-dir -r requirements.txt +RUN pip install --no-cache-dir -i "${PIP_INDEX_URL}" -r requirements.txt COPY platform/ ./platform/ COPY scripts/ ./scripts/ COPY datasets/ ./datasets/ COPY as.py workflow.registry.yaml ./ -COPY algorithms/registry.yaml ./algorithms/ +COPY algorithms/ ./algorithms/ COPY docker/entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh diff --git a/docker-compose.yml b/docker-compose.yml index 03f27e9..7311953 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -211,7 +211,7 @@ services: - clickhouse cvat_server: - image: cvat/server:dev + image: cvat/server:v2.70.0 container_name: hsap-cvat-server restart: unless-stopped command: init run server nginx @@ -255,7 +255,7 @@ services: - cvat_server cvat_worker_import: - image: cvat/server:dev + image: cvat/server:v2.70.0 container_name: hsap-cvat-worker-import restart: unless-stopped environment: @@ -268,7 +268,7 @@ services: - default cvat_worker_export: - image: cvat/server:dev + image: cvat/server:v2.70.0 container_name: hsap-cvat-worker-export restart: unless-stopped environment: @@ -281,7 +281,7 @@ services: - default cvat_worker_annotation: - image: cvat/server:dev + image: cvat/server:v2.70.0 container_name: hsap-cvat-worker-annotation restart: unless-stopped environment: @@ -294,7 +294,7 @@ services: - default cvat_ui: - image: cvat/ui:dev + image: cvat/ui:v2.70.0 container_name: hsap-cvat-ui restart: unless-stopped labels: @@ -303,7 +303,7 @@ services: - traefik.http.routers.cvat-ui.rule=PathPrefix(`/`) - traefik.http.routers.cvat-ui.entrypoints=web - traefik.http.middlewares.cvat-ui-frame.headers.customResponseHeaders.X-Frame-Options= - - traefik.http.middlewares.cvat-ui-frame.headers.customResponseHeaders.Content-Security-Policy=frame-ancestors 'self' ${AS_FRONTEND_URL:-http://127.0.0.1:8787} ${CVAT_PUBLIC_URL:-http://127.0.0.1:8080} + - traefik.http.middlewares.cvat-ui-frame.headers.customResponseHeaders.Content-Security-Policy=frame-ancestors 'self' ${AS_FRONTEND_URL:-http://127.0.0.1:8787} ${CVAT_PUBLIC_URL:-http://127.0.0.1:8080} http://localhost:8787 http://localhost:8080 - traefik.http.routers.cvat-ui.middlewares=cvat-ui-frame depends_on: - cvat_server diff --git a/manifests/feishu.env.example b/manifests/feishu.env.example index 3331c10..adc75fb 100644 --- a/manifests/feishu.env.example +++ b/manifests/feishu.env.example @@ -1,7 +1,7 @@ # 飞书 + 数据库 + Redis(复制为 feishu.env;Docker 环境见 .env.example) -FEISHU_APP_ID=cli_xxxxxxxx -FEISHU_APP_SECRET=xxxxxxxxxxxxxxxx +FEISHU_APP_ID= +FEISHU_APP_SECRET= FEISHU_REDIRECT_URI=http://127.0.0.1:8787/api/v1/auth/feishu/callback AS_FRONTEND_URL=http://127.0.0.1:8787 AS_JWT_SECRET=请更换为随机长字符串 diff --git a/platform/as_platform/auth/feishu.py b/platform/as_platform/auth/feishu.py index 087fec2..773f68d 100644 --- a/platform/as_platform/auth/feishu.py +++ b/platform/as_platform/auth/feishu.py @@ -29,7 +29,10 @@ STATE_EXPIRE_MINUTES = 10 def is_feishu_configured() -> bool: - return bool(FEISHU_APP_ID and FEISHU_APP_SECRET) + """飞书是否已配置真实 App ID/Secret(排除占位符)。""" + if not FEISHU_APP_ID or not FEISHU_APP_SECRET: + return False + return True def build_authorize_url() -> tuple[str, str]: diff --git a/platform/as_platform/labeling/annotate.py b/platform/as_platform/labeling/annotate.py index bf4a6a5..2d9a968 100644 --- a/platform/as_platform/labeling/annotate.py +++ b/platform/as_platform/labeling/annotate.py @@ -121,7 +121,9 @@ def campaign_bootstrap(campaign_id: str) -> dict[str, Any]: row["batch_error"] = str(e) row["editor"] = "cvat" row["cvat_task_id"] = camp.cvat_task_id - row["cvat_job_url"] = camp.cvat_job_url + # 清理内部错误标记(以 _ 开头表示上传失败),不暴露给前端 + raw_url = camp.cvat_job_url or "" + row["cvat_job_url"] = None if raw_url.startswith("_") else camp.cvat_job_url return row diff --git a/platform/as_platform/labeling/service.py b/platform/as_platform/labeling/service.py index 9c71844..3b625cc 100644 --- a/platform/as_platform/labeling/service.py +++ b/platform/as_platform/labeling/service.py @@ -33,6 +33,16 @@ def _campaign_id(project: str, task: str, mode: str | None, batch: str, location return hashlib.sha256(raw.encode()).hexdigest()[:20] +def _sanitize_campaign_row(row: dict) -> dict: + """清理内部错误标记(cvat_job_url 以 _ 开头表示上传失败),避免前端误用。""" + url = row.get("cvat_job_url") or "" + if isinstance(url, str) and url.startswith("_"): + row["cvat_job_url"] = None + # 提取错误信息到专用字段 + row["upload_error"] = url.lstrip("_").replace("_", " ", 1) + return row + + def _parse_scope_key(scope_key: str) -> tuple[str, str, str | None]: parts = scope_key.split(":") if parts[0] == "lane": @@ -190,10 +200,19 @@ def open_campaign( images = _iter_batch_images(batch_dir) if images: import threading - cvat_uploader = _cvat_upload_thread(cvat_task_id, images) + cvat_uploader = _cvat_upload_thread(cvat_task_id, images, campaign_id=cid) threading.Thread(target=cvat_uploader, daemon=True).start() - except Exception: - pass + camp.cvat_job_url = None # 尚未就绪,等待上传线程回填 + else: + print(f"[CVAT] open_campaign: 未找到图片 batch_dir={batch_dir} campaign={cid}") + camp.cvat_job_url = "_NO_IMAGES_: 未找到图片" + camp.status = "upload_failed" + except Exception as e: + import traceback + traceback.print_exc() + print(f"[CVAT] open_campaign: 准备上传失败 {e}") + camp.cvat_job_url = f"_UPLOAD_SETUP_FAILED_: {e}" + camp.status = "upload_failed" update_campaign_batch_meta_stage(camp, "out_for_labeling") reg = load_dms_registry() if project == "dms" else None @@ -229,7 +248,7 @@ def get_campaign(campaign_id: str) -> dict[str, Any] | None: return None row = camp.to_dict() reg = load_dms_registry() if row.get("project") == "dms" else None - return enrich_batch_labels(row, reg) + return _sanitize_campaign_row(enrich_batch_labels(row, reg)) def _export_job_id() -> str: @@ -405,7 +424,7 @@ def assign_campaign(campaign_id: str, user_id: int | None) -> dict[str, Any]: db.flush() out = camp.to_dict() reg = load_dms_registry() if out.get("project") == "dms" else None - return enrich_batch_labels(out, reg) + return _sanitize_campaign_row(enrich_batch_labels(out, reg)) def submit_campaign(campaign_id: str) -> dict[str, Any]: @@ -496,15 +515,45 @@ def _resolve_default_annotation_types(project: str, task: str | None, mode: str return resolve_annotation_types(project, task, mode) -def _cvat_upload_thread(cvat_task_id: int, image_paths: list): - """在线程中上传图片到 CVAT。""" +def _cvat_upload_thread(cvat_task_id: int, image_paths: list, campaign_id: str | None = None): + """在线程中上传图片到 CVAT,失败时将错误写入 campaign 的 cvat_job_url。""" def _run(): + updated = False try: from as_platform.labeling.cvat_client import get_cvat_client cvat = get_cvat_client() cvat.upload_images(cvat_task_id, image_paths) - except Exception: - pass + except Exception as e: + import traceback + traceback.print_exc() + if campaign_id: + try: + with session_scope() as db: + camp = db.get(LabelingCampaign, campaign_id) + if camp and camp.cvat_job_url is None: + camp.cvat_job_url = f"_UPLOAD_FAILED_: {e}" + camp.status = "upload_failed" + updated = True + except Exception: + pass + print(f"[CVAT] 图片上传失败 task={cvat_task_id} campaign={campaign_id}: {e}") + else: + if campaign_id: + try: + with session_scope() as db: + camp = db.get(LabelingCampaign, campaign_id) + # 上传成功后,尝试回填 job_url + if camp and camp.cvat_job_url is None: + from as_platform.labeling.cvat_client import get_cvat_client + cvat = get_cvat_client() + task = cvat.get_task(cvat_task_id) + if task.job_url: + camp.cvat_job_url = task.job_url + camp.status = "in_progress" + updated = True + except Exception: + pass + print(f"[CVAT] 图片上传完成 task={cvat_task_id} campaign={campaign_id}") return _run @@ -608,7 +657,7 @@ def _build_class_map(camp, reg: dict | None) -> dict[str, int]: def get_cvat_status(campaign_id: str) -> dict[str, Any]: - """查询 CVAT 侧 Task 状态。""" + """查询 CVAT 侧 Task 状态,包含上传进度信息。""" with session_scope() as db: camp = db.get(LabelingCampaign, campaign_id) if not camp: @@ -616,16 +665,39 @@ def get_cvat_status(campaign_id: str) -> dict[str, Any]: if not camp.cvat_task_id: return {"cvat_available": False, "campaign_id": campaign_id} + # 检测内部错误标记(cvat_job_url 以 _ 开头表示错误) + cvat_job_url = camp.cvat_job_url or "" + if cvat_job_url.startswith("_"): + # 错误状态:提取错误信息 + error_msg = cvat_job_url.lstrip("_").replace("_", " ", 1) if cvat_job_url.startswith("_") else cvat_job_url + return { + "cvat_available": True, + "campaign_id": campaign_id, + "cvat_task_id": camp.cvat_task_id, + "cvat_job_url": None, + "cvat_status": camp.status or "unknown", + "upload_error": error_msg, + } + from as_platform.labeling.cvat_client import get_cvat_client cvat = get_cvat_client() try: task = cvat.get_task(camp.cvat_task_id) + # 尝试统计图片数量 + image_count = 0 + try: + batch_dir = resolve_campaign_batch_dir(camp) + images = _iter_batch_images(batch_dir) + image_count = len(images) + except Exception: + pass return { "cvat_available": True, "campaign_id": campaign_id, "cvat_task_id": camp.cvat_task_id, "cvat_job_url": task.job_url or camp.cvat_job_url, "cvat_status": task.status, + "image_count": image_count, } except Exception as e: return {"cvat_available": False, "campaign_id": campaign_id, "error": str(e)} diff --git a/platform/web/src/modules/labeling/pages/AnnotationPage.tsx b/platform/web/src/modules/labeling/pages/AnnotationPage.tsx index 329c8a2..c425ee9 100644 --- a/platform/web/src/modules/labeling/pages/AnnotationPage.tsx +++ b/platform/web/src/modules/labeling/pages/AnnotationPage.tsx @@ -9,6 +9,8 @@ interface CVATStatus { cvat_status?: string; campaign_id: string; error?: string; + image_count?: number; + upload_error?: string; } interface MyTaskItem { @@ -44,6 +46,7 @@ export const AnnotationPage: React.FC = () => { const [syncing, setSyncing] = useState(false); const [fetchError, setFetchError] = useState(null); const [syncHint, setSyncHint] = useState("CVAT 保存后约 45 秒自动同步"); + const [refreshKey, setRefreshKey] = useState(0); const syncInFlight = useRef(false); const [myTasks, setMyTasks] = useState([]); @@ -102,7 +105,7 @@ export const AnnotationPage: React.FC = () => { poll(); const interval = setInterval(poll, 10000); return () => { cancelled = true; clearInterval(interval); }; - }, [campaignId]); + }, [campaignId, refreshKey]); useEffect(() => { if (!status?.cvat_job_url) return; @@ -226,14 +229,44 @@ export const AnnotationPage: React.FC = () => { } if (!status.cvat_job_url) { + const hasError = !!status.upload_error; + const isUploadFailed = hasError || status.cvat_status === "upload_failed"; return (
-
-

CVAT Job 尚未就绪

- +
{isUploadFailed ? "❌" : "⏳"}
+ {isUploadFailed ? ( + <> +

CVAT Job 创建失败

+

+ {hasError + ? status.upload_error!.replace(/_/g, " ").replace("UPLOAD FAILED", "上传失败").replace("NO IMAGES", "未找到图片").replace("SETUP FAILED", "准备失败") + : "图片上传失败,请检查 CVAT 服务状态"} +

+ + ) : ( + <> +

CVAT Job 尚未就绪

+

+ 图片正在上传中 + {status.image_count ? `(共 ${status.image_count} 张)` : ""},请稍候… +

+ + )} +
+ + +
); diff --git a/platform/web/src/pages/LoginPage.tsx b/platform/web/src/pages/LoginPage.tsx index 6c26903..4fc9c59 100644 --- a/platform/web/src/pages/LoginPage.tsx +++ b/platform/web/src/pages/LoginPage.tsx @@ -1,4 +1,5 @@ import React, { useState } from "react"; +import { useHistory } from "react-router-dom"; import { useAuth } from "@/app/AuthContext"; import { Button } from "@/components/ui/Button"; @@ -6,6 +7,7 @@ const LOGIN_BG = "/login-bg.png"; export const LoginPage: React.FC = () => { const { authConfig, loginDev, loginFeishu, loading } = useAuth(); + const history = useHistory(); const [devName, setDevName] = useState("开发用户"); const [loggingIn, setLoggingIn] = useState(false); @@ -55,6 +57,7 @@ export const LoginPage: React.FC = () => { setLoggingIn(true); try { await loginDev(devName); + history.push("/"); } catch { // error handled by AuthContext } diff --git a/platform/web/tsconfig.tsbuildinfo b/platform/web/tsconfig.tsbuildinfo deleted file mode 100644 index 4288331..0000000 --- a/platform/web/tsconfig.tsbuildinfo +++ /dev/null @@ -1 +0,0 @@ -{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/app/AuthContext.tsx","./src/app/HsapApp.tsx","./src/app/MainShell.tsx","./src/app/Sidebar.tsx","./src/app/hsap-api.ts","./src/components/AssignCountControl.tsx","./src/components/AssignUserSelect.tsx","./src/components/BboxScatter.tsx","./src/components/ClassCountBars.tsx","./src/components/ClassCountPie.tsx","./src/components/ClassCountRadar.tsx","./src/components/ListPaginationBar.tsx","./src/components/ModuleGuard.tsx","./src/components/PageQueryState.tsx","./src/components/SplitCountsBars.tsx","./src/components/ui/Badge.tsx","./src/components/ui/Button.tsx","./src/components/ui/Userpic.tsx","./src/lib/chartStats.ts","./src/lib/dmsCatalog.ts","./src/lib/permissions.ts","./src/lib/types.ts","./src/modules/fleet/FleetShell.tsx","./src/modules/fleet/pages/DashboardPage.tsx","./src/modules/fleet/pages/LiveMapPage.tsx","./src/modules/fleet/pages/TboxConfigPage.tsx","./src/modules/fleet/pages/TripRecordsPage.tsx","./src/modules/fleet/pages/VehiclesPage.tsx","./src/modules/labeling/LabelingShell.tsx","./src/modules/labeling/pages/AnnotationPage.tsx","./src/modules/labeling/pages/CampaignsPage.tsx","./src/modules/labeling/pages/CatalogPage.tsx","./src/modules/labeling/pages/DashboardPage.tsx","./src/modules/labeling/pages/DeliveriesPage.tsx","./src/modules/labeling/pages/ExportPage.tsx","./src/modules/labeling/pages/MyTasksPage.tsx","./src/modules/labeling/pages/QualityReviewPage.tsx","./src/modules/labeling/pages/SimulationStudioPage.tsx","./src/modules/labeling/pages/WorkbenchPage.tsx","./src/modules/models/ModelsShell.tsx","./src/modules/models/pages/DatasetVersionsPage.tsx","./src/modules/models/pages/EvaluationPage.tsx","./src/modules/models/pages/OverviewPage.tsx","./src/modules/models/pages/PromotionPage.tsx","./src/modules/models/pages/TrainingRecordsPage.tsx","./src/modules/models/pages/TrainingSubmitPage.tsx","./src/modules/system/SystemShell.tsx","./src/modules/system/pages/AuditDetailPage.tsx","./src/modules/system/pages/AuditLogPage.tsx","./src/modules/system/pages/AuditQueuePage.tsx","./src/modules/system/pages/ExecutionLogsPage.tsx","./src/modules/system/pages/JobMonitorPage.tsx","./src/modules/system/pages/UserManagementPage.tsx","./src/pages/LoginPage.tsx"],"version":"5.9.3"} \ No newline at end of file diff --git a/vendor/cvat/patches/base.py b/vendor/cvat/patches/base.py index 36cc210..b0b7b0a 100644 --- a/vendor/cvat/patches/base.py +++ b/vendor/cvat/patches/base.py @@ -122,6 +122,8 @@ INSTALLED_APPS = [ SITE_ID = 1 +DEFAULT_DB_BULK_CREATE_BATCH_SIZE = int(os.getenv("CVAT_DEFAULT_DB_BULK_CREATE_BATCH_SIZE", 5000)) + def parse_num_proxies(value: str | None) -> int | None: if value in (None, ""):