feat: Unified Ingest SDK for DMS/ADAS promote, cuboid export and 3D fit

Replace subprocess build with promote_batch SDK, add ADAS cuboid export/fit/validate pipeline, stage normalization, and offline unit tests wired into smoke_labeling_api.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-16 09:58:35 +08:00
parent bc653d53a1
commit 0b8ade048e
42 changed files with 2074 additions and 104 deletions

View File

@@ -162,6 +162,12 @@ export const hsapApi = {
labelingExport: (campaignId: string) =>
postJson<{ ok: boolean; job?: { id: string } }>(`${API_BASE}/api/v1/labeling/campaigns/${campaignId}/export`),
labelingExportStats: (campaignId: string) =>
fetchJson<Record<string, unknown>>(`${API_BASE}/api/v1/labeling/campaigns/${campaignId}/export-stats`),
cuboidFit: (campaignId: string) =>
postJson<Record<string, unknown>>(`${API_BASE}/api/v1/labeling/campaigns/${campaignId}/cuboid-fit`),
submitLabelingCampaign: (campaignId: string) =>
postJson<Record<string, unknown>>(`${API_BASE}/api/v1/labeling/campaigns/${campaignId}/submit`),

View File

@@ -339,7 +339,13 @@ export const CampaignsPage: React.FC = () => {
</Link>
<button
onClick={() => handleExport(b.campaign_id!)}
className="inline-flex items-center gap-1 px-3 py-1.5 text-xs font-medium rounded-lg bg-gray-50 text-gray-600 hover:bg-gray-100 transition-colors"
disabled={!["labeling_submitted", "returned"].includes(b.stage || "")}
title={!["labeling_submitted", "returned"].includes(b.stage || "") ? "质检通过后才可导出" : undefined}
className={`inline-flex items-center gap-1 px-3 py-1.5 text-xs font-medium rounded-lg transition-colors ${
["labeling_submitted", "returned"].includes(b.stage || "")
? "bg-gray-50 text-gray-600 hover:bg-gray-100"
: "bg-gray-50 text-gray-300 cursor-not-allowed"
}`}
>
📤
</button>
@@ -347,7 +353,7 @@ export const CampaignsPage: React.FC = () => {
onClick={() => handleSubmit(b.campaign_id!)}
className="inline-flex items-center gap-1 px-3 py-1.5 text-xs font-medium rounded-lg bg-green-50 text-green-700 hover:bg-green-100 transition-colors"
>
</button>
<button
onClick={() => toggleExpand(b.campaign_id!)}

View File

@@ -10,7 +10,11 @@ export const ExportPage: React.FC = () => {
const [batches, setBatches] = useState<LabelingBatchRow[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [info, setInfo] = useState<string | null>(null);
const [importingId, setImportingId] = useState<string | null>(null);
const [statsMap, setStatsMap] = useState<Record<string, Record<string, unknown>>>({});
const [buildingId, setBuildingId] = useState<string | null>(null);
const [fittingId, setFittingId] = useState<string | null>(null);
const [search, setSearch] = useState("");
const [stageFilter, setStageFilter] = useState("");
@@ -33,6 +37,20 @@ export const ExportPage: React.FC = () => {
if (returned.status === "fulfilled") results.push(...((returned.value.items || []) as LabelingBatchRow[]));
if (submitted.status === "rejected" && returned.status === "rejected") setError(String(submitted.reason));
setBatches(results);
const adasReturned = results.filter((b) => b.stage === "returned" && b.project === "adas" && b.campaign_id);
if (adasReturned.length) {
const entries = await Promise.allSettled(
adasReturned.map(async (b) => {
const s = await hsapApi.labelingExportStats(b.campaign_id!);
return [b.campaign_id!, s] as const;
}),
);
const map: Record<string, Record<string, unknown>> = {};
for (const e of entries) {
if (e.status === "fulfilled") map[e.value[0]] = e.value[1];
}
setStatsMap(map);
}
} catch (e) { setError(String(e)); }
setLoading(false);
}, []);
@@ -40,10 +58,41 @@ export const ExportPage: React.FC = () => {
useEffect(() => { load(); }, [load]);
const handleExport = async (campaignId: string) => {
try { await hsapApi.labelingExport(campaignId); load(); }
try { await hsapApi.labelingExport(campaignId); setInfo("导出任务已提交"); load(); }
catch (e) { setError(String(e)); }
};
const handleCuboidFit = async (campaignId: string) => {
setFittingId(campaignId);
try {
await hsapApi.cuboidFit(campaignId);
setInfo("3D 拟合任务已提交");
load();
} catch (e) { setError(String(e)); }
setFittingId(null);
};
const handleSubmitBuild = async (b: LabelingBatchRow) => {
if (!b.task || !b.batch) return;
setBuildingId(b.campaign_id || b.batch);
setError(null);
try {
await hsapApi.submitBuildBatch({
project: b.project || "dms",
task: b.task,
batch: b.batch,
pack: b.pack || (b.project === "adas" ? "adas_moon3d_v1" : "dms_v2"),
location: b.location || "inbox",
note: `入库 ${b.batch}`,
});
setInfo("build 已提交至审核队列");
load();
} catch (e) {
setError(String(e));
}
setBuildingId(null);
};
const handleImportVendor = async (campaignId: string) => {
const input = document.createElement("input");
input.type = "file"; input.accept = ".zip";
@@ -64,9 +113,11 @@ export const ExportPage: React.FC = () => {
<div className="page-container">
<div className="page-header">
<h1></h1>
<p></p>
<p>build </p>
</div>
{info && <div className="bg-green-50 border border-green-200 rounded p-3 mb-4 text-sm text-green-700">{info}</div>}
<div className="bg-white rounded-xl border border-gray-200 p-3 mb-4">
<div className="flex items-center gap-3 flex-wrap">
<div className="flex-1 min-w-[200px] relative">
@@ -77,7 +128,7 @@ export const ExportPage: React.FC = () => {
placeholder="搜索批次/任务..." value={search} onChange={(e) => setSearch(e.target.value)} />
</div>
<div className="flex gap-1.5">
{["全部", "待导出", "待入库"].map((label, i) => {
{["全部", "待导出", "待 build"].map((label, i) => {
const val = i === 0 ? "" : ["labeling_submitted", "returned"][i - 1];
return <button key={val} onClick={() => setStageFilter(val)} className={`px-3 py-1.5 rounded-lg text-xs font-medium transition-colors ${stageFilter === val ? "bg-blue-600 text-white" : "bg-gray-100 text-gray-600 hover:bg-gray-200"}`}>{label}</button>;
})}
@@ -86,33 +137,29 @@ export const ExportPage: React.FC = () => {
</div>
</div>
{/* Workflow guide */}
<div className="card mb-4">
<div className="card-header"></div>
<div className="text-sm text-gray-600 space-y-2">
<div className="flex items-center gap-3">
<span className="w-8 h-8 rounded-full bg-blue-100 text-blue-700 flex items-center justify-center text-xs font-bold">1</span>
<span><strong></strong> Campaign "提交批次"</span>
<span><strong></strong> </span>
</div>
<div className="flex items-center gap-3">
<span className="w-8 h-8 rounded-full bg-blue-100 text-blue-700 flex items-center justify-center text-xs font-bold">2</span>
<span>
<strong></strong> "执行导出" YOLO
{hasData && <span className="text-gray-400"></span>}
</span>
<span><strong></strong> </span>
</div>
<div className="flex items-center gap-3">
<span className="w-8 h-8 rounded-full bg-orange-100 text-orange-700 flex items-center justify-center text-xs font-bold">3</span>
<span>
<strong></strong> "导入供应商" ZIP
<span className="block text-xs text-gray-400 mt-0.5">ZIP .txt YOLO </span>
<strong></strong> CVAT DMSYOLOADASquaternion_json
{hasData && <span className="text-gray-400"></span>}
</span>
</div>
<div className="flex items-center gap-3">
<span className="w-8 h-8 rounded-full bg-green-100 text-green-700 flex items-center justify-center text-xs font-bold">4</span>
<span>
<strong> build</strong> <Badge variant="success" size="small"></Badge> build
<span className="block text-xs text-gray-400 mt-0.5">build </span>
<strong> build</strong> <Badge variant="warning" size="small"> build</Badge> build <Badge variant="success" size="small"></Badge>
<span className="block text-xs text-gray-400 mt-0.5"> buildingested </span>
</span>
</div>
</div>
@@ -127,10 +174,17 @@ export const ExportPage: React.FC = () => {
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-semibold text-sm">{b.batch}</span>
<span className="text-xs text-gray-400">{b.task || "—"}</span>
<Badge variant={b.stage === "returned" ? "success" : "warning"}>{b.stage === "labeling_submitted" ? "待导出" : "待入库"}</Badge>
<span className="text-xs text-gray-400">{b.project}/{b.task || "—"}</span>
<Badge variant={b.stage === "returned" ? "warning" : "warning"}>{b.stage === "labeling_submitted" ? "待导出" : "待 build"}</Badge>
</div>
<div className="text-xs text-gray-400 font-mono mt-1">{b.campaign_id?.slice(0, 16) || "—"}</div>
{b.stage === "returned" && b.project === "adas" && b.campaign_id && statsMap[b.campaign_id] && (
<div className="text-xs text-gray-500 mt-1">
quaternion: {String(statsMap[b.campaign_id].quaternion_files ?? "—")} ·
fit_ok: {((Number(statsMap[b.campaign_id].fit_ok_ratio) || 0) * 100).toFixed(0)}% ·
pack: adas_moon3d_v1
</div>
)}
</div>
<div className="flex items-center gap-1.5 shrink-0">
{b.campaign_id && b.stage === "labeling_submitted" && (
@@ -139,7 +193,24 @@ export const ExportPage: React.FC = () => {
<Button size="small" variant="default" loading={importingId === b.campaign_id} onClick={() => handleImportVendor(b.campaign_id!)}>📥 </Button>
</>
)}
{b.stage === "returned" && <span className="text-green-600 text-sm font-medium"> </span>}
{b.stage === "returned" && (
<>
{b.project === "adas" && b.campaign_id && (
<Button size="small" variant="default" loading={fittingId === b.campaign_id} onClick={() => handleCuboidFit(b.campaign_id!)}>
3D
</Button>
)}
<Button
size="small"
variant="primary"
loading={buildingId === (b.campaign_id || b.batch)}
onClick={() => handleSubmitBuild(b)}
>
🏗 build
</Button>
<Link to="/system/audit" className="text-xs text-blue-600 hover:underline"> </Link>
</>
)}
</div>
</div>
</div>
@@ -147,8 +218,8 @@ export const ExportPage: React.FC = () => {
</div>
) : (
<div className="card text-center py-12">
<p className="text-gray-400 text-lg mb-3"></p>
<p className="text-gray-400 text-sm mb-4"></p>
<p className="text-gray-400 text-lg mb-3"> build </p>
<p className="text-gray-400 text-sm mb-4"></p>
<Link to="/labeling/campaigns"><Button variant="default" size="small"> </Button></Link>
</div>
)}

View File

@@ -33,7 +33,7 @@ const ReviewListPage: React.FC = () => {
const results: LabelingBatchRow[] = [];
const [inReview, approved, rejected] = await Promise.allSettled([
hsapApi.labelingBatches({ stage: "in_review", limit: 100 }),
hsapApi.labelingBatches({ stage: "review_approved", limit: 50 }),
hsapApi.labelingBatches({ stage: "labeling_submitted", limit: 50 }),
hsapApi.labelingBatches({ stage: "review_rejected", limit: 50 }),
]);
if (inReview.status === "fulfilled") results.push(...((inReview.value.items || []) as LabelingBatchRow[]));
@@ -80,7 +80,7 @@ const ReviewListPage: React.FC = () => {
{/* Filter chips */}
<div className="flex gap-1.5">
{["全部", "质检中", "已通过", "已退回"].map((label, i) => {
const val = i === 0 ? "" : ["in_review", "review_approved", "review_rejected"][i - 1];
const val = i === 0 ? "" : ["in_review", "labeling_submitted", "review_rejected"][i - 1];
return (
<button key={val} onClick={() => setStageFilter(val)}
className={`px-3 py-1.5 rounded-lg text-xs font-medium transition-colors ${
@@ -120,7 +120,9 @@ const ReviewListPage: React.FC = () => {
</div>
<div className="shrink-0">
{b.stage === "in_review" && <Link to={`/labeling/review/${b.campaign_id}`}><Button size="small" variant="primary"> </Button></Link>}
{b.stage === "review_approved" && <span className="text-green-600 text-sm font-medium"> </span>}
{b.stage === "labeling_submitted" && (
<Link to="/labeling/export"><Button size="small" variant="default"> </Button></Link>
)}
{b.stage === "review_rejected" && <span className="text-red-600 text-sm font-medium"> 退</span>}
</div>
</div>

View File

@@ -198,6 +198,12 @@ export const WorkbenchPage: React.FC = () => {
</Link>
)}
{b.stage === "returned" && (
<Link to="/labeling/export"
className="inline-flex items-center gap-1 px-3 py-1.5 text-xs font-medium rounded-lg bg-orange-50 text-orange-700 hover:bg-orange-100 transition-colors">
🏗 build
</Link>
)}
</div>
</div>
</div>