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:
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user