Clarify surgery result errors and expose offline batch timing.

Return specific codes when results are unavailable (not started vs in progress vs ended empty), block duplicate starts with SURGERY_ALREADY_RECORDING, and show text/video/total durations in the demo client.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Kevin
2026-05-22 14:01:25 +08:00
parent 704a83607d
commit d6f4590969
12 changed files with 374 additions and 16 deletions

View File

@@ -72,6 +72,83 @@
return String(detail || body);
}
const RESULT_UNAVAILABLE_LABELS = {
SURGERY_NOT_STARTED: "手术未开始",
SURGERY_STARTING: "手术启动中,算法尚未就绪",
SURGERY_IN_PROGRESS_NO_DETAILS: "手术进行中,尚无消耗明细",
SURGERY_ENDED_NO_CONSUMPTION: "手术已结束,无消耗明细",
SURGERY_ALREADY_RECORDING: "该手术已在录制中",
RESULT_NOT_READY: "结果尚不可查询",
};
function formatResultUnavailable(body) {
const detail = body?.detail;
if (detail && typeof detail === "object") {
const code = detail.code || "";
const label = RESULT_UNAVAILABLE_LABELS[code];
const msg = detail.message || "";
if (label && msg) return `${label}${msg}`;
if (label) return label;
if (msg) return msg;
}
return formatDetail(body);
}
function formatDurationSec(sec) {
if (sec == null || Number.isNaN(Number(sec))) return "—";
const n = Number(sec);
if (n < 60) return n.toFixed(2) + " 秒";
const m = Math.floor(n / 60);
const s = (n % 60).toFixed(1);
return m + " 分 " + s + " 秒";
}
function showOfflineBatchTiming(textSec, videoSec, totalSec, videoStatus) {
const el = $("offline-batch-timing");
if (!el) return;
if (textSec == null) {
el.textContent = "";
el.classList.add("hidden");
return;
}
let line = "耗时 · 文本结果:" + formatDurationSec(textSec);
if (videoStatus === "pending") {
line += ";标注视频:生成中…";
} else if (videoStatus === "ready" && videoSec != null) {
line += ";标注视频:" + formatDurationSec(videoSec);
} else if (videoStatus === "failed") {
line += ";标注视频:生成失败";
} else if (videoStatus === "skipped") {
line += ";标注视频:未生成";
}
line += ";合计:" + formatDurationSec(totalSec);
el.textContent = line;
el.classList.remove("hidden");
}
async function pollOfflineBatchTiming(sid, includeVis) {
if (!includeVis) return;
const maxAttempts = 120;
for (let i = 0; i < maxAttempts; i++) {
await sleep(2000);
try {
const { res, body } = await apiJson("GET", `/internal/demo/offline-batch/${sid}/timing`);
if (!res.ok || !body) continue;
showOfflineBatchTiming(
body.text_duration_sec,
body.video_duration_sec,
body.total_duration_sec,
body.video_status,
);
if (body.video_status === "ready" || body.video_status === "failed" || body.video_status === "skipped") {
return;
}
} catch {
/* ignore poll errors */
}
}
}
const logEl = $("log-scroll");
function addLog(method, url, status, body, { error = false, hint = "" } = {}) {
const item = document.createElement("div");
@@ -445,6 +522,19 @@
}
};
showBanner("标注视频已就绪", "ok");
try {
const { res: tr, body: tb } = await apiJson("GET", `/internal/demo/offline-batch/${sid}/timing`);
if (tr.ok && tb) {
showOfflineBatchTiming(
tb.text_duration_sec,
tb.video_duration_sec,
tb.total_duration_sec,
tb.video_status,
);
}
} catch {
/* ignore */
}
return;
}
if (attempt === 0) {
@@ -650,6 +740,16 @@
return;
}
lastVideoBatchDoctorDisplay = body?.doctor_display || "";
const includeVis = $("offline-batch-include-vis")?.checked;
showOfflineBatchTiming(
body.text_duration_sec,
body.video_duration_sec,
body.total_duration_sec,
includeVis ? "pending" : "skipped",
);
if (includeVis) {
void pollOfflineBatchTiming(sid, true);
}
if ($("offline-batch-include-vis")?.checked && body?.visualization_url) {
showVideoBatchVisualization(sid, body.visualization_url, lastVideoBatchDoctorDisplay);
} else {
@@ -694,7 +794,11 @@
"simulated-start",
);
if (!res.ok) {
showBanner("模拟开录失败:" + formatDetail(body), "err");
const dup = body?.detail?.code === "SURGERY_ALREADY_RECORDING";
showBanner(
dup ? "请勿重复开始:" + formatResultUnavailable(body) : "模拟开录失败:" + formatDetail(body),
"err",
);
return;
}
showBanner("模拟开录已接受,请打开语音终端", "ok");
@@ -715,7 +819,11 @@
candidate_consumables: candidateConsumables,
});
if (!res.ok) {
showBanner("开录失败:" + formatDetail(body), "err");
const dup = body?.detail?.code === "SURGERY_ALREADY_RECORDING";
showBanner(
dup ? "请勿重复开始:" + formatResultUnavailable(body) : "开录失败:" + formatDetail(body),
"err",
);
return;
}
showBanner("开录已接受,请打开语音终端", "ok");
@@ -756,7 +864,7 @@
if (!target) return;
target.innerHTML = "";
if (!res.ok || !body || typeof body !== "object") {
showBanner(res.ok ? "无结果数据" : "查询失败:" + formatDetail(body), "err");
showBanner(res.ok ? "无结果数据" : "查询失败:" + formatResultUnavailable(body), "err");
return;
}
const { details = [], summary = [] } = body;

View File

@@ -191,6 +191,7 @@
<section class="card">
<h2>结果</h2>
<p id="offline-batch-timing" class="labels-meta timing-meta hidden"></p>
<p id="video-batch-doctor-info" class="labels-meta"></p>
<div id="video-batch-vis" class="vis-block hidden">
<video id="video-batch-vis-player" controls playsinline></video>