feat: HSAP platform v2 — modular navigation, quality review, audit log, world model simulation
Major changes: - New frontend (platform/web/): Vite + React 18 + TypeScript + Tailwind - 4-module navigation: 数据送标 / 模型管理 / 车队管理 / 系统管理 - Data catalog with charts (DMS/ADAS/Lane 3-tab view) - Quality review workflow (标注质检): Good/Fine/Bad scoring with auto-advance - Audit enhancements: batch operations, rejection categories, Feishu notifications - Operation audit log (操作日志) - World model simulation studio (仿真工坊) - Dataset version management with snapshots and diff - ADAS 7-class dataset integration (138K images organized + compressed) - User management with Feishu integration and pagination - CRUD/search/filter on all pages, card layout redesign - PIL-optimized image overlay rendering - Auto-snapshot on build, in_review workflow stage - Removed embedded algorithm code (now in workspace)
This commit is contained in:
24
platform/as_platform/jobs/feishu_bitable_sync.py
Normal file
24
platform/as_platform/jobs/feishu_bitable_sync.py
Normal file
@@ -0,0 +1,24 @@
|
||||
"""飞书多维表格同步周期任务。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from as_platform.config import FEISHU_BITABLE_AUTO_INGEST, FEISHU_BITABLE_SYNC_ENABLED
|
||||
from as_platform.integrations.feishu_bitable import is_bitable_configured
|
||||
from as_platform.integrations.feishu_bitable_ingest import process_pending_ingest
|
||||
from as_platform.integrations.feishu_bitable_sync import sync_hsap_to_bitable
|
||||
|
||||
|
||||
def run_sync_cycle() -> dict[str, Any]:
|
||||
if not is_bitable_configured():
|
||||
return {"ok": False, "message": "飞书多维表格未配置"}
|
||||
|
||||
out: dict[str, Any] = {"ok": True}
|
||||
if FEISHU_BITABLE_AUTO_INGEST:
|
||||
out["ingest"] = process_pending_ingest()
|
||||
out["sync"] = sync_hsap_to_bitable()
|
||||
return out
|
||||
|
||||
|
||||
def should_run_background_sync() -> bool:
|
||||
return FEISHU_BITABLE_SYNC_ENABLED and is_bitable_configured()
|
||||
@@ -62,12 +62,24 @@ def get_job(job_id: str) -> dict[str, Any] | None:
|
||||
return rec.to_dict() if rec else None
|
||||
|
||||
|
||||
def list_jobs(status: str | None = None, limit: int = 100) -> list[dict[str, Any]]:
|
||||
def list_jobs(
|
||||
status: str | None = None,
|
||||
*,
|
||||
offset: int = 0,
|
||||
limit: int = 20,
|
||||
) -> dict[str, Any]:
|
||||
with session_scope() as db:
|
||||
q = db.query(Job).order_by(Job.created_at.desc())
|
||||
if status:
|
||||
q = q.filter(Job.status == status)
|
||||
return [j.to_dict() for j in q.limit(limit).all()]
|
||||
total = q.count()
|
||||
rows = q.offset(max(0, offset)).limit(max(1, limit)).all()
|
||||
return {
|
||||
"items": [j.to_dict() for j in rows],
|
||||
"total": total,
|
||||
"offset": offset,
|
||||
"limit": limit,
|
||||
}
|
||||
|
||||
|
||||
def _patch(job_id: str, **fields: Any) -> dict[str, Any] | None:
|
||||
@@ -124,12 +136,25 @@ def _run_job(job_id: str) -> None:
|
||||
with trace_span("job_end", job_id=job_id, status="succeeded"):
|
||||
pass
|
||||
_sync_approval(job.get("approval_id"), "executed", persisted)
|
||||
if job.get("action") == "labeling_export":
|
||||
from as_platform.labeling.batch_stage import on_labeling_export_job_succeeded
|
||||
|
||||
on_labeling_export_job_succeeded(job)
|
||||
except Exception as e:
|
||||
_patch(job_id, status="failed", finished_at=_now(), result={"ok": False, "error": str(e)})
|
||||
publish("job.failed", {"job_id": job_id, "error": str(e)})
|
||||
with trace_span("job_end", job_id=job_id, status="failed", error=str(e)):
|
||||
pass
|
||||
_sync_approval(job.get("approval_id"), "failed", {"error": str(e)})
|
||||
if job.get("action") == "delivery_ingest":
|
||||
from as_platform.deliveries.service import mark_delivery_ingest_failed
|
||||
|
||||
params = job.get("params") or {}
|
||||
mark_delivery_ingest_failed(
|
||||
params.get("delivery_id"),
|
||||
job.get("approval_id"),
|
||||
str(e),
|
||||
)
|
||||
|
||||
|
||||
def _sync_approval(approval_id: str | None, status: str, result: dict) -> None:
|
||||
|
||||
@@ -19,6 +19,18 @@ AS_PY = ML_PY
|
||||
LONG_ACTIONS = {"train_dms", "train_lane", "pipeline_dms", "eval_dms", "eval_lane", "visualize_dms", "visualize_lane"}
|
||||
|
||||
|
||||
def _auto_snapshot(project: str, task: str = "") -> None:
|
||||
"""build 成功后自动创建数据集版本快照。"""
|
||||
try:
|
||||
from as_platform.data.versions import create_snapshot
|
||||
desc = f"自动快照 · build {project}"
|
||||
if task:
|
||||
desc += f"/{task}"
|
||||
create_snapshot(project, description=desc, author="system")
|
||||
except Exception:
|
||||
pass # 快照失败不影响 build
|
||||
|
||||
|
||||
def _run_ml(argv: list[str], timeout: int = 7200) -> dict[str, Any]:
|
||||
cmd = [sys.executable, str(ML_PY), *argv]
|
||||
proc = subprocess.run(cmd, cwd=str(WORKSPACE), capture_output=True, text=True, timeout=timeout)
|
||||
@@ -34,9 +46,14 @@ def execute_action(action: str, params: dict[str, Any]) -> dict[str, Any]:
|
||||
track = p.get("track", "platform")
|
||||
if track == "local":
|
||||
from algorithms.dms_yolo.adapter import train_local
|
||||
return train_local(p["task"], p.get("mode", "full"), p.get("config_overrides"))
|
||||
return train_local(
|
||||
p["task"],
|
||||
p.get("mode", "full"),
|
||||
p.get("config_overrides"),
|
||||
submode=p.get("submode"),
|
||||
)
|
||||
from algorithms.dms_yolo.adapter import train_platform
|
||||
return train_platform(p["task"], p.get("mode", "full"))
|
||||
return train_platform(p["task"], p.get("mode", "full"), submode=p.get("submode"))
|
||||
|
||||
if action == "train_lane":
|
||||
track = p.get("track", "platform")
|
||||
@@ -69,10 +86,15 @@ def execute_action(action: str, params: dict[str, Any]) -> dict[str, Any]:
|
||||
argv.append("--skip-validate")
|
||||
if p.get("no_refresh"):
|
||||
argv.append("--no-refresh")
|
||||
return _run_ml(argv)
|
||||
result = _run_ml(argv)
|
||||
# 自动创建数据集快照
|
||||
_auto_snapshot("dms", task=p.get("task", ""))
|
||||
return result
|
||||
|
||||
if action == "build_lane":
|
||||
return _run_ml(["build", "lane"])
|
||||
result = _run_ml(["build", "lane"])
|
||||
_auto_snapshot("lane")
|
||||
return result
|
||||
|
||||
if action == "enable_pack":
|
||||
return _run_ml(["enable", p["project"], p["pack"]])
|
||||
@@ -144,6 +166,20 @@ def execute_action(action: str, params: dict[str, Any]) -> dict[str, Any]:
|
||||
)
|
||||
return {"ok": True, "stdout": "register_batch ok", "stderr": ""}
|
||||
|
||||
if action == "delivery_ingest":
|
||||
from as_platform.integrations.delivery_ingest import run_delivery_ingest
|
||||
|
||||
delivery_id = p.get("delivery_id") or ""
|
||||
if not delivery_id:
|
||||
raise ValueError("缺少 delivery_id")
|
||||
result = run_delivery_ingest(delivery_id)
|
||||
return {
|
||||
"ok": True,
|
||||
"stdout": json.dumps(result, ensure_ascii=False),
|
||||
"stderr": "",
|
||||
"result": result,
|
||||
}
|
||||
|
||||
if action == "analyze_uploaded_dataset":
|
||||
from as_platform.data.lake import analyze_uploaded_candidate
|
||||
|
||||
@@ -156,4 +192,76 @@ def execute_action(action: str, params: dict[str, Any]) -> dict[str, Any]:
|
||||
"result": result,
|
||||
}
|
||||
|
||||
if action == "labeling_export":
|
||||
from as_platform.db.engine import session_scope
|
||||
from as_platform.db.models import LabelingCampaign
|
||||
from as_platform.labeling.annotate import resolve_campaign_batch_dir
|
||||
from as_platform.labeling.service import get_campaign
|
||||
|
||||
campaign_id = p.get("campaign_id", "")
|
||||
row = get_campaign(campaign_id)
|
||||
if not row:
|
||||
raise ValueError("campaign not found")
|
||||
task = row.get("task") or "dam"
|
||||
batch = row.get("batch") or ""
|
||||
pack = row.get("pack") or "dms_v2"
|
||||
export = row.get("export_default") or "yolo"
|
||||
if row.get("project") == "dms" and export in ("yolo", "yolo_pose") and batch:
|
||||
scripts_dir = WORKSPACE / "datasets" / "dms" / "scripts"
|
||||
if str(scripts_dir) not in sys.path:
|
||||
sys.path.insert(0, str(scripts_dir))
|
||||
from export_ls_to_yolo import export_batch
|
||||
|
||||
with session_scope() as db:
|
||||
camp = db.get(LabelingCampaign, campaign_id)
|
||||
if not camp:
|
||||
raise ValueError("campaign not found")
|
||||
batch_dir = resolve_campaign_batch_dir(camp)
|
||||
export_mode = "pose" if export == "yolo_pose" else "detect"
|
||||
conv = export_batch(
|
||||
batch_dir,
|
||||
task,
|
||||
mode=export_mode,
|
||||
task_mode=row.get("mode"),
|
||||
)
|
||||
if conv.get("written", 0) == 0:
|
||||
raise ValueError(
|
||||
"export_ls_to_yolo: 无有效标注可导出 (written=0); "
|
||||
f"skipped_empty={conv.get('skipped_empty')} missing_ann={conv.get('missing_ann')}"
|
||||
)
|
||||
argv = ["build", "dms", task, "--pack", pack, "--batch", batch]
|
||||
result = _run_ml(argv)
|
||||
result["export_convert"] = conv
|
||||
return result
|
||||
if row.get("project") == "lane" and export == "lane_gt_txt":
|
||||
scripts_dir = WORKSPACE / "datasets" / "lane" / "scripts"
|
||||
if str(scripts_dir) not in sys.path:
|
||||
sys.path.insert(0, str(scripts_dir))
|
||||
from export_ls_to_lane_gt import export_batch
|
||||
|
||||
with session_scope() as db:
|
||||
camp = db.get(LabelingCampaign, campaign_id)
|
||||
if not camp:
|
||||
raise ValueError("campaign not found")
|
||||
batch_dir = resolve_campaign_batch_dir(camp)
|
||||
conv = export_batch(batch_dir)
|
||||
if conv.get("written", 0) == 0:
|
||||
raise ValueError(
|
||||
"export_ls_to_lane_gt: 无有效标注可导出 (written=0); "
|
||||
f"skipped_empty={conv.get('skipped_empty')} missing_ann={conv.get('missing_ann')}"
|
||||
)
|
||||
argv = ["build", "lane"]
|
||||
result = _run_ml(argv)
|
||||
result["export_convert"] = conv
|
||||
return result
|
||||
return {
|
||||
"ok": True,
|
||||
"stdout": json.dumps({"export": export, "campaign": row}, ensure_ascii=False),
|
||||
"stderr": "",
|
||||
"message": f"export 类型 {export} 暂无 CLI,已记录",
|
||||
}
|
||||
|
||||
if action == "labeling_ml_predict":
|
||||
raise ValueError("labeling_ml_predict 已停用")
|
||||
|
||||
raise ValueError(f"未实现执行: {action}")
|
||||
|
||||
Reference in New Issue
Block a user