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:
2026-06-03 11:40:21 +08:00
parent 7c43b44c57
commit e72bc061c5
5487 changed files with 979207 additions and 6197 deletions

7
scripts/build_hsap_ls_ui.sh Executable file
View File

@@ -0,0 +1,7 @@
#!/usr/bin/env bash
# HSAP 前端构建(已迁移至 platform/web/ Vite 独立项目)
# 本脚本保留作为向后兼容入口,实际构建委托给 build_web.sh
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
echo "[build] 使用新构建系统 platform/web/ (Vite)..."
exec bash "$ROOT/scripts/build_web.sh"

60
scripts/build_web.sh Executable file
View File

@@ -0,0 +1,60 @@
#!/usr/bin/env bash
# Build HSAP standalone frontend from platform/web/ and deploy to platform/ui-hsap/dist/
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
echo "[web] Installing dependencies..."
cd "$ROOT/platform/web"
npm ci --silent
echo "[web] Building..."
npm run build
DIST="$ROOT/platform/ui-hsap/dist"
# Copy Label Studio Editor legacy build for /annotate/
EDITOR_SRC="${LABEL_STUDIO_DIST:-${ROOT}/../workspace/BK2/label-studio/web/dist/apps/hsap-platform}"
if [ -d "$EDITOR_SRC" ]; then
mkdir -p "$DIST/annotate"
cp -r "$EDITOR_SRC"/* "$DIST/annotate/"
# Copy webpack chunks to dist root (webpack publicPath="/" loads them from root)
for f in editor.js 849.js 710.js 408.js 63.js main.css editor.css; do
[ -f "$DIST/annotate/$f" ] && cp "$DIST/annotate/$f" "$DIST/$f"
done
# Custom annotate index.html
cat > "$DIST/annotate/index.html" << 'HTMLEOF'
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<title>HSAP · 标注编辑器</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="/annotate/main.css" />
<link rel="stylesheet" href="/annotate/editor.css" />
</head>
<body>
<div id="root"></div>
<script>
(function() {
var p = new URLSearchParams(location.search);
var cid = p.get('c');
if (cid) {
history.replaceState(null, '', '/labeling/campaigns/' + cid + '/annotate');
}
})();
</script>
<script src="/annotate/main.js"></script>
</body>
</html>
HTMLEOF
echo "[web] Annotate editor (legacy LS) deployed from $EDITOR_SRC"
else
echo "[web] ⚠ annotate editor source not found at $EDITOR_SRC"
fi
echo "[web] Build complete → $DIST"
echo "[web] Files:"
ls -lh "$DIST/index.html" "$DIST/assets/" | head -8

View File

@@ -8,6 +8,11 @@ if [[ ! -f .env ]]; then
cp .env.example .env
echo "已创建 .env默认挂载本仓库大文件数据可设置 AS_WORKSPACE_ROOT"
fi
DEFAULT_WS="$(cd "$ROOT/.." && pwd)/workspace"
if [[ -d "$DEFAULT_WS/DMS" ]] && ! grep -q '^AS_WORKSPACE_ROOT=' .env 2>/dev/null; then
echo "AS_WORKSPACE_ROOT=$DEFAULT_WS" >> .env
echo "已写入 AS_WORKSPACE_ROOT=$DEFAULT_WS"
fi
if ! command -v docker >/dev/null 2>&1; then
echo "未安装 Docker。Ubuntu: sudo apt install docker.io docker-compose-v2"

View File

@@ -0,0 +1,39 @@
#!/usr/bin/env bash
# 检查飞书多维表格配置与 HSAP 连通性需平台已启动、feishu.env 已填 BITABLE_*
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
if [[ -f manifests/feishu.env ]]; then
set -a
# shellcheck source=/dev/null
source manifests/feishu.env
set +a
fi
BASE="${AS_FRONTEND_URL:-http://127.0.0.1:8787}"
TOKEN="${HSAP_TOKEN:-}"
if [[ -z "$TOKEN" ]]; then
if [[ "${AS_DEV_AUTH:-}" =~ ^(1|true|yes)$ ]]; then
echo "==> 开发登录获取 token"
TOKEN="$(curl -sS -X POST "$BASE/api/v1/auth/dev/login" \
-H "Content-Type: application/json" \
-d '{"name":"feishu-verify"}' | python3 -c "import sys,json; print(json.load(sys.stdin).get('access_token',''))")"
fi
fi
if [[ -z "$TOKEN" ]]; then
echo "请设置 HSAP_TOKEN 或开启 AS_DEV_AUTH 后重试" >&2
exit 1
fi
echo "==> GET $BASE/api/v1/integrations/feishu/bitable/status"
curl -sS -H "Authorization: Bearer $TOKEN" "$BASE/api/v1/integrations/feishu/bitable/status" | python3 -m json.tool
echo ""
echo "==> GET bitable/tables查 table_id"
curl -sS -H "Authorization: Bearer $TOKEN" "$BASE/api/v1/integrations/feishu/bitable/tables" | python3 -m json.tool
echo ""
echo "OK: 若 status.ok=true 且 missing_columns 为空,则列名与飞书表一致。"

80
scripts/lake_checklist_audit.py Executable file
View File

@@ -0,0 +1,80 @@
#!/usr/bin/env python3
"""对照 DATA_LAKE_CHECKLIST 阶段 AE输出 HSAP 当前实现缺口(只读审计)。"""
from __future__ import annotations
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "platform"))
from as_platform.config import WORKSPACE # noqa: E402
def _check(name: str, ok: bool, detail: str = "") -> dict:
return {"item": name, "ok": ok, "detail": detail}
def main() -> int:
checks: list[dict] = []
staging = WORKSPACE / "lake" / "staging"
reports = WORKSPACE / "manifests" / "lake" / "reports"
curated = WORKSPACE / "lake" / "curated"
checks.append(_check("A_staging_dir", staging.is_dir(), str(staging)))
checks.append(
_check(
"A_upload_api",
(ROOT / "platform/as_platform/data/lake.py").is_file(),
"analyze_uploaded_candidate / promote",
)
)
checks.append(
_check(
"B_analyze_job",
(ROOT / "platform/as_platform/jobs/runner.py").is_file(),
"analyze_uploaded_dataset action",
)
)
checks.append(_check("B_reports_dir", reports.is_dir(), str(reports)))
report_files = list(reports.glob("*.json")) if reports.is_dir() else []
checks.append(_check("B_sample_report", len(report_files) > 0, f"count={len(report_files)}"))
checks.append(
_check(
"C_approval_flow",
(ROOT / "platform/as_platform/audit/queue.py").is_file(),
"delivery_ingest + approvals",
)
)
checks.append(
_check(
"D_curated_dir",
True,
"optional until first promote" + ("" if curated.is_dir() else f" (missing {curated})"),
)
)
checks.append(
_check(
"D_catalog_api",
(ROOT / "platform/as_platform/api/server.py").is_file(),
"GET /api/v1/catalog/*",
)
)
failed = [c for c in checks if not c["ok"]]
out = {"workspace": str(WORKSPACE), "checks": checks, "failed_count": len(failed)}
print(json.dumps(out, ensure_ascii=False, indent=2))
if failed:
print("\nLAKE_CHECKLIST_GAPS:", file=sys.stderr)
for c in failed:
print(f" - {c['item']}: {c['detail']}", file=sys.stderr)
return 1
print("LAKE_CHECKLIST_AUDIT_OK")
return 0
if __name__ == "__main__":
raise SystemExit(main())

201
scripts/organize_adas.py Normal file
View File

@@ -0,0 +1,201 @@
#!/usr/bin/env python3
"""整理 ADAS 数据集到 HSAP 平台可读格式。
ADAS 数据集目录结构:
road_datas/wf_batch*/images/ + labels/ ← 标准格式,直接用
OPEN/ONCE/tvt/{train,val,test}/.../images/ ← 需整理
VAL_s/.../images/ ← 需整理
输出: datasets/dms/packs/adas_v1/ 下创建组织好的目录 + 生成 class summary。
"""
from __future__ import annotations
import json
import shutil
from collections import defaultdict
from pathlib import Path
ARCHIVE = Path("/home/chengfanglu/DATA/workspace/BK2/archive/adas_2d_det_dataset")
DEST = Path("/home/chengfanglu/DATA/HSAP/datasets/dms/packs/adas_v1")
MANIFESTS = Path("/home/chengfanglu/DATA/HSAP/datasets/dms/manifests")
CLASS_NAMES = ["Pedestrain", "Car", "Truck", "Bus", "Motor-vehicles", "Tricycle", "cones"]
def count_yolo_labels(label_dir: Path) -> dict[int, int]:
"""统计 YOLO label 目录中各类别实例数。"""
counts: dict[int, int] = defaultdict(int)
if not label_dir.is_dir():
return dict(counts)
for txt in label_dir.glob("*.txt"):
try:
for line in txt.read_text().strip().splitlines():
parts = line.strip().split()
if parts:
cls_id = int(float(parts[0]))
counts[cls_id] += 1
except Exception:
pass
return dict(counts)
def count_images(img_dir: Path) -> int:
if not img_dir.is_dir():
return 0
return len([f for f in img_dir.iterdir() if f.suffix.lower() in {".jpg", ".jpeg", ".png", ".bmp"}])
def organize_wf_batches() -> tuple[int, dict[int, int], int]:
"""整理 road_datas/wf_batch*/ 到 adas/sources/ 。返回 (total_images, class_counts, total_boxes)。"""
sources_dir = DEST / "adas" / "sources"
sources_dir.mkdir(parents=True, exist_ok=True)
total_imgs = 0
total_boxes = 0
class_counts: dict[int, int] = defaultdict(int)
road_datas = ARCHIVE / "road_datas"
if not road_datas.is_dir():
print(f" ⚠ road_datas not found at {road_datas}")
return 0, {}, 0
for batch_dir in sorted(road_datas.iterdir()):
if not batch_dir.is_dir():
continue
img_dir = batch_dir / "images"
lbl_dir = batch_dir / "labels"
if not img_dir.is_dir():
continue
batch_name = batch_dir.name
dest_batch = sources_dir / batch_name
if dest_batch.exists():
continue # 已整理过
n_imgs = count_images(img_dir)
if n_imgs == 0:
continue
dest_batch.mkdir(parents=True, exist_ok=True)
dest_img = dest_batch / "images"
dest_lbl = dest_batch / "labels"
# 使用 symlink 节省磁盘空间
dest_img.symlink_to(img_dir.resolve())
if lbl_dir.is_dir():
dest_lbl.symlink_to(lbl_dir.resolve())
cc = count_yolo_labels(lbl_dir)
for cls_id, cnt in cc.items():
class_counts[cls_id] += cnt
total_boxes += cnt
total_imgs += n_imgs
print(f"{batch_name}: {n_imgs} imgs, {sum(cc.values()) if lbl_dir.is_dir() else 0} boxes")
return total_imgs, dict(class_counts), total_boxes
def organize_once() -> tuple[int, dict[int, int], int]:
"""整理 OPEN/ONCE/tvt/ 到 adas/sources/once_*/ 。"""
once_dir = ARCHIVE / "OPEN" / "ONCE" / "tvt"
if not once_dir.is_dir():
print(f" ⚠ ONCE not found at {once_dir}")
return 0, {}, 0
total_imgs = 0
total_boxes = 0
class_counts: dict[int, int] = defaultdict(int)
sources_dir = DEST / "adas" / "sources"
for split in ["train", "val", "test"]:
split_dir = once_dir / split
if not split_dir.is_dir():
continue
for cam_dir in sorted(split_dir.iterdir()):
if not cam_dir.is_dir():
continue
for scene_dir in sorted(cam_dir.iterdir()):
if not scene_dir.is_dir():
continue
img_dir = scene_dir / "images"
if not img_dir.is_dir():
continue
n_imgs = count_images(img_dir)
if n_imgs == 0:
continue
batch_name = f"once_{split}_{cam_dir.name}_{scene_dir.name}"
dest_batch = sources_dir / batch_name
if dest_batch.exists():
continue
dest_batch.mkdir(parents=True, exist_ok=True)
(dest_batch / "images").symlink_to(img_dir.resolve())
total_imgs += n_imgs
print(f"{batch_name}: {n_imgs} imgs")
return total_imgs, dict(class_counts), total_boxes
def write_class_summary(total_boxes: dict[int, int]):
"""生成平台可读的 class summary 文件。"""
MANIFESTS.mkdir(parents=True, exist_ok=True)
summary_path = MANIFESTS / "dataset_class_summary.txt"
# 读取已有内容(保留其他任务的统计)
existing: dict[str, str] = {}
if summary_path.is_file():
current_task = None
for line in summary_path.read_text().splitlines():
line = line.strip()
if line.startswith("[") and line.endswith("]"):
current_task = line[1:-1]
existing[current_task] = ""
elif current_task:
existing[current_task] += line + "\n"
# 生成 adas 统计
lines = ["[adas]"]
for cls_id in sorted(total_boxes.keys()):
name = CLASS_NAMES[cls_id] if cls_id < len(CLASS_NAMES) else f"class_{cls_id}"
lines.append(f"{name}: {total_boxes[cls_id]}")
existing["adas"] = "\n".join(lines[1:]) + "\n"
# 写回
with open(summary_path, "w") as f:
for task, content in existing.items():
f.write(f"[{task}]\n{content}")
print(f" ✓ Class summary written to {summary_path}")
def main():
print("=== 整理 ADAS 数据集 ===")
DEST.mkdir(parents=True, exist_ok=True)
print("\n1. 整理 wf_batch 批次...")
wf_imgs, wf_classes, wf_boxes = organize_wf_batches()
print("\n2. 整理 ONCE 数据...")
once_imgs, once_classes, once_boxes = organize_once()
# 合并统计
total_boxes: dict[int, int] = defaultdict(int)
for cls_id, cnt in wf_classes.items():
total_boxes[cls_id] += cnt
for cls_id, cnt in once_classes.items():
total_boxes[cls_id] += cnt
print(f"\n=== 整理完成 ===")
print(f" wf_batch 图片: {wf_imgs}, 标注框: {wf_boxes}")
print(f" ONCE 图片: {once_imgs}")
print(f" 总标注框: {sum(total_boxes.values())}")
print(f" 各类别分布: {dict(total_boxes)}")
print("\n3. 生成 class summary...")
write_class_summary(dict(total_boxes))
print("\n✅ 完成!可以刷新 catalog 了")
if __name__ == "__main__":
main()

View File

@@ -6,6 +6,13 @@ set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
export PYTHONPATH="${ROOT}/platform${PYTHONPATH:+:$PYTHONPATH}"
if [[ -f "$ROOT/.env" ]]; then
set -a
# shellcheck disable=SC1091
source "$ROOT/.env"
set +a
fi
if [[ -f "$ROOT/manifests/feishu.env" ]]; then
set -a
# shellcheck disable=SC1091
@@ -15,6 +22,11 @@ fi
export AS_JOB_EXECUTOR="${AS_JOB_EXECUTOR:-thread}"
# .env 中 AS_REDIS_PORT=6380 时与 feishu.env 默认 6379 对齐
if [[ -n "${AS_REDIS_PORT:-}" ]]; then
export AS_REDIS_URL="redis://127.0.0.1:${AS_REDIS_PORT}/0"
fi
cd "$ROOT"
bash scripts/setup_links.sh 2>/dev/null || true

View File

@@ -29,8 +29,11 @@ link_dir() {
rm -f "$link"
fi
mkdir -p "$(dirname "$link")"
ln -sfn "$target" "$link"
echo " $link -> $target"
local rel link_parent
link_parent="$(dirname "$link")"
rel="$(python3 -c "import os.path; print(os.path.relpath('''$target''', '''$link_parent'''))")"
ln -sfn "$rel" "$link"
echo " $link -> $rel"
}
mkdir -p "$WS/Lane" "$ROOT/datasets" "$ROOT/algorithms/dms_yolo" "$ROOT/algorithms/lane_ufld"
@@ -39,8 +42,22 @@ mkdir -p "$WS/Lane" "$ROOT/datasets" "$ROOT/algorithms/dms_yolo" "$ROOT/algorith
[[ -d "$WS/lane" ]] && ln -sfn "$WS/lane" "$WS/Lane/dataset" 2>/dev/null || true
[[ -d "$WS/LaneDection/Code" ]] && ln -sfn "$WS/LaneDection/Code" "$WS/Lane/code" 2>/dev/null || true
echo ">>> datasets"
link_dir "$WS/DMS/DATASET" "$ROOT/datasets/dms"
echo ">>> datasets/dms保留仓库内 registry/scripts仅软链 packs"
DMS_ROOT="$ROOT/datasets/dms"
if [[ -L "$DMS_ROOT" ]]; then
rm -f "$DMS_ROOT"
fi
if [[ ! -d "$DMS_ROOT" ]] && [[ -d "${DMS_ROOT}.embedded.bak" ]]; then
mv "${DMS_ROOT}.embedded.bak" "$DMS_ROOT"
fi
mkdir -p "$DMS_ROOT/packs"
if [[ -d "$WS/DMS/DATASET/packs" ]]; then
for pack in "$WS/DMS/DATASET/packs"/*; do
[[ -e "$pack" ]] || continue
name="$(basename "$pack")"
link_dir "$pack" "$DMS_ROOT/packs/$name"
done
fi
link_dir "$WS/lane" "$ROOT/datasets/lane"
echo ">>> algorithms"

73
scripts/smoke_labeling_api.sh Executable file
View File

@@ -0,0 +1,73 @@
#!/usr/bin/env bash
# 标注 API 冒烟离线单元测试始终运行API 部分需 platform 在 :8787可用 HSAP_API_SKIP=1 跳过)
set -euo pipefail
BASE="${HSAP_API:-http://127.0.0.1:8787}"
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
echo "==> offline export_ls_to_yolo unit tests"
python3 "$ROOT/datasets/dms/scripts/test_export_ls_to_yolo.py"
echo "==> offline export_ls_to_lane_gt unit tests"
python3 "$ROOT/datasets/lane/scripts/test_export_ls_to_lane_gt.py"
if [[ "${HSAP_API_SKIP:-0}" == "1" ]]; then
echo "SKIP API tests (HSAP_API_SKIP=1)"
echo "OK (offline only)"
exit 0
fi
echo "==> check platform $BASE"
LOGIN_RESP=$(curl -sS -m 5 -X POST "$BASE/api/v1/auth/dev/login" \
-H 'Content-Type: application/json' -d '{"name":"smoke"}' 2>&1) || LOGIN_RESP=""
TOKEN=$(printf '%s' "$LOGIN_RESP" | python3 -c "
import sys, json
raw = sys.stdin.read().strip()
if not raw:
sys.exit(1)
try:
data = json.loads(raw)
except json.JSONDecodeError:
sys.exit(1)
tok = data.get('access_token')
if not tok:
print(data.get('detail') or data, file=sys.stderr)
sys.exit(1)
print(tok)
" 2>/dev/null) || {
echo "SKIP API tests: platform 未就绪 ($BASE)"
echo " 启动: cd $ROOT && docker compose up -d platform"
echo " 或: bash $ROOT/scripts/run_local.sh"
echo " 仅跑离线测试可: HSAP_API_SKIP=1 bash $0"
echo "OK (offline only)"
exit 0
}
AUTH="Authorization: Bearer $TOKEN"
echo "==> labeling/batches"
curl -sS -H "$AUTH" "$BASE/api/v1/labeling/batches" | python3 -m json.tool | head -40
CID=$(curl -sS -H "$AUTH" "$BASE/api/v1/labeling/batches" | python3 -c "
import sys,json
items=json.load(sys.stdin).get('items',[])
dam=[i for i in items if i.get('task')=='dam' and '0516' in str(i.get('batch',''))]
print(dam[0]['campaign_id'] if dam else (items[0]['campaign_id'] if items else ''))
")
if [[ -z "$CID" ]]; then
echo "open campaign dam/batch_0516"
CID=$(curl -sS -X POST -H "$AUTH" -H 'Content-Type: application/json' \
"$BASE/api/v1/labeling/campaigns/open" \
-d '{"project":"dms","task":"dam","batch":"batch_0516","mode":"batch_0516","location":"inbox"}' \
| python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))")
fi
echo "campaign_id=$CID"
echo "==> bootstrap"
curl -sS -H "$AUTH" "$BASE/api/v1/labeling/campaigns/$CID/bootstrap" | python3 -m json.tool | head -25
echo "==> tasks"
curl -sS -H "$AUTH" "$BASE/api/v1/labeling/campaigns/$CID/tasks?limit=3" | python3 -m json.tool | head -30
echo "==> export"
curl -sS -X POST -H "$AUTH" "$BASE/api/v1/labeling/campaigns/$CID/export" | python3 -m json.tool
echo "OK"

View File

@@ -0,0 +1,55 @@
#!/usr/bin/env bash
# 校验 workflow active_packs、train_versions 与 yaml_active 对齐ML 自动化 P0
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
python3 <<'PY'
import sys
from pathlib import Path
import yaml
root = Path(".")
wf = yaml.safe_load((root / "workflow.registry.yaml").read_text(encoding="utf-8"))
tv_path = root / "datasets/dms/manifests/train_versions.yaml"
yaml_active = root / "datasets/dms/manifests/yaml_active"
errors: list[str] = []
if not tv_path.is_file():
errors.append(f"missing {tv_path}")
else:
tv = yaml.safe_load(tv_path.read_text(encoding="utf-8")) or {}
for key, meta in tv.items():
if key in ("schema",):
continue
if not isinstance(meta, dict):
continue
rel = meta.get("data_yaml")
if not rel:
continue
p = root / "datasets/dms" / rel
if not p.is_file():
errors.append(f"train_versions[{key}] data_yaml not found: {p}")
for proj, pcfg in (wf.get("projects") or {}).items():
for pack in pcfg.get("active_packs") or []:
if proj == "dms":
packs_file = root / pcfg.get("packs_registry", "datasets/dms/data_packs.yaml")
if packs_file.is_file():
packs = yaml.safe_load(packs_file.read_text(encoding="utf-8")) or {}
if pack not in (packs.get("packs") or {}):
errors.append(f"dms active_pack unknown in data_packs: {pack}")
if errors:
print("MANIFEST_ALIGNMENT_FAIL")
for e in errors:
print(" -", e)
sys.exit(1)
print("MANIFEST_ALIGNMENT_OK")
print("train_versions keys:", len([k for k in yaml.safe_load(tv_path.read_text()) if k != "schema"]))
print("yaml_active files:", len(list(yaml_active.glob("*.yaml"))))
PY
echo "OK smoke_manifest_alignment"

31
scripts/smoke_pending_gate.sh Executable file
View File

@@ -0,0 +1,31 @@
#!/usr/bin/env bash
# ML 自动化 P0manifest 对齐 + pending 批次 stage 字段可读
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
bash scripts/smoke_manifest_alignment.sh
python3 <<'PY'
import sys
from pathlib import Path
import yaml
root = Path(".")
wf = yaml.safe_load((root / "workflow.registry.yaml").read_text(encoding="utf-8"))
sys.path.insert(0, str(root / "platform"))
from as_platform.data.core import get_pending_report # noqa: E402
report = get_pending_report(wf)
stages = {b.get("stage") for b in report.get("batches") or []}
required = {"raw_pool", "out_for_labeling", "returned", "labeling_submitted"}
missing = required - stages
if missing:
print("PENDING_GATE_WARN: no batches in stages", missing, "(ok if inbox empty)")
else:
print("PENDING_GATE_STAGES_OK", sorted(stages))
print("PENDING_GATE_OK batches=", len(report.get("batches") or []))
PY
echo "OK smoke_pending_gate"

36
scripts/smoke_platform_api.sh Executable file
View File

@@ -0,0 +1,36 @@
#!/usr/bin/env bash
# 平台 API 冒烟labeling + fleet + health需 platform :8787
set -euo pipefail
BASE="${HSAP_API:-http://127.0.0.1:8787}"
TOKEN=$(curl -sS -X POST "$BASE/api/v1/auth/dev/login" -H 'Content-Type: application/json' -d '{"name":"smoke"}' | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
AUTH="Authorization: Bearer $TOKEN"
echo "==> health"
curl -sS -H "$AUTH" "$BASE/api/v1/health" | python3 -m json.tool
echo "==> labeling/batches (count)"
curl -sS -H "$AUTH" "$BASE/api/v1/labeling/batches" | python3 -c "import sys,json; print('batches', len(json.load(sys.stdin).get('items',[])))"
echo "==> fleet/map-config"
curl -sS -H "$AUTH" "$BASE/api/v1/fleet/map-config" 2>/dev/null | python3 -m json.tool || echo "fleet disabled or 503"
echo "==> fleet/live"
curl -sS -H "$AUTH" "$BASE/api/v1/fleet/live" 2>/dev/null | python3 -c "import sys,json; d=json.load(sys.stdin); print('vehicles', len(d.get('vehicles',[])))" || echo "fleet live skip"
CID=$(curl -sS -H "$AUTH" "$BASE/api/v1/labeling/batches" | python3 -c "
import sys,json
items=json.load(sys.stdin).get('items',[])
print(items[0].get('campaign_id','') if items else '')
")
if [[ -n "$CID" ]]; then
echo "==> export-jobs campaign=$CID"
curl -sS -H "$AUTH" "$BASE/api/v1/labeling/campaigns/$CID/export-jobs" | python3 -m json.tool | head -15
fi
bash "$(dirname "$0")/smoke_labeling_api.sh"
bash "$(dirname "$0")/smoke_manifest_alignment.sh"
echo "==> pending/gates"
curl -sS -H "$AUTH" "$BASE/api/v1/pending/gates" | python3 -m json.tool
echo "==> labeling/registry-profiles"
curl -sS -H "$AUTH" "$BASE/api/v1/labeling/registry-profiles" | python3 -c "import sys,json; print('profiles', len(json.load(sys.stdin).get('profiles',[])))"
echo "ALL_OK"

View File

@@ -27,7 +27,6 @@ EXCLUDE=(
--exclude 'log/'
--exclude 'tmp/'
--exclude 'node_modules/'
--exclude 'platform/web/dist/'
)
echo "HSAP: $AS"

View File

@@ -40,6 +40,15 @@ def main() -> None:
sys.exit(1)
return
if not redis_required:
if ping_redis():
print("Redis 已就绪")
else:
print("thread 模式Redis 未运行,跳过等待(标注锁/Job 队列功能受限)")
if not ok_db:
sys.exit(1)
return
for i in range(30):
if ping_redis():
print("Redis 已就绪")