const columns = [
"姓名",
"性别",
"年龄",
"住院号",
"诊断",
"入院时间",
"最后书写时间",
"住院天数",
"出院时间",
"手术后天数",
];
const state = {
items: [],
current: null,
auditItems: [],
auditCurrent: null,
auditHistory: [],
auditHistoryCurrent: null,
processingOverview: null,
permissions: {},
permissionLabels: {},
kimiEnabled: true,
activeView: "overview",
aiBusy: false,
aiTaskLabel: "",
imageZoom: 1,
imagePan: null,
auditImageZoom: 1,
auditImagePan: null,
auditHistoryImageZoom: 1,
auditHistoryImagePan: null,
page: 1,
pageSize: 60,
totalItems: 0,
loadingItems: false,
pgTimer: null,
debounce: null,
};
const datetimeColumns = new Set(["入院时间", "出院时间", "最后书写时间"]);
const $ = (selector) => document.querySelector(selector);
function permissionKeyForView(view) {
return view === "auditHistory" ? "audit_history" : view;
}
function permissionEntries() {
const labels = state.permissionLabels || {};
const fallback = {
overview: "概览",
review: "复核",
audit: "抽查",
audit_history: "抽查一览",
settings: "设置",
};
return Object.entries({ ...fallback, ...labels });
}
function badgeClass(item) {
if (item.manual_state === "AI修改-待确认") return "ai";
if (item.manual_state === "人工复核通过") return "done";
if (item.manual_state === "修订后仍需确认") return "warn";
return "pending";
}
function auditBadgeClass(item) {
if (item.audit_result === "人工核验异常") return "pending";
if (item.audit_result === "人工核验通过") return "done";
if (item.audit_result === "暂不确定") return "warn";
if (item.audit_machine_verdict === "异常") return "pending";
if (item.audit_machine_verdict) return "ai";
return "warn";
}
function auditBadgeText(item) {
if (item.audit_result) return item.audit_result;
if (item.audit_machine_verdict === "通过") return "AI核验通过";
if (item.audit_machine_verdict === "异常") return "AI核验异常";
if (item.audit_machine_verdict === "不确定") return "AI无法确定";
return "未核验";
}
function auditHistoryBadgeText(item) {
if (item.audit_result) return item.audit_result;
return item.audit_ai_feedback || item.audit_ai_raw_output || item.audit_machine_verdict ? "未人工核验" : "未填写人工结论";
}
function auditSourceLabel(value) {
const labels = {
manual_review_passed: "已处理-人工复核通过项",
db_auto_passed: "数据库-自动复核通过",
};
return labels[value] || value || "";
}
function validationBadgeHtml(item) {
const validationWarnings = validatePatientLocal(item.patient || {});
return validationWarnings.length
? `当前字段未通过校验`
: "";
}
function auditTitleHtml(item, badgeText = auditBadgeText(item)) {
return `
${escapeHtml(item.patient["姓名"] || "未识别姓名")} · ${escapeHtml(item.patient["住院号"] || "无住院号")}
${escapeHtml(badgeText)}
${validationBadgeHtml(item)}
`;
}
function auditQueueItemHtml(item, activeKey, extraRows = [], badgeText = auditBadgeText(item)) {
const metaRows = [
item.patient["住院号"] || "无住院号",
...extraRows,
].filter(Boolean);
return `
`;
}
function auditProgressText() {
const total = state.auditItems.length;
if (!total) return "";
const aiDone = state.auditItems.filter((item) => item.audit_machine_verdict || item.audit_ai_feedback).length;
const manualDone = state.auditItems.filter((item) => item.audit_result).length;
return `抽查进度:AI ${aiDone}/${total},人工 ${manualDone}/${total}`;
}
function setSaveState(text) {
$("#saveState").textContent = text || "";
}
function setStatusFilter(value, reload = true) {
if (value === "ai_pending" || value === "still_issue") value = "confirming";
$("#statusFilter").value = value;
document.querySelectorAll(".status-pill").forEach((button) => {
button.classList.toggle("active", button.dataset.status === value);
});
const more = $("#moreStatusFilter");
more.value = value === "pending" ? "" : value;
if (value === "done") {
$("#sortFilter").value = "updated_desc";
}
if (reload) loadItems(true);
}
function reviewConfirmCount(summary) {
return summary.state_counts?.["待确认"] ?? (
(summary.state_counts?.["AI修改-待确认"] || 0) + (summary.state_counts?.["修订后仍需确认"] || 0)
);
}
function setAiBusy(label, busy) {
state.aiBusy = busy;
state.aiTaskLabel = busy ? label : "";
document.body.classList.toggle("ai-busy", busy);
document.querySelectorAll("#kimiCurrentButton, #kimiFiveButton, #kimiRemainingButton, #auditKimiButton, #auditKimiAllButton").forEach((button) => {
button.disabled = busy;
});
}
function updateAiVisibility() {
document.body.classList.toggle("ai-disabled", !state.kimiEnabled);
document.querySelectorAll(".review-ai-panel, .audit-ai-panel").forEach((panel) => {
panel.classList.toggle("hidden", !state.kimiEnabled);
});
}
function ensureAiReady(label) {
if (state.aiBusy) {
setSaveState(`${state.aiTaskLabel || "AI处理"}中,请等待完成后再操作`);
window.alert(`${state.aiTaskLabel || "AI处理"}中,请等待完成后再切换页面或发起新的AI任务。`);
return false;
}
if (!state.kimiEnabled) {
setSaveState("目前无AI功能");
return false;
}
setAiBusy(label, true);
return true;
}
function finishAiTask() {
setAiBusy("", false);
}
function extractJsonData(text) {
const raw = String(text ?? "").trim();
if (!raw) return null;
const candidates = [];
const fencePattern = /```(?:json)?\s*([\s\S]*?)```/gi;
let fenced;
while ((fenced = fencePattern.exec(raw))) {
candidates.push(fenced[1].trim());
}
candidates.push(raw);
for (let index = 0; index < raw.length; index += 1) {
if (raw[index] === "{") candidates.push(raw.slice(index));
}
for (const candidate of candidates) {
try {
const data = JSON.parse(candidate);
if (data && typeof data === "object" && !Array.isArray(data)) return data;
} catch (error) {
const parsed = parseJsonPrefix(candidate);
if (parsed) return parsed;
}
}
return null;
}
function parseJsonPrefix(text) {
let depth = 0;
let inString = false;
let escaped = false;
for (let index = 0; index < text.length; index += 1) {
const char = text[index];
if (inString) {
if (escaped) {
escaped = false;
} else if (char === "\\") {
escaped = true;
} else if (char === '"') {
inString = false;
}
continue;
}
if (char === '"') {
inString = true;
} else if (char === "{") {
depth += 1;
} else if (char === "}") {
depth -= 1;
if (depth === 0) {
try {
const data = JSON.parse(text.slice(0, index + 1));
return data && typeof data === "object" && !Array.isArray(data) ? data : null;
} catch (error) {
return null;
}
}
}
}
return null;
}
function equivalentFieldValue(column, left, right) {
const leftText = normalizeFieldValue(column, left);
const rightText = normalizeFieldValue(column, right);
return leftText === rightText;
}
function cleanAuditJsonData(data, patient = null) {
if (!data) return null;
const cleaned = { ...data };
const patterns = [
"最后书写时间与出院时间顺序异常",
"最后书写时间晚于出院时间",
"出院后仍有书写记录",
"已出院后仍有书写记录",
"实际值一致",
"时间值一致",
"格式差异",
"补零差异",
"小时格式",
"日期格式补零",
"仅格式",
"无需修正",
];
if (cleaned.建议修正 && typeof cleaned.建议修正 === "object" && !Array.isArray(cleaned.建议修正) && patient) {
const keptSuggestions = {};
Object.entries(cleaned.建议修正).forEach(([column, value]) => {
if (columns.includes(column) && equivalentFieldValue(column, value, patient[column] || "")) return;
keptSuggestions[column] = value;
});
cleaned.建议修正 = keptSuggestions;
}
if (Array.isArray(cleaned.问题)) {
const kept = cleaned.问题.filter((issue) => !patterns.some((pattern) => String(issue).includes(pattern)));
cleaned.问题 = kept;
}
if ((!cleaned.问题 || !cleaned.问题.length) && (!cleaned.建议修正 || !Object.keys(cleaned.建议修正).length)) {
cleaned.结论 = "通过";
}
return cleaned;
}
function normalizeModelResultText(text, patient = null) {
const data = cleanAuditJsonData(extractJsonData(text), patient);
if (data) return JSON.stringify(data, null, 2);
return "";
}
function renderModelResult(element, jsonText, rawText = jsonText, patient = null) {
const normalized = normalizeModelResultText(jsonText || rawText, patient);
element.dataset.json = normalized;
element.dataset.raw = String(rawText ?? "");
const data = extractJsonData(normalized);
if (!String(jsonText || rawText || "").trim()) {
element.innerHTML = `
暂无AI输出
`;
return;
}
if (!data) {
element.innerHTML = `无法解析
`;
return;
}
const rows = Object.entries(data).map(([key, value]) => {
const display = typeof value === "string" ? value : JSON.stringify(value, null, 2);
return `| ${escapeHtml(key)} | ${escapeHtml(display)} |
`;
}).join("");
element.innerHTML = ``;
}
function setAuditModelResult(jsonText, rawText = jsonText, patient = state.auditCurrent?.patient || null) {
renderModelResult($("#auditModelResult"), jsonText, rawText, patient);
$("#auditRawOutput").textContent = String(rawText || jsonText || "");
const hasStructuredResult = Boolean($("#auditModelResult").dataset.json || String(jsonText || rawText || "").trim());
$("#auditPromptDetails").open = !hasStructuredResult;
$("#auditRawOutputDetails").open = !hasStructuredResult;
}
function setAuditPrompt(text) {
const value = String(text || "");
$("#auditPrompt").value = value;
$("#auditPromptText").textContent = value || "暂无询问提示词";
}
function getAuditModelResult() {
return $("#auditModelResult").dataset.json || "";
}
function getAuditRawOutput() {
return $("#auditModelResult").dataset.raw || "";
}
function setAuditHistoryModelResult(jsonText, rawText = jsonText, patient = state.auditHistoryCurrent?.patient || null) {
renderModelResult($("#auditHistoryAiResult"), jsonText, rawText, patient);
$("#auditHistoryRawOutput").textContent = String(rawText || jsonText || "");
}
function modelVerdict(rawText, patient = null) {
const data = cleanAuditJsonData(extractJsonData(rawText), patient);
const verdict = String(data?.结论 || data?.verdict || data?.result || "").trim();
return ["通过", "异常", "不确定"].includes(verdict) ? verdict : verdict;
}
function formatTime(value) {
if (!value) return "";
return String(value).replace("T", " ");
}
async function api(path, options = {}) {
let response;
try {
response = await fetch(path, {
headers: { "Content-Type": "application/json", ...(options.headers || {}) },
...options,
});
} catch (error) {
throw new Error("网络连接中断或网页端服务暂不可用,请确认服务仍在运行后重试");
}
if (response.status === 401) {
window.location.href = "/login";
return null;
}
if (!response.ok) {
const text = await response.text();
try {
const data = JSON.parse(text);
throw new Error(data.error || text || response.statusText);
} catch (error) {
if (error instanceof SyntaxError) throw new Error(text || response.statusText);
throw error;
}
}
return response.json();
}
async function loadSummary() {
const summary = await api("/api/summary");
if (!summary) return;
renderOverview(summary);
$("#statsGrid").innerHTML = `
${summary.total}复核条目
${summary.state_counts["待处理"]}待处理
${reviewConfirmCount(summary)}待确认
${summary.state_counts["人工复核通过"]}人工通过条目
`;
if (summary.postgres) {
renderPostgresState(summary.postgres);
}
const current = $("#batchFilter").value;
$("#batchFilter").innerHTML = `` + summary.batches
.map((batch) => ``)
.join("");
$("#batchFilter").value = current;
}
function renderOverview(summary) {
$("#overviewGrid").innerHTML = `
${summary.total}复核条目
${summary.state_counts["待处理"]}待处理
${reviewConfirmCount(summary)}待确认
${summary.state_counts["人工复核通过"]}人工通过
${summary.postgres?.pending_sync_count ?? 0}PG待同步
`;
$("#overviewBatches").innerHTML = summary.batches.map((batch) => `
${escapeHtml(batch.batch_name)}
${escapeHtml(batch.pending)}待处理
${escapeHtml(batch.still_issue)}待确认
${escapeHtml(batch.done)}人工通过
${escapeHtml(batch.total)}总条目
`).join("");
renderAuditOverview(summary.audit_summary || {});
}
function renderAuditOverview(auditSummary) {
const cards = [
["抽查记录", auditSummary.total || 0],
["未人工核验", auditSummary.unreviewed || 0],
["人工通过", auditSummary.passed || 0],
["人工异常", auditSummary.failed || 0],
["暂不确定", auditSummary.unsure || 0],
];
$("#overviewAudit").innerHTML = cards.map(([label, value]) => `
${escapeHtml(label)}
${escapeHtml(value)}${escapeHtml(label)}
`).join("");
}
function renderProcessingOverview(data) {
state.processingOverview = data;
const grid = $("#processingFinderGrid");
if (!grid) return;
const cards = [
["复核待处理", data.pending_review || 0],
["待确认", data.still_issue || 0],
["抽查未人工核验", data.audit_unreviewed || 0],
["抽查异常", data.audit_failed || 0],
["待同步", data.postgres_pending_sync || 0],
];
grid.innerHTML = cards.map(([label, value]) => `
${escapeHtml(value)}
${escapeHtml(label)}
`).join("");
}
async function findProcessingItems(quiet = false) {
const stateBox = $("#processingFinderState");
if (!quiet && stateBox) stateBox.textContent = "寻找中";
try {
const data = await api("/api/processing-items");
if (!data) return;
renderProcessingOverview(data);
if (stateBox) {
stateBox.textContent = `复核待处理${data.pending_review || 0},抽查未人工核验${data.audit_unreviewed || 0},待同步${data.postgres_pending_sync || 0}`;
}
} catch (error) {
if (stateBox) stateBox.textContent = `寻找失败:${error.message}`;
}
}
function escapeHtml(value) {
return String(value ?? "")
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
function itemSubtitle(item) {
const rowText = displayRowText(item);
return [
item.batch_name,
item.source_folder,
item.image_name ? `${item.image_name} ${rowText}` : "",
].filter(Boolean).join(" / ");
}
function displayRowNo(item) {
const row = Number(item.image_row_no || 0);
if (!row) return "";
return Math.max(1, row - 2);
}
function displayRowText(item) {
const row = Number(item.image_row_no || 0);
if (!row) return "";
const patientRow = displayRowNo(item);
return patientRow === row ? `第${row}行` : `第${patientRow}条(图片第${row}行)`;
}
async function loadItems(selectFirst = false, append = false) {
if (state.loadingItems) return;
state.loadingItems = true;
if (!append) {
state.page = 1;
}
const params = new URLSearchParams({
page: state.page,
page_size: state.pageSize,
status: $("#statusFilter").value,
sort: $("#sortFilter").value,
batch: $("#batchFilter").value,
q: $("#searchInput").value,
});
try {
const data = await api(`/api/items?${params.toString()}`);
if (!data) return;
state.totalItems = data.total;
state.page = data.page;
if (append) {
const seen = new Set(state.items.map((item) => item.key));
state.items = state.items.concat((data.items || []).filter((item) => !seen.has(item.key)));
} else {
state.items = data.items || [];
}
renderQueue();
if (selectFirst && state.items.length) {
selectItem(state.items[0].key);
} else if (state.current) {
highlightActive(state.current.key);
}
} catch (error) {
setSaveState(`列表加载失败:${error.message}`);
if (!append) {
$("#queueList").innerHTML = `${escapeHtml(error.message)}
`;
}
} finally {
state.loadingItems = false;
}
}
async function loadNextItems() {
if (state.loadingItems || state.items.length >= state.totalItems) return;
state.page += 1;
await loadItems(false, true);
}
async function reloadLoadedItems() {
const pageCount = Math.max(1, Math.ceil(Math.max(state.items.length, state.pageSize) / state.pageSize));
state.items = [];
state.totalItems = 0;
for (let page = 1; page <= pageCount; page += 1) {
state.page = page;
await loadItems(false, page > 1);
if (state.items.length >= state.totalItems) break;
}
}
function renderQueue() {
const list = $("#queueList");
if (!state.items.length) {
list.innerHTML = `当前筛选下没有记录
`;
return;
}
const footer = state.items.length < state.totalItems ? "继续向下滚动加载更多" : "已加载全部";
list.innerHTML = state.items.map((item) => `
`).join("") + `${state.items.length} / ${state.totalItems} · ${footer}
`;
list.querySelectorAll(".queue-item").forEach((button) => {
button.addEventListener("click", () => {
$("#queuePanel").focus({ preventScroll: true });
selectItem(button.dataset.key);
});
});
if (state.current) highlightActive(state.current.key);
}
function highlightActive(key) {
document.querySelectorAll(".queue-item").forEach((button) => {
const active = button.dataset.key === key;
button.classList.toggle("active", active);
if (active) {
button.scrollIntoView({ block: "nearest" });
}
});
}
async function selectItem(key) {
const item = await api(`/api/items/${key}`);
if (!item) return;
state.current = item;
highlightActive(key);
renderCurrent();
}
async function selectAdjacentItem(offset) {
if (!state.current) return;
let index = state.items.findIndex((item) => item.key === state.current.key);
if (index < 0) return;
let nextIndex = index + offset;
if (nextIndex >= state.items.length && state.items.length < state.totalItems) {
await loadNextItems();
index = state.items.findIndex((item) => item.key === state.current.key);
nextIndex = index + offset;
}
if (nextIndex < 0 || nextIndex >= state.items.length) return;
await selectItem(state.items[nextIndex].key);
}
function renderCurrent() {
const item = state.current;
if (!item) return;
$("#recordTitle").textContent = `${item.patient["姓名"] || "未识别姓名"} · ${item.patient["住院号"] || "无住院号"}`;
const handledAt = item.updated_at ? ` / 处理时间 ${formatTime(item.updated_at)}` : "";
$("#recordMeta").textContent = `${item.major_department} / ${item.sub_department} / ${item.source_folder} / ${item.image_name} ${displayRowText(item)}${handledAt}`;
const cropUrl = `/api/crop?path=${encodeURIComponent(item.image_path)}&row=${encodeURIComponent(item.image_row_no)}&context=2&v=${Date.now()}`;
$("#cropImage").src = cropUrl;
resetImageView();
$("#fullImageLink").href = `/api/image?path=${encodeURIComponent(item.image_path)}`;
columns.forEach((column) => {
const field = document.querySelector(`[name="${column}"]`);
if (field) field.value = item.patient[column] ?? "";
});
$("#manualNote").value = item.manual_note || "";
renderReviewChangeLog();
renderTips();
}
function renderTips() {
const item = state.current;
const currentTips = (item.validation_warnings || []).map((tip) => ({ text: `当前修订: ${tip}`, cls: "error" }));
const historyTips = (item.review_tips || []).map((tip) => ({ text: `原始OCR提示: ${tip}`, cls: "info" }));
if (!currentTips.length && !historyTips.length) {
$("#tipsBox").innerHTML = `当前字段通过校验
`;
return;
}
const currentHtml = currentTips.length
? currentTips.map((tip) => `${escapeHtml(tip.text)}
`).join("")
: `当前字段通过校验
`;
$("#tipsBox").innerHTML = currentHtml + historyTips.map((tip) => `${escapeHtml(tip.text)}
`).join("");
}
function formPatient(writeBack = false) {
const patient = {};
columns.forEach((column) => {
const field = document.querySelector(`[name="${column}"]`);
patient[column] = field ? normalizeFieldValue(column, field.value) : "";
if (writeBack && field && datetimeColumns.has(column)) field.value = patient[column];
});
return patient;
}
function normalizeDateTime(value) {
const text = String(value ?? "").replaceAll(":", ":").trim().replace(/\s+/g, " ");
if (!text) return "";
const match = text.match(/^(\d{4})-(\d{1,2})-(\d{1,2})\s+(\d{1,2}):(\d{1,2}):(\d{1,2})$/);
if (!match) return text;
const [, year, month, day, hour, minute, second] = match;
const date = new Date(Number(year), Number(month) - 1, Number(day), Number(hour), Number(minute), Number(second));
if (
date.getFullYear() !== Number(year) ||
date.getMonth() !== Number(month) - 1 ||
date.getDate() !== Number(day) ||
date.getHours() !== Number(hour) ||
date.getMinutes() !== Number(minute) ||
date.getSeconds() !== Number(second)
) {
return text;
}
const pad = (value) => String(value).padStart(2, "0");
return `${year}-${pad(month)}-${pad(day)} ${pad(hour)}:${pad(minute)}:${pad(second)}`;
}
function normalizeFieldValue(column, value) {
const text = String(value ?? "").replaceAll(":", ":").trim();
return datetimeColumns.has(column) ? normalizeDateTime(text) : text;
}
function patientFromEditor(selector) {
const patient = {};
document.querySelectorAll(`${selector} [data-patient-field]`).forEach((field) => {
patient[field.dataset.patientField] = normalizeFieldValue(field.dataset.patientField, field.value);
});
return patient;
}
function parseDateTime(value) {
const text = normalizeDateTime(value);
const match = text.match(/^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/);
if (!match) return null;
const [, year, month, day, hour, minute, second] = match.map(Number);
const date = new Date(year, month - 1, day, hour, minute, second);
if (
date.getFullYear() !== year ||
date.getMonth() !== month - 1 ||
date.getDate() !== day ||
date.getHours() !== hour ||
date.getMinutes() !== minute ||
date.getSeconds() !== second
) return null;
return date;
}
function validatePatientLocal(patient) {
const warnings = [];
if (!String(patient["姓名"] || "").trim()) warnings.push("缺少姓名");
if (!["男", "女"].includes(String(patient["性别"] || "").trim())) warnings.push("性别异常");
const age = String(patient["年龄"] || "").trim();
if (age && !/^\d{1,3}岁$/.test(age)) warnings.push("年龄格式异常");
if (!String(patient["住院号"] || "").trim()) warnings.push("缺少住院号");
const admission = String(patient["入院时间"] || "").trim();
const discharge = String(patient["出院时间"] || "").trim();
const lastWrite = String(patient["最后书写时间"] || "").trim();
const admissionTime = parseDateTime(admission);
const dischargeTime = parseDateTime(discharge);
const lastWriteTime = parseDateTime(lastWrite);
if (!admission) warnings.push("缺少入院时间");
else if (!admissionTime) warnings.push("入院时间格式异常");
if (discharge && !dischargeTime) warnings.push("出院时间格式异常");
if (lastWrite && !lastWriteTime) warnings.push("最后书写时间格式异常");
if (admissionTime && dischargeTime && dischargeTime < admissionTime) warnings.push("出院时间早于入院时间");
if (admissionTime && lastWriteTime && lastWriteTime < admissionTime) warnings.push("最后书写时间早于入院时间");
const hospitalDays = String(patient["住院天数"] || "").trim();
if (hospitalDays && !/^\d+$/.test(hospitalDays)) warnings.push("住院天数格式异常");
const postoperativeDays = String(patient["手术后天数"] || "").trim();
if (postoperativeDays && !/^后\d+天$/.test(postoperativeDays)) warnings.push("手术后天数格式异常");
return warnings;
}
function renderValidationBox(selector, warnings, prefix = "当前修订") {
const box = $(selector);
if (!box) return;
if (!warnings.length) {
box.innerHTML = `当前字段通过校验
`;
return;
}
box.innerHTML = warnings.map((warning) => `${escapeHtml(prefix)}: ${escapeHtml(warning)}
`).join("");
}
function updateAuditValidation(editorSelector, tipsSelector, targetItem = null) {
const patient = patientFromEditor(editorSelector);
const warnings = validatePatientLocal(patient);
if (targetItem) targetItem.validation_warnings = warnings;
renderValidationBox(tipsSelector, warnings);
return warnings;
}
function patientChanges(before = {}, after = {}) {
return columns
.map((column) => ({
column,
before: normalizeFieldValue(column, before[column] || ""),
after: normalizeFieldValue(column, after[column] || ""),
}))
.filter((item) => item.before !== item.after);
}
function displayChangeLogEntries(entries = []) {
if (!Array.isArray(entries)) return [];
return entries
.map((entry) => ({
column: entry.column || entry["字段"] || "",
before: normalizeFieldValue(entry.column || entry["字段"] || "", entry.before ?? entry["修改前"] ?? ""),
after: normalizeFieldValue(entry.column || entry["字段"] || "", entry.after ?? entry["修改后"] ?? ""),
source: entry.source || entry["修改者"] || "",
}))
.filter((entry) => columns.includes(entry.column));
}
function renderChangeLog(selector, item, title = "修改记录", sourceLabel = "", explicitChanges = null) {
const box = $(selector);
if (!box) return;
const changes = displayChangeLogEntries(explicitChanges || item.change_log || [])
.concat(explicitChanges || item.change_log ? [] : patientChanges(item.audit_original_patient || item.original_patient || {}, item.patient || {}));
if (!changes.length && !item.audit_change_summary) {
box.innerHTML = "";
return;
}
const showSource = changes.some((change) => change.source);
const rows = changes.length
? changes.map((change) => `
| ${escapeHtml(change.column)} |
${escapeHtml(change.before || "空")} |
${escapeHtml(change.after || "空")} |
${showSource ? `${escapeHtml(change.source || sourceLabel || "人工")} | ` : ""}
`).join("")
: `| ${escapeHtml(item.audit_change_summary || "")} |
`;
box.innerHTML = `
${escapeHtml(title)}
| 字段 | 修改前 | 修改后 | ${showSource ? "修改者 | " : ""}
${rows}
`;
}
function reviewPatientChangesWithSource(draftPatient) {
const currentPatient = state.current?.patient || {};
const savedLog = displayChangeLogEntries(state.current?.change_log || []);
const pendingManualChanges = patientChanges(currentPatient, draftPatient).map((change) => ({ ...change, source: "人工" }));
if (savedLog.length || pendingManualChanges.length) return savedLog.concat(pendingManualChanges);
const original = state.current?.original_patient || {};
const source = state.current?.ai_corrected ? "AI" : "人工";
return patientChanges(original, draftPatient).map((change) => ({ ...change, source }));
}
function renderReviewChangeLog() {
if (!state.current) {
$("#reviewChangeLog").innerHTML = "";
return;
}
const draft = {
...state.current,
patient: formPatient(),
};
const changes = reviewPatientChangesWithSource(draft.patient);
renderChangeLog("#reviewChangeLog", draft, "修改记录", "", changes);
}
function replaceFullwidthColon(field) {
if (!field.value.includes(":")) return;
const start = field.selectionStart;
const end = field.selectionEnd;
field.value = field.value.replaceAll(":", ":");
if (typeof start === "number" && typeof end === "number") {
field.setSelectionRange(start, end);
}
}
function formOptions() {
return {};
}
async function validateForm() {
if (!state.current) return;
const data = await api(`/api/items/${state.current.key}/validate`, {
method: "POST",
body: JSON.stringify({ patient: formPatient(), options: formOptions() }),
});
if (!data) return;
if (data.patient) fillPatientFields(data.patient);
if (state.current) state.current.validation_warnings = data.warnings || [];
renderTips();
renderReviewChangeLog();
return data.warnings || [];
}
function updateLocalReviewValidation() {
if (!state.current) return;
state.current.validation_warnings = validatePatientLocal(formPatient(false));
renderTips();
renderReviewChangeLog();
}
function setImageZoom(value, anchorEvent = null) {
const frame = $("#cropFrame");
const image = $("#cropImage");
const previousZoom = state.imageZoom;
const nextZoom = Math.min(3, Math.max(0.4, value));
if (!image.naturalWidth) {
state.imageZoom = nextZoom;
$("#zoomLabel").textContent = `${Math.round(state.imageZoom * 100)}%`;
return;
}
const rect = frame.getBoundingClientRect();
const anchorX = anchorEvent ? anchorEvent.clientX - rect.left : frame.clientWidth / 2;
const anchorY = anchorEvent ? anchorEvent.clientY - rect.top : frame.clientHeight / 2;
const scrollX = frame.scrollLeft + anchorX;
const scrollY = frame.scrollTop + anchorY;
const ratio = nextZoom / previousZoom;
state.imageZoom = nextZoom;
image.style.width = `${Math.round(image.naturalWidth * state.imageZoom)}px`;
$("#zoomLabel").textContent = `${Math.round(state.imageZoom * 100)}%`;
requestAnimationFrame(() => {
frame.scrollLeft = scrollX * ratio - anchorX;
frame.scrollTop = scrollY * ratio - anchorY;
});
}
function resetImageView() {
const frame = $("#cropFrame");
state.imageZoom = 1;
$("#cropImage").style.width = "";
$("#zoomLabel").textContent = "100%";
requestAnimationFrame(() => {
frame.scrollLeft = 0;
frame.scrollTop = 0;
});
}
function startImagePan(event) {
if (event.button !== 0) return;
const frame = $("#cropFrame");
state.imagePan = {
pointerId: event.pointerId,
startX: event.clientX,
startY: event.clientY,
scrollLeft: frame.scrollLeft,
scrollTop: frame.scrollTop,
};
frame.classList.add("is-dragging");
frame.setPointerCapture(event.pointerId);
}
function moveImagePan(event) {
const pan = state.imagePan;
if (!pan || pan.pointerId !== event.pointerId) return;
event.preventDefault();
const frame = $("#cropFrame");
frame.scrollLeft = pan.scrollLeft - (event.clientX - pan.startX);
frame.scrollTop = pan.scrollTop - (event.clientY - pan.startY);
}
function endImagePan(event) {
const pan = state.imagePan;
if (!pan || pan.pointerId !== event.pointerId) return;
const frame = $("#cropFrame");
state.imagePan = null;
frame.classList.remove("is-dragging");
if (frame.hasPointerCapture(event.pointerId)) {
frame.releasePointerCapture(event.pointerId);
}
}
function setAuditImageZoom(value, anchorEvent = null) {
const frame = $("#auditFrame");
const image = $("#auditImage");
const previousZoom = state.auditImageZoom;
const nextZoom = Math.min(3, Math.max(0.4, value));
if (!image.naturalWidth) {
state.auditImageZoom = nextZoom;
$("#auditZoomLabel").textContent = `${Math.round(state.auditImageZoom * 100)}%`;
return;
}
const rect = frame.getBoundingClientRect();
const anchorX = anchorEvent ? anchorEvent.clientX - rect.left : frame.clientWidth / 2;
const anchorY = anchorEvent ? anchorEvent.clientY - rect.top : frame.clientHeight / 2;
const scrollX = frame.scrollLeft + anchorX;
const scrollY = frame.scrollTop + anchorY;
const ratio = nextZoom / previousZoom;
state.auditImageZoom = nextZoom;
image.style.width = `${Math.round(image.naturalWidth * state.auditImageZoom)}px`;
$("#auditZoomLabel").textContent = `${Math.round(state.auditImageZoom * 100)}%`;
requestAnimationFrame(() => {
frame.scrollLeft = scrollX * ratio - anchorX;
frame.scrollTop = scrollY * ratio - anchorY;
});
}
function resetAuditImageView() {
const frame = $("#auditFrame");
state.auditImageZoom = 1;
$("#auditImage").style.width = "";
$("#auditZoomLabel").textContent = "100%";
requestAnimationFrame(() => {
frame.scrollLeft = Math.max(0, (frame.scrollWidth - frame.clientWidth) / 2);
frame.scrollTop = Math.max(0, (frame.scrollHeight - frame.clientHeight) / 2);
});
}
function startAuditImagePan(event) {
if (event.button !== 0) return;
const frame = $("#auditFrame");
state.auditImagePan = {
pointerId: event.pointerId,
startX: event.clientX,
startY: event.clientY,
scrollLeft: frame.scrollLeft,
scrollTop: frame.scrollTop,
};
frame.classList.add("is-dragging");
frame.setPointerCapture(event.pointerId);
}
function moveAuditImagePan(event) {
const pan = state.auditImagePan;
if (!pan || pan.pointerId !== event.pointerId) return;
event.preventDefault();
const frame = $("#auditFrame");
frame.scrollLeft = pan.scrollLeft - (event.clientX - pan.startX);
frame.scrollTop = pan.scrollTop - (event.clientY - pan.startY);
}
function endAuditImagePan(event) {
const pan = state.auditImagePan;
if (!pan || pan.pointerId !== event.pointerId) return;
const frame = $("#auditFrame");
state.auditImagePan = null;
frame.classList.remove("is-dragging");
if (frame.hasPointerCapture(event.pointerId)) {
frame.releasePointerCapture(event.pointerId);
}
}
function setAuditHistoryImageZoom(value, anchorEvent = null) {
const frame = $("#auditHistoryFrame");
const image = $("#auditHistoryImage");
const previousZoom = state.auditHistoryImageZoom;
const nextZoom = Math.min(3, Math.max(0.4, value));
if (!image.naturalWidth) {
state.auditHistoryImageZoom = nextZoom;
$("#auditHistoryZoomLabel").textContent = `${Math.round(state.auditHistoryImageZoom * 100)}%`;
return;
}
const rect = frame.getBoundingClientRect();
const anchorX = anchorEvent ? anchorEvent.clientX - rect.left : frame.clientWidth / 2;
const anchorY = anchorEvent ? anchorEvent.clientY - rect.top : frame.clientHeight / 2;
const scrollX = frame.scrollLeft + anchorX;
const scrollY = frame.scrollTop + anchorY;
const ratio = nextZoom / previousZoom;
state.auditHistoryImageZoom = nextZoom;
image.style.width = `${Math.round(image.naturalWidth * state.auditHistoryImageZoom)}px`;
$("#auditHistoryZoomLabel").textContent = `${Math.round(state.auditHistoryImageZoom * 100)}%`;
requestAnimationFrame(() => {
frame.scrollLeft = scrollX * ratio - anchorX;
frame.scrollTop = scrollY * ratio - anchorY;
});
}
function resetAuditHistoryImageView() {
const frame = $("#auditHistoryFrame");
state.auditHistoryImageZoom = 1;
$("#auditHistoryImage").style.width = "";
$("#auditHistoryZoomLabel").textContent = "100%";
requestAnimationFrame(() => {
frame.scrollLeft = Math.max(0, (frame.scrollWidth - frame.clientWidth) / 2);
frame.scrollTop = Math.max(0, (frame.scrollHeight - frame.clientHeight) / 2);
});
}
function startAuditHistoryImagePan(event) {
if (event.button !== 0) return;
const frame = $("#auditHistoryFrame");
state.auditHistoryImagePan = {
pointerId: event.pointerId,
startX: event.clientX,
startY: event.clientY,
scrollLeft: frame.scrollLeft,
scrollTop: frame.scrollTop,
};
frame.classList.add("is-dragging");
frame.setPointerCapture(event.pointerId);
}
function moveAuditHistoryImagePan(event) {
const pan = state.auditHistoryImagePan;
if (!pan || pan.pointerId !== event.pointerId) return;
event.preventDefault();
const frame = $("#auditHistoryFrame");
frame.scrollLeft = pan.scrollLeft - (event.clientX - pan.startX);
frame.scrollTop = pan.scrollTop - (event.clientY - pan.startY);
}
function endAuditHistoryImagePan(event) {
const pan = state.auditHistoryImagePan;
if (!pan || pan.pointerId !== event.pointerId) return;
const frame = $("#auditHistoryFrame");
state.auditHistoryImagePan = null;
frame.classList.remove("is-dragging");
if (frame.hasPointerCapture(event.pointerId)) {
frame.releasePointerCapture(event.pointerId);
}
}
function switchView(view) {
if (state.permissions && state.permissions[permissionKeyForView(view)] === false) return;
if (state.aiBusy && view !== state.activeView) {
window.alert(`${state.aiTaskLabel || "AI处理"}中,请保持当前页面打开,完成后再切换到其他页面。`);
setSaveState(`${state.aiTaskLabel || "AI处理"}中,请保持当前页面打开`);
return;
}
document.querySelectorAll(".page-view").forEach((panel) => {
panel.classList.toggle("hidden", panel.id !== `${view}View`);
});
document.querySelectorAll(".nav-button").forEach((button) => {
button.classList.toggle("active", button.dataset.view === view);
});
state.activeView = view;
if (view === "review" && !state.items.length) loadItems(true);
if (view === "auditHistory") loadAuditHistory();
if (view === "settings") loadSettings();
}
async function loadSession() {
const data = await api("/api/session");
if (!data) return;
state.permissions = data.permissions || {};
state.permissionLabels = data.permission_labels || {};
state.kimiEnabled = data.kimi_enabled !== false;
updateAiVisibility();
document.querySelectorAll(".nav-button").forEach((button) => {
const view = button.dataset.view;
button.classList.toggle("hidden", state.permissions[permissionKeyForView(view)] === false);
});
const active = document.querySelector(".nav-button.active:not(.hidden)");
if (!active) {
const first = document.querySelector(".nav-button:not(.hidden)");
if (first) switchView(first.dataset.view);
}
}
async function saveCorrection(event) {
event.preventDefault();
if (!state.current) return;
const currentKey = state.current.key;
const wasAiPending = state.current.manual_state === "AI修改-待确认";
const currentIndex = state.items.findIndex((item) => item.key === currentKey);
const nextKey = state.items[currentIndex + 1]?.key || state.items[currentIndex - 1]?.key || "";
const precheckWarnings = await validateForm();
if (precheckWarnings && precheckWarnings.length) {
const message = `当前修订仍有 ${precheckWarnings.length} 个提示:\n\n${precheckWarnings.join("\n")}\n\n仍然保存吗?`;
if (!window.confirm(message)) {
setSaveState("已取消保存");
return;
}
}
setSaveState("保存中");
const result = await api(`/api/items/${state.current.key}/correction`, {
method: "POST",
body: JSON.stringify({
patient: formPatient(),
options: formOptions(),
manual_note: $("#manualNote").value.trim(),
}),
});
if (!result) return;
if (result.postgres?.error) {
window.alert(`修订已保存到本地,但同步 PostgreSQL 失败,已纳入待同步:\n${result.postgres.error}`);
} else if (result.postgres?.enabled && result.postgres.updated === 0) {
window.alert("修订已保存到本地,但 PostgreSQL 暂未找到匹配记录,已纳入待同步。");
}
await loadSummary();
if (wasAiPending) {
setStatusFilter("confirming", false);
}
await reloadLoadedItems();
if (!result.warnings.length && state.items.length) {
const fallbackIndex = currentIndex >= 0 ? Math.min(currentIndex, state.items.length - 1) : 0;
const targetKey = state.items.some((item) => item.key === nextKey) ? nextKey : state.items[fallbackIndex].key;
await selectItem(targetKey);
} else {
state.current = result.item;
renderCurrent();
highlightActive(result.item.key);
}
setSaveState(result.warnings.length ? "已保存,仍需确认" : "已保存");
}
async function deleteCorrection() {
if (!state.current) return;
setSaveState("删除中");
const result = await api(`/api/items/${state.current.key}/correction`, { method: "DELETE" });
if (result?.postgres?.error) {
window.alert(`本地修订已删除,但同步 PostgreSQL 失败:\n${result.postgres.error}`);
}
const item = await api(`/api/items/${state.current.key}`);
state.current = item;
renderCurrent();
await loadSummary();
await reloadLoadedItems();
setSaveState("已删除修订");
}
function resetToOriginal() {
if (!state.current) return;
columns.forEach((column) => {
const field = document.querySelector(`[name="${column}"]`);
if (field) field.value = state.current.original_patient[column] ?? "";
});
$("#manualNote").value = "";
renderReviewChangeLog();
validateForm();
}
function fillPatientFields(patient) {
columns.forEach((column) => {
const field = document.querySelector(`[name="${column}"]`);
if (field && Object.prototype.hasOwnProperty.call(patient, column)) {
field.value = patient[column] ?? "";
}
});
}
function bindEvents() {
document.querySelectorAll(".nav-button").forEach((button) => {
button.addEventListener("click", () => switchView(button.dataset.view));
});
$("#batchFilter").addEventListener("change", () => loadItems(true));
document.querySelectorAll(".status-pill").forEach((button) => {
button.addEventListener("click", () => setStatusFilter(button.dataset.status));
});
$("#moreStatusFilter").addEventListener("change", () => {
if ($("#moreStatusFilter").value) {
setStatusFilter($("#moreStatusFilter").value);
}
});
$("#sortFilter").addEventListener("change", () => loadItems(true));
$("#queuePanel").addEventListener("scroll", () => {
const panel = $("#queuePanel");
if (panel.scrollTop + panel.clientHeight >= panel.scrollHeight - 180) {
loadNextItems();
}
});
$("#queuePanel").addEventListener("keydown", (event) => {
if (event.key === "ArrowDown") {
event.preventDefault();
selectAdjacentItem(1);
} else if (event.key === "ArrowUp") {
event.preventDefault();
selectAdjacentItem(-1);
}
});
$("#pgTestButton").addEventListener("click", () => testPostgres(false));
$("#pgSyncButton").addEventListener("click", syncPostgres);
$("#searchInput").addEventListener("input", () => {
clearTimeout(state.debounce);
state.debounce = setTimeout(() => loadItems(true), 220);
});
$("#editForm").addEventListener("submit", saveCorrection);
$("#deleteButton").addEventListener("click", deleteCorrection);
$("#resetButton").addEventListener("click", resetToOriginal);
$("#zoomInButton").addEventListener("click", () => setImageZoom(state.imageZoom + 0.15));
$("#zoomOutButton").addEventListener("click", () => setImageZoom(state.imageZoom - 0.15));
$("#zoomResetButton").addEventListener("click", resetImageView);
$("#cropFrame").addEventListener("wheel", (event) => {
event.preventDefault();
setImageZoom(state.imageZoom + (event.deltaY < 0 ? 0.12 : -0.12), event);
}, { passive: false });
$("#cropFrame").addEventListener("pointerdown", startImagePan);
$("#cropFrame").addEventListener("pointermove", moveImagePan);
$("#cropFrame").addEventListener("pointerup", endImagePan);
$("#cropFrame").addEventListener("pointercancel", endImagePan);
$("#cropImage").addEventListener("load", resetImageView);
$("#cropImage").addEventListener("error", () => {
$("#tipsBox").innerHTML = `截图加载失败,请检查原图路径或点击“原图”查看。
`;
});
$("#auditZoomInButton").addEventListener("click", () => setAuditImageZoom(state.auditImageZoom + 0.15));
$("#auditZoomOutButton").addEventListener("click", () => setAuditImageZoom(state.auditImageZoom - 0.15));
$("#auditZoomResetButton").addEventListener("click", resetAuditImageView);
$("#auditFrame").addEventListener("wheel", (event) => {
event.preventDefault();
setAuditImageZoom(state.auditImageZoom + (event.deltaY < 0 ? 0.12 : -0.12), event);
}, { passive: false });
$("#auditFrame").addEventListener("pointerdown", startAuditImagePan);
$("#auditFrame").addEventListener("pointermove", moveAuditImagePan);
$("#auditFrame").addEventListener("pointerup", endAuditImagePan);
$("#auditFrame").addEventListener("pointercancel", endAuditImagePan);
$("#auditImage").addEventListener("load", resetAuditImageView);
$("#auditImage").addEventListener("error", () => {
$("#auditState").textContent = "截图加载失败,请检查原图路径";
});
$("#auditSampleButton").addEventListener("click", loadAuditSample);
$("#auditKimiButton").addEventListener("click", () => runKimiAudit());
$("#auditKimiAllButton").addEventListener("click", runKimiAuditAll);
$("#auditHistoryRefreshButton").addEventListener("click", loadAuditHistory);
$("#auditHistorySourceFilter").addEventListener("change", loadAuditHistory);
$("#auditHistoryStatusFilter").addEventListener("change", loadAuditHistory);
$("#auditHistorySortFilter").addEventListener("change", loadAuditHistory);
$("#auditHistorySearchInput").addEventListener("input", () => {
clearTimeout(state.debounce);
state.debounce = setTimeout(loadAuditHistory, 220);
});
$("#auditHistoryZoomInButton").addEventListener("click", () => setAuditHistoryImageZoom(state.auditHistoryImageZoom + 0.15));
$("#auditHistoryZoomOutButton").addEventListener("click", () => setAuditHistoryImageZoom(state.auditHistoryImageZoom - 0.15));
$("#auditHistoryZoomResetButton").addEventListener("click", resetAuditHistoryImageView);
$("#auditHistoryFrame").addEventListener("wheel", (event) => {
event.preventDefault();
setAuditHistoryImageZoom(state.auditHistoryImageZoom + (event.deltaY < 0 ? 0.12 : -0.12), event);
}, { passive: false });
$("#auditHistoryFrame").addEventListener("pointerdown", startAuditHistoryImagePan);
$("#auditHistoryFrame").addEventListener("pointermove", moveAuditHistoryImagePan);
$("#auditHistoryFrame").addEventListener("pointerup", endAuditHistoryImagePan);
$("#auditHistoryFrame").addEventListener("pointercancel", endAuditHistoryImagePan);
$("#auditHistoryImage").addEventListener("load", resetAuditHistoryImageView);
$("#auditHistoryImage").addEventListener("error", () => {
$("#auditHistoryState").textContent = "截图加载失败,请检查原图路径";
});
$("#auditPassButton").addEventListener("click", () => saveAuditResult("人工核验通过"));
$("#auditFailButton").addEventListener("click", () => saveAuditResult("人工核验异常"));
$("#auditUnsureButton").addEventListener("click", () => saveAuditResult("暂不确定"));
$("#auditHistoryPassButton").addEventListener("click", () => saveAuditHistoryResult("人工核验通过"));
$("#auditHistoryFailButton").addEventListener("click", () => saveAuditHistoryResult("人工核验异常"));
$("#auditHistoryUnsureButton").addEventListener("click", () => saveAuditHistoryResult("暂不确定"));
$("#userForm").addEventListener("submit", saveUser);
$("#kimiForm").addEventListener("submit", saveKimiSettings);
$("#kimiToggleButton").addEventListener("click", toggleKimiEnabled);
$("#processingFindButton").addEventListener("click", () => findProcessingItems(false));
$("#commitManualPassedButton").addEventListener("click", commitManualPassed);
$("#resetAuditHistoryButton").addEventListener("click", resetAuditHistory);
$("#kimiCurrentButton").addEventListener("click", kimiCorrectCurrent);
$("#kimiFiveButton").addEventListener("click", kimiCorrectFive);
$("#kimiRemainingButton").addEventListener("click", kimiCorrectRemaining);
document.querySelectorAll(".edit-form input, .edit-form textarea").forEach((field) => {
field.addEventListener("input", () => {
const column = field.getAttribute("name");
if (datetimeColumns.has(column)) {
replaceFullwidthColon(field);
}
updateLocalReviewValidation();
});
field.addEventListener("keydown", (event) => {
const column = field.getAttribute("name");
if (event.key === "Enter" && datetimeColumns.has(column)) {
event.preventDefault();
field.value = normalizeDateTime(field.value);
validateForm();
}
});
field.addEventListener("blur", () => {
const column = field.getAttribute("name");
if (datetimeColumns.has(column)) {
field.value = normalizeDateTime(field.value);
validateForm();
} else {
updateLocalReviewValidation();
}
});
});
}
async function loadAuditSample() {
$("#auditState").textContent = "抽取中";
const count = Math.min(20, Math.max(1, Number($("#auditCount").value || 5)));
$("#auditCount").value = count;
const source = $("#auditSource").value;
try {
const data = await api("/api/audit/sample", {
method: "POST",
body: JSON.stringify({ count, source }),
});
if (!data) return;
state.auditItems = data.items || [];
renderAuditList();
if (state.auditItems.length) selectAuditItem(state.auditItems[0].key);
$("#auditState").textContent = `已抽取 ${state.auditItems.length} 条,${auditProgressText()}`;
} catch (error) {
$("#auditState").textContent = `抽取失败:${error.message}`;
}
}
function renderAuditList() {
$("#auditList").innerHTML = state.auditItems
.map((item) => auditQueueItemHtml(item, state.auditCurrent?.key, item.audit_checked_at ? [`抽查时间 ${formatTime(item.audit_checked_at)}`] : []))
.join("");
$("#auditList").querySelectorAll(".queue-item").forEach((button) => {
button.addEventListener("click", () => selectAuditItem(button.dataset.key));
});
}
function selectAuditItem(key) {
const item = state.auditItems.find((candidate) => candidate.key === key);
if (!item) return;
if (!item.audit_original_patient) {
item.audit_original_patient = { ...(item.patient || {}) };
}
state.auditCurrent = item;
$("#auditTitle").innerHTML = auditTitleHtml(item);
$("#auditMeta").textContent = `${item.major_department} / ${item.sub_department} / ${item.source_folder} / ${item.image_name} ${displayRowText(item)}`;
$("#auditImage").src = `/api/crop?path=${encodeURIComponent(item.image_path)}&row=${encodeURIComponent(item.image_row_no)}&context=2&v=${Date.now()}`;
$("#auditFullImageLink").href = `/api/image?path=${encodeURIComponent(item.image_path)}`;
resetAuditImageView();
setAuditPrompt(item.audit_prompt || "");
setAuditModelResult(item.audit_ai_feedback || item.audit_result_text || "", item.audit_ai_raw_output || item.audit_ai_feedback || item.audit_result_text || "", item.patient || null);
$("#auditMachineVerdict").value = item.audit_machine_verdict || "";
$("#auditManualFeedback").value = item.audit_manual_feedback || "";
renderAuditPatientInfo(item);
renderAuditVerdicts(item);
updateAuditValidation("#auditPatientInfo", "#auditTipsBox", item);
renderChangeLog("#auditChangeLog", item);
renderAuditList();
}
function renderAuditPatientInfo(item) {
renderPatientEditor("#auditPatientInfo", item, "#auditTipsBox", "#auditChangeLog");
}
function renderPatientEditor(selector, item, tipsSelector = "", changeLogSelector = "") {
const wideColumns = new Set(["诊断", "手术后天数"]);
$(selector).innerHTML = columns.map((column) => {
const value = escapeHtml(item.patient[column] || "");
const wide = wideColumns.has(column) ? "wide" : "";
const control = column === "诊断"
? ``
: ``;
return ``;
}).join("");
document.querySelectorAll(`${selector} [data-patient-field]`).forEach((field) => {
field.addEventListener("input", () => {
const column = field.dataset.patientField;
if (datetimeColumns.has(column)) replaceFullwidthColon(field);
readPatientEditor(selector, item, false);
if (tipsSelector) updateAuditValidation(selector, tipsSelector, item);
if (changeLogSelector) renderChangeLog(changeLogSelector, item);
refreshAuditIdentity(selector, item);
});
field.addEventListener("blur", () => {
const column = field.dataset.patientField;
field.value = normalizeFieldValue(column, field.value);
readPatientEditor(selector, item);
if (tipsSelector) updateAuditValidation(selector, tipsSelector, item);
if (changeLogSelector) renderChangeLog(changeLogSelector, item);
refreshAuditIdentity(selector, item);
});
});
}
function refreshAuditIdentity(selector, item) {
if (selector === "#auditPatientInfo") {
$("#auditTitle").innerHTML = auditTitleHtml(item);
renderAuditList();
} else if (selector === "#auditHistoryPatientInfo") {
$("#auditHistoryTitle").innerHTML = auditTitleHtml(item, auditHistoryBadgeText(item));
renderAuditHistoryList();
}
}
function readPatientEditor(selector, targetItem, writeBack = true) {
const patient = { ...(targetItem.patient || {}) };
document.querySelectorAll(`${selector} [data-patient-field]`).forEach((field) => {
const column = field.dataset.patientField;
patient[column] = normalizeFieldValue(column, field.value);
if (writeBack) field.value = patient[column];
});
targetItem.patient = patient;
return patient;
}
function renderAuditVerdicts(item) {
$("#auditMachineVerdictText").textContent = item.audit_machine_verdict || "未判断";
$("#auditManualVerdictText").textContent = item.audit_result || "未选择";
}
async function runKimiAudit(options = {}) {
if (!state.auditCurrent) return;
if (!ensureAiReady("AI抽查")) return;
readPatientEditor("#auditPatientInfo", state.auditCurrent);
setSaveState("AI抽查中,请保持当前页面打开");
$("#auditState").textContent = auditProgressText();
try {
await runKimiAuditForItem(state.auditCurrent);
setAuditPrompt(state.auditCurrent.audit_prompt);
setAuditModelResult(state.auditCurrent.audit_ai_feedback, state.auditCurrent.audit_ai_raw_output, state.auditCurrent.patient || null);
$("#auditMachineVerdict").value = state.auditCurrent.audit_machine_verdict;
renderAuditVerdicts(state.auditCurrent);
renderAuditList();
await persistAuditCurrent(state.auditCurrent.audit_result || "");
$("#auditState").textContent = auditProgressText();
setSaveState("AI抽查完成,AI反馈已保存");
} catch (error) {
setSaveState(`AI抽查失败:${error.message}`);
$("#auditState").textContent = auditProgressText();
if (options.bubble) throw error;
} finally {
finishAiTask();
}
}
async function runKimiAuditForItem(item) {
const result = await api("/api/audit/kimi", {
method: "POST",
body: JSON.stringify({ key: item.key, item }),
});
if (!result) return null;
item.audit_prompt = result.prompt || "";
item.audit_ai_feedback = normalizeModelResultText(result.result || "", item.patient || null);
item.audit_ai_raw_output = result.raw_result || result.result || "";
item.audit_machine_verdict = result.machine_verdict || modelVerdict(item.audit_ai_feedback || item.audit_ai_raw_output, item.patient || null) || "";
return result;
}
async function persistAuditItem(item, status, source = $("#auditSource").value) {
const originalPatient = item.audit_original_patient || item.original_patient || item.patient || {};
const changes = patientChanges(originalPatient, item.patient || {});
const updates = {
audit_result: status,
audit_ai_feedback: normalizeModelResultText(item.audit_ai_feedback || item.audit_ai_raw_output || "", item.patient || null),
audit_ai_raw_output: item.audit_ai_raw_output || item.audit_ai_feedback || "",
audit_manual_feedback: item.audit_manual_feedback || "",
audit_machine_verdict: item.audit_machine_verdict || "",
audit_source: source || item.audit_source || "",
audit_change_summary: changes.map((change) => `${change.column}: ${change.before || "空"} -> ${change.after || "空"}`).join(";"),
};
const payload = {
key: item.key,
item,
original_patient: originalPatient,
...updates,
};
const result = await api("/api/audit/result", {
method: "POST",
body: JSON.stringify(payload),
});
if (!result) return null;
Object.assign(item, updates, {
audit_checked_at: new Date().toISOString().slice(0, 19),
});
return result;
}
async function persistAuditCurrent(status) {
if (!state.auditCurrent) return;
readPatientEditor("#auditPatientInfo", state.auditCurrent);
state.auditCurrent.audit_result = status;
state.auditCurrent.audit_ai_feedback = getAuditModelResult();
state.auditCurrent.audit_ai_raw_output = getAuditRawOutput();
state.auditCurrent.audit_manual_feedback = $("#auditManualFeedback").value.trim();
state.auditCurrent.audit_machine_verdict = $("#auditMachineVerdict").value;
const result = await persistAuditItem(state.auditCurrent, status, $("#auditSource").value);
if (!result) return;
renderAuditList();
renderAuditVerdicts(state.auditCurrent);
return result;
}
async function saveAuditResult(status) {
if (!state.auditCurrent) return;
state.auditCurrent.audit_result = status;
renderAuditVerdicts(state.auditCurrent);
const result = await persistAuditCurrent(status);
if (!result) return;
$("#auditState").textContent = result.postgres?.updated ? `当前项已保存:${status},${auditProgressText()}` : `当前项未匹配数据库,${auditProgressText()}`;
renderChangeLog("#auditChangeLog", state.auditCurrent);
}
async function runKimiAuditAll() {
if (!state.auditItems.length) return;
if (!window.confirm(`将对当前抽取的 ${state.auditItems.length} 条逐条调用 AI,确定继续?`)) return;
if (!ensureAiReady("AI抽查")) return;
let success = 0;
let failed = 0;
const currentKey = state.auditCurrent?.key || "";
try {
for (const item of state.auditItems) {
try {
await runKimiAuditForItem(item);
await persistAuditItem(item, item.audit_result || "", item.audit_source || $("#auditSource").value);
success += 1;
if (item.key === currentKey) {
setAuditPrompt(item.audit_prompt || "");
setAuditModelResult(item.audit_ai_feedback || "", item.audit_ai_raw_output || item.audit_ai_feedback || "", item.patient || null);
$("#auditMachineVerdict").value = item.audit_machine_verdict || "";
renderAuditVerdicts(item);
}
renderAuditList();
const progress = `AI抽查中 ${success + failed}/${state.auditItems.length},${auditProgressText()}`;
$("#auditState").textContent = auditProgressText();
setSaveState(progress);
} catch (error) {
failed += 1;
item.audit_ai_feedback = `调用失败:${error.message}`;
item.audit_ai_raw_output = item.audit_ai_feedback;
item.audit_machine_verdict = "不确定";
if (state.auditCurrent?.key === item.key) {
setAuditModelResult("", item.audit_ai_raw_output);
$("#auditMachineVerdict").value = item.audit_machine_verdict;
renderAuditVerdicts(item);
}
renderAuditList();
}
}
} finally {
finishAiTask();
}
const doneText = `AI抽查所有项完成:成功${success},失败${failed},${auditProgressText()}`;
$("#auditState").textContent = auditProgressText();
setSaveState(doneText);
}
async function loadAuditHistory() {
$("#auditHistoryState").textContent = "加载中";
try {
const params = new URLSearchParams({
page: "1",
page_size: "80",
source: $("#auditHistorySourceFilter").value,
status: $("#auditHistoryStatusFilter").value,
sort: $("#auditHistorySortFilter").value,
q: $("#auditHistorySearchInput").value,
});
const data = await api(`/api/audit/history?${params.toString()}`);
if (!data) return;
state.auditHistory = data.items || [];
$("#auditHistoryState").textContent = `${state.auditHistory.length} / ${data.total}`;
renderAuditHistoryList();
if (state.auditHistory.length) {
selectAuditHistoryItem(state.auditHistory[0].key);
} else {
clearAuditHistoryDetail();
}
} catch (error) {
$("#auditHistoryState").textContent = `加载失败:${error.message}`;
}
}
function renderAuditHistoryList() {
$("#auditHistoryList").innerHTML = state.auditHistory.length
? state.auditHistory.map((item) => auditQueueItemHtml(item, state.auditHistoryCurrent?.key, [
`抽查源 ${auditSourceLabel(item.audit_source)}`,
`抽查时间 ${formatTime(item.audit_checked_at)}`,
], auditHistoryBadgeText(item))).join("")
: `暂无抽查记录
`;
$("#auditHistoryList").querySelectorAll(".queue-item").forEach((button) => {
button.addEventListener("click", () => selectAuditHistoryItem(button.dataset.key));
});
}
function clearAuditHistoryDetail() {
state.auditHistoryCurrent = null;
$("#auditHistoryTitle").textContent = "未选择抽查记录";
$("#auditHistoryMeta").textContent = "";
$("#auditHistoryFullImageLink").removeAttribute("href");
$("#auditHistoryImage").removeAttribute("src");
setAuditHistoryModelResult("", "");
$("#auditHistoryPatientInfo").innerHTML = "";
$("#auditHistoryTipsBox").innerHTML = "";
$("#auditHistoryChangeLog").innerHTML = "";
$("#auditHistoryAiVerdict").textContent = "";
$("#auditHistoryManualVerdict").textContent = "";
$("#auditHistorySource").textContent = "";
$("#auditHistoryUser").textContent = "";
$("#auditHistoryCheckedAt").textContent = "";
resetAuditHistoryImageView();
}
function selectAuditHistoryItem(key) {
const item = state.auditHistory.find((candidate) => candidate.key === key);
if (!item) return;
if (!item.audit_original_patient) {
item.audit_original_patient = { ...(item.patient || {}) };
}
state.auditHistoryCurrent = item;
$("#auditHistoryTitle").innerHTML = auditTitleHtml(item, auditHistoryBadgeText(item));
$("#auditHistoryMeta").textContent = `${item.major_department} / ${item.sub_department} / ${item.source_folder} / ${item.image_name} ${displayRowText(item)}`;
$("#auditHistoryImage").src = `/api/crop?path=${encodeURIComponent(item.image_path)}&row=${encodeURIComponent(item.image_row_no)}&context=2&v=${Date.now()}`;
$("#auditHistoryFullImageLink").href = `/api/image?path=${encodeURIComponent(item.image_path)}`;
resetAuditHistoryImageView();
setAuditHistoryModelResult(item.audit_ai_feedback || "", item.audit_ai_raw_output || item.audit_ai_feedback || "", item.patient || null);
renderPatientEditor("#auditHistoryPatientInfo", item, "#auditHistoryTipsBox", "#auditHistoryChangeLog");
updateAuditValidation("#auditHistoryPatientInfo", "#auditHistoryTipsBox", item);
renderChangeLog("#auditHistoryChangeLog", item);
$("#auditHistoryAiVerdict").textContent = item.audit_machine_verdict || "未判断";
$("#auditHistoryManualVerdict").textContent = item.audit_result || "未人工核验";
$("#auditHistorySource").textContent = auditSourceLabel(item.audit_source);
$("#auditHistoryUser").textContent = item.audit_checked_by || "";
$("#auditHistoryCheckedAt").textContent = formatTime(item.audit_checked_at);
renderAuditHistoryList();
}
async function saveAuditHistoryResult(status) {
if (!state.auditHistoryCurrent) return;
readPatientEditor("#auditHistoryPatientInfo", state.auditHistoryCurrent);
state.auditHistoryCurrent.audit_result = status;
state.auditHistoryCurrent.audit_manual_feedback = "";
const result = await persistAuditItem(
state.auditHistoryCurrent,
status,
state.auditHistoryCurrent.audit_source || $("#auditHistorySourceFilter").value,
);
if (!result) return;
$("#auditHistoryManualVerdict").textContent = status;
renderChangeLog("#auditHistoryChangeLog", state.auditHistoryCurrent);
renderAuditHistoryList();
$("#auditHistoryState").textContent = result.postgres?.updated ? `已保存:${status}` : "保存成功,本次未匹配数据库";
}
async function loadSettings() {
const data = await api("/api/settings");
if (!data) return;
state.permissionLabels = data.permission_labels || state.permissionLabels;
state.kimiEnabled = data.kimi_enabled !== false;
updateAiVisibility();
$("#kimiModel").value = data.kimi_model || "kimi-k2.6";
$("#kimiApiKey").placeholder = data.kimi_api_key_set ? "已设置,留空表示不修改" : "未设置";
$("#kimiToggleButton").textContent = state.kimiEnabled ? "关闭API" : "开启API";
const defaultPermissions = { overview: true, review: true, audit: true, audit_history: true, settings: false };
$("#settingsUserCount").textContent = `${(data.users || []).length} 个用户`;
$("#newUserPermissions").innerHTML = renderPermissionControls("new-perm", defaultPermissions);
$("#userList").innerHTML = (data.users || []).map((user, index) => `
${escapeHtml(user.username)}
${user.builtin ? "环境变量用户" : "配置用户"}
${escapeHtml(user.created_at || "")}
${renderPermissionControls(`user-perm-${index}`, user.permissions || {}, user.builtin)}
${user.builtin ? "" : ``}
`).join("");
document.querySelectorAll(".user-permission-save").forEach((button) => {
button.addEventListener("click", async () => {
const wrapper = button.closest(".settings-user");
await saveUserPermissions(wrapper.dataset.username, wrapper.dataset.prefix);
});
});
if (state.processingOverview) {
renderProcessingOverview(state.processingOverview);
} else {
findProcessingItems(true);
}
}
async function saveUser(event) {
event.preventDefault();
const username = $("#newUsername").value.trim();
const password = $("#newPassword").value;
if (!username || !password) return;
await api("/api/settings/users", {
method: "POST",
body: JSON.stringify({ username, password, permissions: readPermissionControls("new-perm") }),
});
$("#newUsername").value = "";
$("#newPassword").value = "";
await loadSettings();
}
function renderPermissionControls(prefix, permissions = {}, disabled = false) {
return permissionEntries().map(([key, label]) => {
const checked = permissions[key] !== false;
return `
`;
}).join("");
}
function readPermissionControls(prefix) {
const permissions = {};
document.querySelectorAll(`.${prefix}[data-permission]`).forEach((input) => {
permissions[input.dataset.permission] = input.checked;
});
return permissions;
}
async function saveUserPermissions(username, prefix) {
if (!username || !prefix) return;
await api(`/api/settings/users/${encodeURIComponent(username)}/permissions`, {
method: "POST",
body: JSON.stringify({ permissions: readPermissionControls(prefix) }),
});
$("#settingsState").textContent = "用户权限已保存";
await loadSettings();
}
async function saveKimiSettings(event) {
event.preventDefault();
await api("/api/settings/kimi", {
method: "POST",
body: JSON.stringify({ api_key: $("#kimiApiKey").value.trim(), model: $("#kimiModel").value.trim() }),
});
$("#kimiApiKey").value = "";
$("#settingsState").textContent = "已保存";
await loadSettings();
}
async function toggleKimiEnabled() {
const enabled = !state.kimiEnabled;
await api("/api/settings/kimi/enabled", {
method: "POST",
body: JSON.stringify({ enabled }),
});
state.kimiEnabled = enabled;
updateAiVisibility();
$("#kimiToggleButton").textContent = enabled ? "关闭API" : "开启API";
$("#settingsState").textContent = enabled ? "AI功能已开启" : "AI功能已关闭";
}
async function commitManualPassed() {
if (!window.confirm("确定提交已人工复核通过的数据?提交后这些人工通过条目会归档隐藏,不再出现在复核工作台。")) return;
$("#commitManualPassedState").textContent = "提交中";
try {
const result = await api("/api/settings/commit-manual-passed", {
method: "POST",
body: JSON.stringify({}),
});
if (!result) return;
const message = [
`可提交${result.eligible}`,
`合并结果更新${result.merged_updated}`,
`合并已是最新${result.merged_already_current ?? 0}`,
`数据库成功${result.postgres_success}`,
`已同步${result.postgres_already_success ?? 0}`,
`未匹配${result.postgres_not_found}`,
`失败${result.postgres_failed}`,
`跳过${result.skipped}(AI待确认${result.skipped_ai ?? 0},仍有问题${result.skipped_issue ?? 0})`,
`已归档隐藏${result.archived_corrections ?? 0}`,
].join(",");
$("#commitManualPassedState").textContent = message;
setSaveState("已提交人工通过数据");
await loadSummary();
window.alert(message);
} catch (error) {
$("#commitManualPassedState").textContent = `提交失败:${error.message}`;
}
}
async function resetAuditHistory() {
if (!window.confirm("确定清空抽查一览结果?这只会删除抽查结论、AI反馈和人工核验结果,不会删除患者基础信息。")) return;
$("#resetAuditHistoryState").textContent = "重置中";
try {
const result = await api("/api/settings/reset-audit-history", {
method: "POST",
body: JSON.stringify({}),
});
if (!result) return;
const message = `已重置${result.updated ?? 0}条抽查记录`;
$("#resetAuditHistoryState").textContent = message;
state.auditHistory = [];
clearAuditHistoryDetail();
renderAuditHistoryList();
$("#auditHistoryState").textContent = "暂无抽查记录";
} catch (error) {
$("#resetAuditHistoryState").textContent = `重置失败:${error.message}`;
}
}
function renderPostgresState(pg) {
const pending = `待同步${pg.pending_sync_count ?? 0}`;
const text = pg.ok
? `数据库已连接,${pending}`
: `数据库${pg.enabled ? "未连接" : "未启用"},${pending}${pg.error ? `:${pg.error}` : ""}`;
$("#pgState").textContent = text;
$("#pgState").classList.toggle("warn", !pg.ok && pg.enabled);
}
async function testPostgres(quiet = false) {
if (!quiet) setSaveState("测试PG中");
try {
const status = await api("/api/postgres/status");
if (!status) return;
renderPostgresState(status);
if (!quiet) setSaveState(status.ok ? `待同步${status.pending_sync_count ?? 0}` : "数据库未连接");
} catch (error) {
$("#pgState").textContent = `数据库未连接:${error.message}`;
$("#pgState").classList.add("warn");
if (!quiet) setSaveState("数据库连接失败");
}
}
async function syncPostgres() {
setSaveState("提交待同步中");
try {
const result = await api("/api/postgres/sync", { method: "POST", body: JSON.stringify({}) });
if (!result) return;
if (result.postgres) renderPostgresState(result.postgres);
await loadSummary();
await loadItems(false);
const message = `成功${result.success},未匹配${result.not_found},失败${result.failed}`;
window.alert(message);
const failedKeys = (result.items || []).filter((item) => item.status !== "success").map((item) => item.key);
if (failedKeys.length && window.confirm(`有 ${failedKeys.length} 条未成功同步。确定删除这些待同步记录?取消则继续保留。`)) {
const cleanup = await api("/api/postgres/sync/drop_failed", {
method: "POST",
body: JSON.stringify({ keys: failedKeys }),
});
if (cleanup?.postgres) renderPostgresState(cleanup.postgres);
await loadSummary();
setSaveState(`已删除${cleanup?.removed ?? 0}条未成功待同步`);
} else {
setSaveState(`待同步${result.postgres?.pending_sync_count ?? 0}`);
}
} catch (error) {
setSaveState(`同步失败:${error.message}`);
}
}
async function applyKimiCorrection(key) {
let result;
try {
result = await api(`/api/items/${key}/kimi-correction`, {
method: "POST",
body: JSON.stringify({}),
});
} catch (error) {
throw new Error(`AI修改失败:${error.message}`);
}
if (!result) return null;
const index = state.items.findIndex((item) => item.key === key);
if (index >= 0) state.items[index] = result.item;
if (state.current?.key === key) {
state.current = result.item;
renderCurrent();
}
renderQueue();
return result.item;
}
async function refreshKimiReviewList(focusKey = "") {
setStatusFilter("confirming", false);
await loadSummary();
await reloadLoadedItems();
if (focusKey && state.items.some((item) => item.key === focusKey)) {
await selectItem(focusKey);
} else if (state.items.length) {
await selectItem(state.items[0].key);
}
}
async function loadKimiTargetsFromCurrent(limit = null) {
if (!state.current) return [];
const start = state.items.findIndex((item) => item.key === state.current.key);
if (start < 0) return [];
const targetEnd = limit ? start + limit : Number.POSITIVE_INFINITY;
while (state.items.length < state.totalItems && state.items.length < targetEnd) {
await loadNextItems();
}
return state.items.slice(start, limit ? start + limit : undefined);
}
async function kimiCorrectCurrent() {
if (!state.current) return;
if (!window.confirm("确定调用 AI 修改当前项?修改后会标记为 AI修改-待确认。")) return;
if (!ensureAiReady("AI修改")) return;
const focusKey = state.current.key;
setSaveState("AI修改当前项中,请保持当前页面打开");
try {
await applyKimiCorrection(focusKey);
await refreshKimiReviewList(focusKey);
setSaveState("已生成 AI 修改,待人工确认");
} catch (error) {
setSaveState(error.message);
} finally {
finishAiTask();
}
}
async function kimiCorrectFive() {
if (!state.current) return;
if (!window.confirm("将从当前项开始调用 AI 修改下 5 项,确定继续?")) return;
if (!ensureAiReady("AI修改")) return;
setSaveState("AI修改5项中,请保持当前页面打开");
const focusKey = state.current.key;
let success = 0;
let failed = 0;
const failureReasons = new Map();
let targets = [];
try {
targets = await loadKimiTargetsFromCurrent(5);
for (const item of targets) {
try {
await applyKimiCorrection(item.key);
success += 1;
setSaveState(`AI修改中 ${success + failed}/${targets.length}`);
} catch (error) {
failed += 1;
failureReasons.set(error.message, (failureReasons.get(error.message) || 0) + 1);
setSaveState(`AI修改中 ${success + failed}/${targets.length},失败原因:${error.message}`);
}
}
await refreshKimiReviewList(focusKey);
} catch (error) {
failureReasons.set(`刷新列表失败:${error.message}`, 1);
} finally {
finishAiTask();
}
const reasonText = [...failureReasons.entries()]
.map(([reason, count]) => `${reason}${count > 1 ? ` x${count}` : ""}`)
.join(";");
setSaveState(`AI修改完成:成功${success},失败${failed}${reasonText ? `,失败原因:${reasonText}` : ""}`);
}
async function kimiCorrectRemaining() {
if (!state.current) return;
const start = state.items.findIndex((item) => item.key === state.current.key);
const remainingCount = start >= 0 ? Math.max(0, state.totalItems - start) : 0;
if (!remainingCount) return;
const message = `将从当前项开始调用 AI 修改下方全部 ${remainingCount} 项,会产生约 ${remainingCount} 次 AI 调用,确定继续?`;
if (!window.confirm(message)) return;
if (!ensureAiReady("AI修改")) return;
const focusKey = state.current.key;
setSaveState(`AI修改全部剩余项中,请保持当前页面打开`);
let success = 0;
let failed = 0;
const failureReasons = new Map();
let targets = [];
try {
targets = await loadKimiTargetsFromCurrent(null);
for (const item of targets) {
try {
await applyKimiCorrection(item.key);
success += 1;
setSaveState(`AI修改中 ${success + failed}/${targets.length}`);
} catch (error) {
failed += 1;
failureReasons.set(error.message, (failureReasons.get(error.message) || 0) + 1);
setSaveState(`AI修改中 ${success + failed}/${targets.length},失败原因:${error.message}`);
}
}
await refreshKimiReviewList(focusKey);
} catch (error) {
failureReasons.set(`刷新列表失败:${error.message}`, 1);
} finally {
finishAiTask();
}
const reasonText = [...failureReasons.entries()]
.map(([reason, count]) => `${reason}${count > 1 ? ` x${count}` : ""}`)
.join(";");
setSaveState(`AI修改全部剩余项完成:成功${success},失败${failed}${reasonText ? `,失败原因:${reasonText}` : ""}`);
}
async function boot() {
bindEvents();
await loadSession();
await loadSummary();
await findProcessingItems(true);
if (state.permissions.review !== false) await loadItems(true);
state.pgTimer = setInterval(() => testPostgres(true).catch(() => {}), 20000);
}
boot().catch((error) => {
setSaveState("加载失败");
$("#queueList").innerHTML = `${escapeHtml(error.message)}
`;
});