@@ -16,6 +16,8 @@ const state = {
auditLogs : [ ] ,
settings : null ,
ai : null ,
aiJob : null ,
aiJobOwned : false ,
aiPolling : null ,
currentUser : null ,
authenticated : false ,
@@ -28,6 +30,19 @@ const PDF_MIN_ZOOM = 55;
const PDF _MAX _ZOOM = 220 ;
const OPERATION _COLUMNS = [ "手术操作编码" , "手术操作日期" , "手术级别" , "手术操作名称" , "术者" , "I助" , "II助" , "切口愈合等级" , "麻醉方式" , "麻醉医师" , "原始内容" ] ;
const DIAGNOSIS _COLUMNS = [ "诊断类别" , "出院诊断" , "疾病编码" , "入院病情" ] ;
const AI _ACTION _MODE _OPTIONS = [
[ "off" , "关闭" ] ,
[ "default" , "跟随默认模型" ] ,
[ "k25" , "K2.5 关 Thinking" ] ,
[ "k25_thinking" , "K2.5 开 Thinking" ] ,
[ "k26" , "K2.6 关 Thinking" ] ,
[ "k26_thinking" , "K2.6 开 Thinking" ] ,
] ;
const AI _ACTION _SCOPE _LABELS = {
current : "AI当前项" ,
five : "AI后5项" ,
all : "AI后全部" ,
} ;
const GROUP _KEYWORDS = {
基本信息 : [ "住院号" , "病案号" , "首页病案号" , "姓名" , "性别" , "出生" , "年龄" , "身份证" , "婚姻" , "健康卡" , "入院" , "出院" , "科别" , "科室" , "病房" , "住院天数" ] ,
@@ -67,8 +82,10 @@ function statusTag(status) {
auto _corrected : [ "自动修正" , "auto_corrected" ] ,
needs _review : [ "需复核" , "needs_review" ] ,
reviewed : [ "已复核" , "reviewed" ] ,
"AI复核-无问题 " : [ "AI复核-无问题 " , "ai_passed" ] ,
"AI复核-待确认 " : [ "AI复核-待确认 " , "ai_pending" ] ,
"AI已处理-OK " : [ "AI已通过 " , "ai_passed" ] ,
"AI已处理-不OK " : [ "AI建议 复核" , "ai_pending" ] ,
"AI复核-无问题" : [ "AI已通过" , "ai_passed" ] ,
"AI复核-待确认" : [ "AI建议复核" , "ai_pending" ] ,
"已提交" : [ "已提交" , "submitted" ] ,
pending : [ "待处理" , "pending" ] ,
} ;
@@ -76,11 +93,73 @@ function statusTag(status) {
return ` <span class="tag ${ className } "> ${ label } </span> ` ;
}
function hasAiReview ( record ) {
return Boolean ( record ? . ai _reviewed ) || ( record ? . review _logs || [ ] ) . some ( ( log ) => isAiSource ( log ? . changed _by ) ) ;
}
function displayStatusValue ( record ) {
if ( record ? . review _status === "auto_pass" && hasAiReview ( record ) ) return "AI已处理-OK" ;
return record ? . review _status ;
}
function auditTag ( status ) {
const labels = { pending : "待抽查" , passed : "抽查通过" , failed : "抽查异常" , unsure : "不确定" } ;
return ` <span class="tag audit- ${ status || "pending" } "> ${ labels [ status ] || status || "待抽查" } </span> ` ;
}
function displayText ( value ) {
return String ( value ? ? "" )
. replaceAll ( "Kimi AI" , "AI" )
. replaceAll ( "Kimi视觉核验" , "AI视觉核验" )
. replaceAll ( "Kimi" , "AI" )
. replaceAll ( "AI判断不OK" , "AI建议复核" )
. replaceAll ( "AI判断OK" , "AI判断通过" )
. replaceAll ( "需要人工" , "需要复核" )
. replaceAll ( "需人工" , "需复核" )
. replaceAll ( "人工确认" , "复核" )
. replaceAll ( "人工核对" , "复核" )
. replaceAll ( "人工重点看" , "复核" ) ;
}
function isAiSource ( value ) {
const text = String ( value || "" ) ;
return text . includes ( "AI" ) || text . includes ( "Kimi" ) ;
}
function displaySource ( value ) {
return isAiSource ( value ) ? "AI" : displayText ( value || "web" ) ;
}
function isReviewNeededText ( value ) {
const text = String ( value || "" ) . trim ( ) ;
return Boolean ( text ) && ! [ "无需" , "无须" , "不需" , "不用" , "无需补录" , "无须补录" , "无需处理" , "原貌" ] . some ( ( marker ) => text . includes ( marker ) ) ;
}
function aiAttentionSegments ( value ) {
return displayText ( value )
. split ( /[, ,; ;。]/ )
. map ( ( item ) => item . trim ( ) )
. filter ( isReviewNeededText ) ;
}
function locateTargetsFromNotes ( notes ) {
const text = notes . join ( "; " ) ;
const fieldAlerts = new Set ( ) ;
const groupAlerts = new Set ( ) ;
state . schema . forEach ( ( group ) => {
let groupHit = ( GROUP _KEYWORDS [ group . name ] || [ ] ) . some ( ( keyword ) => text . includes ( keyword ) ) ;
group . fields . forEach ( ( [ name , label ] ) => {
const keywords = [ label , name , ... ( FIELD _KEYWORDS [ name ] || [ ] ) ] . filter ( ( keyword ) => String ( keyword ) . length > 1 ) ;
if ( keywords . some ( ( keyword ) => text . includes ( keyword ) ) ) {
fieldAlerts . add ( name ) ;
groupHit = true ;
}
} ) ;
if ( groupHit ) groupAlerts . add ( group . name ) ;
} ) ;
return { fieldAlerts , groupAlerts } ;
}
async function api ( path , options = { } ) {
let response ;
try {
@@ -201,6 +280,11 @@ function changePdfZoom(delta) {
function switchPage ( page ) {
if ( ! state . authenticated || ! canOpenPage ( page ) ) return ;
if ( page !== "review" && state . aiJob ? . running && state . aiJobOwned ) {
const leave = window . confirm ( "AI正在处理。离开复核页会中断本次AI处理, 确认离开? " ) ;
if ( ! leave ) return ;
cancelAiReview ( { silent : true } ) . catch ( ( ) => null ) ;
}
state . activePage = page ;
document . querySelectorAll ( ".page-view" ) . forEach ( ( el ) => el . classList . add ( "is-hidden" ) ) ;
$ ( ` ${ page } Page ` ) ? . classList . remove ( "is-hidden" ) ;
@@ -218,13 +302,14 @@ async function loadStatus() {
const stateLabel = data . database === "online" ? "在线" : data . database === "unchecked" ? "未检查" : "离线" ;
$ ( "dbState" ) . textContent = stateLabel ;
$ ( "dbState" ) . className = data . database === "online" ? "status-online" : data . database === "unchecked" ? "status-muted" : "status-offline" ;
$ ( "dbTotal" ) . textContent = data . total ? ? "--" ;
$ ( "dbTotal" ) . textContent = data . workbench _total ? ? data . review _needed ? ? "--" ;
$ ( "dbNeedsReview" ) . textContent = data . needs _review ? ? "--" ;
$ ( "dbAiPassed" ) . textContent = data . ai _passed ? ? "--" ;
$ ( "dbAiPending" ) . textContent = data . ai _pending ? ? "--" ;
$ ( "dbReviewed" ) . textContent = data . reviewed ? ? "--" ;
$ ( "statusGrid" ) . title = ` ${ data . host } : ${ data . port } / ${ data . database _name } · ${ data . table } · ${ data . message || "" } · ${ data . checked _at ? ` 上次检查 ${ formatTime ( data . checked _at ) } ` : "尚未检查" } ` ;
state . status = data ;
renderFilterActions ( ) ;
return data ;
}
@@ -238,10 +323,10 @@ function renderOverview(data) {
const summary = data . summary || { } ;
const metrics = [
[ "总记录" , summary . total ] ,
[ "复核队列 " , summary . review _queue ] ,
[ "待 复核总数 " , summary . review _queue ] ,
[ "需复核" , summary . needs _review ] ,
[ "AI待确认 " , summary . ai _pending ] ,
[ "AI无问题 " , summary . ai _passed ] ,
[ "AI建议复核 " , summary . ai _pending ] ,
[ "AI已通过 " , summary . ai _passed ] ,
[ "已复核" , summary . reviewed ] ,
[ "已提交" , summary . submitted ] ,
[ "自动通过" , summary . auto _passed ] ,
@@ -255,11 +340,11 @@ function renderOverview(data) {
</article>
` ) . join ( "" ) ;
$ ( "overviewQueues" ) . innerHTML = `
<div class="queue-line"><span>复核工作台默认</span><strong>全部数据:需复核 ${ summary . needs _review || 0 } / AI待确认 ${ summary . ai _pending || 0 } / 已复核 ${ summary . reviewed || 0 } </strong></div>
<div class="queue-line"><span>复核工作台默认</span><strong>全部数据:需复核 ${ summary . needs _review || 0 } / AI已通过 ${ summary . ai _passed || 0 } / AI建议复核 ${ summary . ai _pending || 0 } / 已复核 ${ summary . reviewed || 0 } </strong></div>
<div class="queue-line"><span>抽查候选池</span><strong>已复核 ${ summary . reviewed || 0 } 条 / 自动通过 ${ summary . auto _passed || 0 } 条</strong></div>
<div class="queue-line"><span>日志存储</span><strong>人工复核和抽查记录均写入主表 JSONB 列</strong></div>
` ;
$ ( "overviewRecentLogs" ) . innerHTML = renderLogTable ( data . recent _logs || [ ] ) ;
$ ( "overviewRecentLogs" ) . innerHTML = renderLogTable ( logChangeRows ( data. recent _logs || [ ] ) ) ;
}
async function loadSchema ( ) {
@@ -281,6 +366,7 @@ async function loadRecords({ append = false } = {}) {
state . recordHasMore = Boolean ( data . has _more ) ;
if ( ! append && ! state . records . some ( ( record ) => record . id === state . selectedId ) ) {
state . selectedId = null ;
state . selectedRecord = null ;
}
renderRecords ( ) ;
renderFilterActions ( ) ;
@@ -316,7 +402,7 @@ function renderRecords() {
<button class="record-item ${ active } " type="button" data-id=" ${ record . id } ">
<div class="record-main">
<span class="record-name"> ${ escapeHtml ( record . patient _name || "未命名" ) } </span>
${ statusTag ( record . review _status ) }
${ statusTag ( displayStatusValue ( record ) ) }
</div>
<div class="record-sub">
${ escapeHtml ( record . inpatient _no || record . medical _record _no || "" ) } · ${ escapeHtml ( record . major _department || record . discharge _dept || "" ) } ${ manual } <br>
@@ -335,7 +421,12 @@ function renderRecords() {
function renderFilterActions ( ) {
const button = $ ( "approveAiNoIssueBtn" ) ;
if ( ! button ) return ;
button . classList . toggle ( "is-hidden" , $ ( "statusFilter" ) . value !== "AI复核-无问题" ) ;
const filter = $ ( "statusFilter" ) ? . value || "review_all" ;
const aiPassedCount = Number ( state . status ? . ai _passed || 0 ) ;
const shouldShow = [ "review_all" , "ai_passed" ] . includes ( filter ) ;
button . classList . toggle ( "is-hidden" , ! shouldShow ) ;
button . disabled = ! shouldShow || aiPassedCount <= 0 ;
button . textContent = aiPassedCount > 0 ? ` 通过所有AI已通过( ${ aiPassedCount } ) ` : "通过所有AI已通过" ;
}
function scrollSelectedRecordIntoView ( ) {
@@ -372,11 +463,16 @@ function recordListIndex() {
function recordMatchesActiveFilter ( record ) {
const status = $ ( "statusFilter" ) . value || "review_all" ;
if ( status === "review_all" ) {
return [ "needs_review" , "reviewed" , "AI复核-无问题" , "AI复核-待确认" ] . includes ( record . review _status ) || record . manual _corrected === true ;
return [ "needs_review" , "reviewed" , "AI已处理-OK" , "AI已处理-不OK" , "AI复核-无问题" , "AI复核-待确认" ] . includes ( record . review _status )
|| ( record . review _status === "auto_pass" && hasAiReview ( record ) )
|| record . manual _corrected === true ;
}
if ( status === "needs_review" ) {
return record . review _status === "needs_review" ;
}
if ( status === "ai_passed" ) {
return [ "AI已处理-OK" , "AI复核-无问题" ] . includes ( record . review _status ) || ( record . review _status === "auto_pass" && hasAiReview ( record ) ) ;
}
if ( status === "reviewed" ) {
return record . review _status === "reviewed" ;
}
@@ -444,6 +540,7 @@ function renderSelectedRecord() {
logTableId : "changeLogTable" ,
} ) ;
renderChangeLogs ( record . review _logs || [ ] , "changeLogCount" , "changeLogTable" ) ;
renderAiQuestionLogs ( record . review _logs || [ ] ) ;
setView ( state . view ) ;
}
@@ -462,7 +559,7 @@ async function refreshSelectedRecord() {
}
}
function r eviewNotes( record ) {
function baseR eviewNotes( record ) {
const notes = [ ] ;
[ "review_notes" , "quality_notes" , "auto_corrections" ] . forEach ( ( key ) => {
const value = record [ key ] ;
@@ -472,31 +569,49 @@ function reviewNotes(record) {
notes . push ( String ( value ) ) ;
}
} ) ;
return notes . filter ( Boolean ) ;
return notes . map ( displayText ) . filter( Boolean ) ;
}
function aiAttentionNotes ( record ) {
const notes = [ ] ;
( record . review _logs || [ ] ) . forEach ( ( log ) => {
const parsed = log . ai _result ? . parsed ;
if ( ! parsed ) return ;
if ( Array . isArray ( parsed . remaining _issues ) ) {
parsed . remaining _issues . forEach ( ( item ) => {
if ( isReviewNeededText ( item ) ) notes . push ( item ) ;
} ) ;
}
const classification = String ( parsed . classification || parsed . decision || "" ) . toLowerCase ( ) ;
if ( ! notes . length && [ "problem" , "not_ok" , "not ok" , "confirm" , "需复核" , "待确认" ] . includes ( classification ) ) {
notes . push ( ... aiAttentionSegments ( parsed . summary ) ) ;
}
} ) ;
return notes . map ( displayText ) . filter ( Boolean ) ;
}
function reviewTargets ( record ) {
const notes = r eviewNotes( record ) ;
const text = n otes. join ( "; " ) ;
const fieldAlerts = new Set ( ) ;
const groupAlerts = new Set ( ) ;
state . schema . forEach ( ( group ) => {
le t groupHit = ( GROUP _KEYWORDS [ group . name ] || [ ] ) . some ( ( keyword ) => text . include s( keyword ) ) ;
group . fields . forEach ( ( [ name , label ] ) => {
const keywords = [ label , name , ... ( FIELD _KEYWORDS [ name ] || [ ] ) ] . filter ( ( keyword ) => String ( keyword ) . length > 1 ) ;
if ( keywords . some ( ( keyword ) => text . includes ( keyword ) ) ) {
fieldAlerts . add ( name ) ;
groupHit = true ;
}
} ) ;
if ( groupHit ) groupAlerts . add ( group. name ) ;
} ) ;
return { notes , fieldAlerts , groupAlerts } ;
const notes = baseR eviewNotes( record ) ;
const aiNotes = aiAttentionN otes( record ) ;
const baseTargets = locateTargetsFromNotes ( notes ) ;
const aiTargets = locateTargetsFromNotes ( aiNotes ) ;
const fieldAlerts = new Set ( [ ... baseTargets . fieldAlerts , ... aiTargets . fieldAlerts ] ) ;
cons t groupAlerts = new Set ( [ ... baseTargets . groupAlerts , ... aiTarget s. groupAlerts ] ) ;
const hasAiNotes = ( record . review _logs || [ ] ) . some ( ( log ) => Boolean ( log . ai _result ) ) ;
return {
notes ,
aiNotes ,
fieldAlerts ,
groupAlerts ,
baseGroupAlerts : baseTargets . groupAlerts ,
aiGroupAlerts : aiTargets . groupAlerts ,
hasAiNotes ,
} ;
}
function renderReviewStrip ( record ) {
$ ( "reviewStrip" ) . innerHTML = `
<div class="strip-item"><span>复核状态</span> ${ statusTag ( record . review _status ) } </div>
<div class="strip-item"><span>复核状态</span> ${ statusTag ( displayStatusValue ( record ) ) } </div>
<label class="strip-item strip-note"><span>本次人工备注</span><input id="manualNote" type="text" placeholder="填写修正原因或核验说明"></label>
` ;
}
@@ -507,11 +622,12 @@ function renderForm(record, options = {}) {
const noteId = options . noteId || "manualNote" ;
const form = $ ( formId ) ;
const targets = reviewTargets ( record ) ;
const sourceMap = fieldSourceMap ( record . review _logs || [ ] ) ;
const hasTarget = targets . groupAlerts . size > 0 ;
renderTargetSummary ( targets , targetId , formId ) ;
form . innerHTML = state . schema . map ( ( group , index ) => {
const groupAlert = targets . groupAlerts . has ( group . name ) ;
const fields = group . fields . map ( ( [ name , label , type , options ] ) => renderField ( record , name , label , type , options , targets . fieldAlerts . has ( name ) ) ) . join ( "" ) ;
const fields = group . fields . map ( ( [ name , label , type , options ] ) => renderField ( record , name , label , type , options , targets . fieldAlerts . has ( name ) , sourceMap )) . join ( "" ) ;
const open = groupAlert || ( ! hasTarget && index === 0 ) ;
return `
<details class="field-group ${ groupAlert ? " is-alert" : "" } " data-group=" ${ escapeHtml ( group . name ) } " ${ open ? "open" : "" } >
@@ -524,21 +640,37 @@ function renderForm(record, options = {}) {
}
function renderTargetSummary ( targets , targetId = "targetSummary" , formId = "detailForm" ) {
const g roups = [ ... targets . g roupAlerts] ;
const notesHtml = targets . notes . length
const baseG roups = [ ... targets . baseG roupAlerts] ;
const aiGroups = [ ... targets . aiGroupAlerts ] ;
const baseNotesHtml = targets . notes . length
? targets . notes . map ( ( note ) => ` <li> ${ escapeHtml ( note ) } </li> ` ) . join ( "" )
: "<li>暂无复核 提示</li>" ;
: "<li>暂无原始复核定位 提示</li>" ;
const aiNotesHtml = targets . aiNotes . length
? targets . aiNotes . map ( ( note ) => ` <li> ${ escapeHtml ( note ) } </li> ` ) . join ( "" )
: "<li>AI未提出需要复核的内容</li>" ;
$ ( targetId ) . innerHTML = `
<details class="collapsible-panel target-panel" open>
<summary>
<strong>复核定位</strong>
<span> ${ g roups. length ? ` ${ g roups. length } 个模块 ` : "未定位" } </span>
<span> ${ baseG roups. length ? ` ${ baseG roups. length } 个模块 ` : "未定位" } </span>
</summary>
<div class="target-chips">
${ g roups. length ? g roups. map ( ( group ) => ` <button type="button" data-target-group=" ${ escapeHtml ( group ) } "> ${ escapeHtml ( group ) } </button> ` ) . join ( "" ) : "<span>未定位到具体模块,先核对基本信息 </span>" }
${ baseG roups. length ? baseG roups. map ( ( group ) => ` <button type="button" data-target-group=" ${ escapeHtml ( group ) } "> ${ escapeHtml ( group ) } </button> ` ) . join ( "" ) : "<span>原始复核信息 未定位到具体模块</span>" }
</div>
<ul> ${ n otesHtml} </ul>
<ul> ${ baseN otesHtml} </ul>
</details>
${ targets . hasAiNotes ? `
<details class="collapsible-panel target-panel is-ai-target" open>
<summary>
<strong>AI建议复核点</strong>
<span> ${ aiGroups . length ? ` ${ aiGroups . length } 个模块 ` : "无待看点" } </span>
</summary>
<div class="target-chips is-ai-target">
${ aiGroups . length ? aiGroups . map ( ( group ) => ` <button type="button" data-target-group=" ${ escapeHtml ( group ) } "> ${ escapeHtml ( group ) } </button> ` ) . join ( "" ) : "<span>AI未标出需要特别关注的模块</span>" }
</div>
<ul class="ai-note-list"> ${ aiNotesHtml } </ul>
</details>
` : "" }
` ;
$ ( targetId ) . querySelectorAll ( "[data-target-group]" ) . forEach ( ( button ) => {
button . addEventListener ( "click" , ( ) => {
@@ -551,23 +683,37 @@ function renderTargetSummary(targets, targetId = "targetSummary", formId = "deta
} ) ;
}
function renderField ( record , name , label , type , options , alerted = false ) {
function fieldSourceMap ( logs ) {
const map = { } ;
logs . forEach ( ( log ) => {
const source = isAiSource ( log . changed _by ) ? "ai" : "manual" ;
rightSideChangedFields ( log ) . forEach ( ( field ) => {
const path = String ( field . field || "" ) ;
if ( path ) map [ path ] = source ;
} ) ;
} ) ;
return map ;
}
function renderField ( record , name , label , type , options , alerted = false , sourceMap = { } ) {
const value = record [ name ] ;
const wide = type === "json" || String ( value || "" ) . length > 28 ? " is-wide" : "" ;
const alertClass = alerted ? " is-alert" : "" ;
const source = type === "json" ? "" : sourceMap [ name ] || "" ;
const sourceClass = source === "ai" ? " is-ai-modified" : source === "manual" ? " is-manual-modified" : "" ;
let control = "" ;
if ( type === "select" ) {
control = ` <select data-field=" ${ name } " data-type=" ${ type } ">
${ ( options || [ ] ) . map ( ( option ) => ` <option value=" ${ escapeHtml ( option ) } " ${ option === value ? "selected" : "" } > ${ escapeHtml ( option ) } </option> ` ) . join ( "" ) }
</select> ` ;
} else if ( type === "json" ) {
control = renderJsonEditor ( name , value ) ;
control = renderJsonEditor ( name , value , sourceMap );
} else {
const inputType = type === "date" ? "date" : "text" ;
const placeholder = type === "datetime" ? "YYYY-MM-DD HH:MM:SS" : "" ;
control = ` <input data-field=" ${ name } " data-type=" ${ type } " type=" ${ inputType } " value=" ${ escapeHtml ( value ? ? "" ) } " placeholder=" ${ placeholder } "> ` ;
}
return ` <label class="field ${ wide } ${ alertClass } "><span> ${ escapeHtml ( label ) } ${ alerted ? "<em>需核对</em>" : "" } </span> ${ control } </label> ` ;
return ` <label class="field ${ wide } ${ alertClass } ${ sourceClass } "><span> ${ escapeHtml ( label ) } ${ source === "ai" ? "<em>AI修改</em>" : source === "manual" ? "<em>人工修改</em>" : alerted ? "<em>需核对</em>" : "" } </span> ${ control } </label> ` ;
}
function preferredColumns ( fieldName , value ) {
@@ -588,11 +734,11 @@ function preferredColumns(fieldName, value) {
return [ "值" ] ;
}
function renderJsonEditor ( fieldName , value ) {
function renderJsonEditor ( fieldName , value , sourceMap = { } ) {
const columns = preferredColumns ( fieldName , value ) ;
const rows = normalizeJsonRows ( value , columns ) ;
const header = columns . map ( ( column ) => ` <th> ${ escapeHtml ( column ) } </th> ` ) . join ( "" ) ;
const body = rows . map ( ( row ) => renderJsonRow ( columns , row ) ) . join ( "" ) ;
const body = rows . map ( ( row , rowIndex ) => renderJsonRow ( columns , row , fieldName , rowIndex , sourceMap )) . join ( "" ) ;
return `
<div class="json-editor" data-json-field=" ${ fieldName } " data-kind=" ${ Array . isArray ( value ) ? "array" : "object" } ">
<div class="json-toolbar">
@@ -602,7 +748,7 @@ function renderJsonEditor(fieldName, value) {
<div class="json-table-wrap">
<table class="json-table" data-columns=" ${ escapeHtml ( JSON . stringify ( columns ) ) } ">
<thead><tr> ${ header } <th>操作</th></tr></thead>
<tbody> ${ body || renderJsonRow ( columns , { } )}</tbody>
<tbody> ${ body || renderJsonRow ( columns , { } , fieldName, 0, sourceMap )}</tbody>
</table>
</div>
</div>
@@ -623,10 +769,14 @@ function normalizeJsonRows(value, columns) {
return [ { [ columns [ 0 ] || "值" ] : value } ] ;
}
function renderJsonRow ( columns , row ) {
function renderJsonRow ( columns , row , fieldName = "" , rowIndex = 0 , sourceMap = { } ) {
const cells = columns . map ( ( column ) => {
const value = row [ column ] ? ? "" ;
return ` <td><input data-json-key=" ${ escapeHtml ( column ) } " value=" ${ escapeHtml ( value ) } "></td> ` ;
const path = ` ${ fieldName } [ ${ rowIndex } ]. ${ column } ` ;
const source = sourceMap [ path ] || "" ;
const sourceClass = source === "ai" ? " is-ai-cell" : source === "manual" ? " is-manual-cell" : "" ;
const title = source === "ai" ? "AI修改" : source === "manual" ? "人工修改" : "" ;
return ` <td class=" ${ sourceClass } " title=" ${ escapeHtml ( title ) } "><input data-json-key=" ${ escapeHtml ( column ) } " value=" ${ escapeHtml ( value ) } "></td> ` ;
} ) . join ( "" ) ;
return ` <tr> ${ cells } <td><button type="button" data-json-delete>删除</button></td></tr> ` ;
}
@@ -733,6 +883,7 @@ function compactRecord(record) {
medical _record _no : record . medical _record _no ,
patient _name : record . patient _name ,
review _status : record . review _status ,
ai _reviewed : record . ai _reviewed || hasAiReview ( record ) ,
manual _corrected : record . manual _corrected ,
major _department : record . major _department ,
discharge _dept : record . discharge _dept ,
@@ -750,38 +901,160 @@ function setView(view) {
}
function renderChangeLogs ( logs , countId = "changeLogCount" , tableId = "changeLogTable" ) {
$ ( countId ) . textContent = ` ${ log s. length } 条 ` ;
$ ( table Id) . innerHTML = renderLogTable ( logs ) ;
const rows = logChangeRow s( logs ) ;
$ ( count Id) . textContent = ` ${ rows . length } 项 ` ;
$ ( tableId ) . innerHTML = renderLogTable ( rows ) ;
}
function renderLogTable ( logs ) {
if ( ! logs . length ) return ` <div class="empty small">暂无修改记录</div> ` ;
const row s = logs . map ( ( log ) => {
const fields = Array . isArray ( log . changed _ fields ) ? log . changed _ fields : [ ] ;
const summary = fields . length
? fields . map ( ( field ) => ` ${ field . label || field . field } : ${ shortValue ( field . old ) } -> ${ shortValue ( field . new ) } ` ) . join ( "; " )
: "仅记录人工备注或状态确认" ;
function rightSideChangedFields ( log ) {
const ignored = new Set ( [ "review_status" , "review_notes" , "manual_corrected" , "ai_method" ] ) ;
const field s = Array . isArray ( log . changed _fields ) ? log . changed _fields : [ ] ;
return fields . flatMap ( ( field ) => expandChangedField ( field ) ) . filter ( ( field) => {
const root = String ( field . field || "" ) . split ( "[" ) [ 0 ] . split ( "." ) [ 0 ] ;
return root && ! ignored . has ( root ) ;
} ) ;
}
function expandChangedField ( field ) {
const path = String ( field . field || "" ) ;
const root = path . split ( "[" ) [ 0 ] . split ( "." ) [ 0 ] ;
if ( ! root || path . includes ( "[" ) || path . includes ( "." ) ) return [ field ] ;
const oldValue = field . old ;
const newValue = field . new ;
if ( ! Array . isArray ( oldValue ) && ! Array . isArray ( newValue ) ) return [ field ] ;
const columns = preferredColumns ( root , Array . isArray ( newValue ) ? newValue : oldValue ) ;
const oldRows = normalizeJsonRows ( oldValue , columns ) ;
const newRows = normalizeJsonRows ( newValue , columns ) ;
const maxRows = Math . max ( oldRows . length , newRows . length ) ;
const expanded = [ ] ;
for ( let rowIndex = 0 ; rowIndex < maxRows ; rowIndex += 1 ) {
columns . forEach ( ( column ) => {
const oldCell = oldRows [ rowIndex ] ? . [ column ] ? ? "" ;
const newCell = newRows [ rowIndex ] ? . [ column ] ? ? "" ;
if ( comparableValue ( oldCell ) === comparableValue ( newCell ) ) return ;
expanded . push ( {
field : ` ${ root } [ ${ rowIndex } ]. ${ column } ` ,
label : ` ${ fieldLabel ( root ) } [ ${ rowIndex + 1 } ]. ${ column } ` ,
old : oldCell ,
new : newCell ,
} ) ;
} ) ;
}
return expanded . length ? expanded : [ field ] ;
}
function fieldLabel ( fieldName ) {
for ( const group of state . schema || [ ] ) {
for ( const [ name , label ] of group . fields || [ ] ) {
if ( name === fieldName ) return label ;
}
}
return fieldName ;
}
function comparableValue ( value ) {
return JSON . stringify ( value ? ? "" ) ;
}
function logChangeRows ( logs ) {
return logs . flatMap ( ( log ) => rightSideChangedFields ( log ) . map ( ( field ) => ( { log , field } ) ) ) ;
}
function renderLogTable ( rows ) {
if ( ! rows . length ) return ` <div class="empty small">暂无修改记录</div> ` ;
const html = rows . map ( ( { log , field } ) => {
const sourceClass = isAiSource ( log . changed _by ) ? " log-ai" : " log-manual" ;
return `
<tr>
<tr class=" ${ sourceClass } " >
<td> ${ escapeHtml ( formatTime ( log . changed _at ) ) } </td>
<td> ${ escapeHtml ( log . changed _by || "web" ) } </td>
<td> ${ escapeHtml ( summary ) } </td>
<td> ${ escapeHtml ( log . manual _note || "" ) } </td>
<td> ${ escapeHtml ( displaySource ( log. changed _by ) ) } </td>
<td> ${ escapeHtml ( field . label || field . field || "" ) } </td>
<td> ${ escapeHtml ( shortValue ( field . old ) ) } </td>
<td> ${ escapeHtml ( shortValue ( field . new ) ) } </td>
<td> ${ escapeHtml ( displayText ( log . manual _note || "" ) ) } </td>
</tr>
` ;
} ) . join ( "" ) ;
return `
<table class="data-table">
<thead><tr><th>修改时间</th><th>来源</th><th>修改内容 </th><th>备注</th></tr></thead>
<tbody> ${ rows } </tbody>
<thead><tr><th>修改时间</th><th>来源</th><th>字段</th><th>修改前</th><th>修改后 </th><th>备注</th></tr></thead>
<tbody> ${ html } </tbody>
</table>
` ;
}
function renderAiQuestionLogs ( logs ) {
const box = $ ( "aiQuestionTable" ) ;
const count = $ ( "aiQuestionCount" ) ;
if ( ! box || ! count ) return ;
const aiLogs = logs . filter ( ( log ) => log . ai _result ) ;
count . textContent = ` ${ aiLogs . length } 条 ` ;
if ( ! aiLogs . length ) {
box . innerHTML = ` <div class="empty small">暂无AI提问记录</div> ` ;
return ;
}
const recordId = state . selectedRecord ? . id ;
box . innerHTML = aiLogs . map ( ( log , logIndex ) => {
const result = log . ai _result || { } ;
const parsed = result . parsed || { } ;
const modules = result . pdf _context ? . modules || [ ] ;
const moduleText = modules . map ( ( item ) => ` ${ item . name || "" } P ${ item . page || "" } ` ) . filter ( Boolean ) . join ( "; " ) || "未定位" ;
const question = result . ai _question || "" ;
const imageGrid = modules . length && recordId ? `
<div>
<strong>传入图片</strong>
<div class="ai-question-images">
${ modules . map ( ( item , moduleIndex ) => `
<figure class="ai-question-figure">
<div class="ai-question-image-frame">
<img
data-ai-src="/api/records/ ${ encodeURIComponent ( recordId ) } /ai-question-image/ ${ logIndex } / ${ moduleIndex } "
alt=" ${ escapeHtml ( ` ${ item . name || "局部截图" } 第 ${ item . page || "" } 页 ` ) } "
loading="lazy"
>
</div>
<figcaption> ${ escapeHtml ( item . name || "局部截图" ) } · P ${ escapeHtml ( item . page || "" ) } · ${ escapeHtml ( ( item . bbox || [ ] ) . join ( ", " ) ) } </figcaption>
</figure>
` ) . join ( "" ) }
</div>
</div>
` : ` <div><strong>传入图片</strong><p>本次未上传图片或图片定位未保存</p></div> ` ;
return `
<details class="ai-question-item">
<summary>
<span> ${ escapeHtml ( formatTime ( log . changed _at ) ) } </span>
<strong> ${ escapeHtml ( displayText ( parsed . classification || parsed . decision || "" ) ) } </strong>
<em> ${ escapeHtml ( moduleText ) } </em>
</summary>
<div class="ai-question-body">
<div><strong>结论</strong><p> ${ escapeHtml ( displayText ( parsed . summary || log . manual _note || "" ) ) } </p></div>
<div><strong>定位</strong><p> ${ escapeHtml ( moduleText ) } </p></div>
${ imageGrid }
<div><strong>提问</strong><pre> ${ escapeHtml ( displayText ( question ) . slice ( 0 , 8000 ) ) } </pre></div>
</div>
</details>
` ;
} ) . join ( "" ) ;
wireAiQuestionImages ( box ) ;
}
function wireAiQuestionImages ( box ) {
box . querySelectorAll ( ".ai-question-item" ) . forEach ( ( item ) => {
item . addEventListener ( "toggle" , ( ) => {
if ( ! item . open ) return ;
item . querySelectorAll ( "img[data-ai-src]" ) . forEach ( ( image ) => {
image . src = image . dataset . aiSrc ;
image . removeAttribute ( "data-ai-src" ) ;
} ) ;
} ) ;
} ) ;
}
function shortValue ( value ) {
if ( value === null || value === undefined || value === "" ) return "空" ;
const text = typeof value === "string" ? value : JSON . stringify ( value ) ;
return text . length > 36 ? ` ${ text . slice ( 0 , 36 ) } ... ` : text ;
return text . length > 90 ? ` ${ text . slice ( 0 , 90 ) } ... ` : text ;
}
async function sampleAudit ( ) {
@@ -854,7 +1127,7 @@ function renderAuditCurrent() {
$ ( "auditDetailMeta" ) . textContent = ` ${ record . source _file || "" } · ${ record . major _department || "" } ` ;
setPdfFrame ( "auditPdfFrame" , record . pdf _url ) ;
$ ( "auditReviewStrip" ) . innerHTML = `
<div class="strip-item"><span>复核状态</span> ${ statusTag ( record . review _status ) } </div>
<div class="strip-item"><span>复核状态</span> ${ statusTag ( displayStatusValue ( record ) ) } </div>
<div class="strip-item"><span>抽查状态</span> ${ auditTag ( item . audit _status || "pending" ) } </div>
<div class="strip-item wide"><span>主要诊断</span><strong> ${ escapeHtml ( record . primary _diagnosis || "无" ) } </strong></div>
<div class="strip-item wide"><span>抽查来源</span><strong> ${ escapeHtml ( item . audit _source || "" ) } </strong></div>
@@ -958,10 +1231,107 @@ function aiAvailable() {
return Boolean ( state . ai ? . available ) ;
}
function aiActionModes ( ) {
return {
current : "default" ,
five : "default" ,
all : "default" ,
... ( state . ai ? . ai _action _modes || { } ) ,
} ;
}
function aiActionPrivacyModes ( ) {
const modes = {
current : true ,
five : true ,
all : true ,
... ( state . ai ? . ai _action _privacy _modes || { } ) ,
} ;
return {
current : modes . current !== false ,
five : modes . five !== false ,
all : modes . all !== false ,
} ;
}
function aiAllowedScopes ( ) {
const modes = aiActionModes ( ) ;
return Object . keys ( modes ) . filter ( ( scope ) => modes [ scope ] !== "off" ) ;
}
function aiActionModeLabel ( mode ) {
return ( AI _ACTION _MODE _OPTIONS . find ( ( [ value ] ) => value === mode ) || AI _ACTION _MODE _OPTIONS [ 1 ] ) [ 1 ] ;
}
function aiActionOverride ( scope ) {
const mode = aiActionModes ( ) [ scope ] || "default" ;
const override = { privacy _mode : aiActionPrivacyModes ( ) [ scope ] !== false } ;
if ( mode === "k25" ) return { ... override , model : "kimi-k2.5" , thinking _enabled : false } ;
if ( mode === "k25_thinking" ) return { ... override , model : "kimi-k2.5" , thinking _enabled : true } ;
if ( mode === "k26" ) return { ... override , model : "kimi-k2.6" , thinking _enabled : false } ;
if ( mode === "k26_thinking" ) return { ... override , model : "kimi-k2.6" , thinking _enabled : true } ;
return override ;
}
function renderAiActionModeOptions ( ) {
document . querySelectorAll ( "[data-ai-action-mode]" ) . forEach ( ( select ) => {
if ( select . options . length ) return ;
select . innerHTML = AI _ACTION _MODE _OPTIONS . map ( ( [ value , label ] ) => ` <option value=" ${ value } "> ${ label } </option> ` ) . join ( "" ) ;
} ) ;
}
function renderAiActionTable ( kimi = state . ai || { } ) {
renderAiActionModeOptions ( ) ;
const modes = { current : "default" , five : "default" , all : "default" , ... ( kimi . ai _action _modes || { } ) } ;
const privacyModes = { current : true , five : true , all : true , ... ( kimi . ai _action _privacy _modes || { } ) } ;
[
[ "current" , "aiActionModeCurrent" , "aiActionPrivacyCurrent" , "aiActionPreviewCurrent" ] ,
[ "five" , "aiActionModeFive" , "aiActionPrivacyFive" , "aiActionPreviewFive" ] ,
[ "all" , "aiActionModeAll" , "aiActionPrivacyAll" , "aiActionPreviewAll" ] ,
] . forEach ( ( [ scope , selectId , privacyId , previewId ] ) => {
const select = $ ( selectId ) ;
if ( select ) select . value = modes [ scope ] || "default" ;
const privacy = $ ( privacyId ) ;
if ( privacy ) privacy . checked = privacyModes [ scope ] !== false ;
const preview = $ ( previewId ) ;
if ( preview ) {
const mode = modes [ scope ] || "default" ;
const privacyLabel = privacyModes [ scope ] === false ? "隐私关" : "隐私开" ;
preview . textContent = mode === "off"
? "复核页隐藏"
: ` ${ AI _ACTION _SCOPE _LABELS [ scope ] } 使用 ${ aiActionModeLabel ( mode ) } · ${ privacyLabel } ` ;
}
} ) ;
renderAiPrivacyNote ( kimi ) ;
}
function renderAiPrivacyNote ( kimi = state . ai || { } ) {
const note = $ ( "aiPrivacyNote" ) ;
if ( ! note ) return ;
const modes = { current : true , five : true , all : true , ... ( kimi . ai _action _privacy _modes || { } ) } ;
const disabled = Object . entries ( modes )
. filter ( ( [ , enabled ] ) => enabled === false )
. map ( ( [ scope ] ) => AI _ACTION _SCOPE _LABELS [ scope ] || scope ) ;
note . classList . toggle ( "is-warning" , disabled . length > 0 ) ;
note . textContent = disabled . length
? ` 隐私模式按按钮生效: ${ disabled . join ( "、" ) } 已关闭, AI会上传基本信息、地址联系人等相关内容用于核验。 `
: "隐私模式全部开启: AI上传内容会排除基本信息、地址联系人、整页截图和PDF全文摘录。" ;
}
function renderAiActions ( ) {
const actions = $ ( "aiActions" ) ;
if ( ! actions ) return ;
actions . classList . toggle ( "is-hidden" , ! aiAvailable ( ) ) ;
const allowed = aiAvailable ( ) ? aiAllowedScopes ( ) : [ ] ;
actions . classList . toggle ( "is-hidden" , allowed . length === 0 ) ;
[
[ "current" , "aiCurrentBtn" ] ,
[ "five" , "aiFiveBtn" ] ,
[ "all" , "aiAllBtn" ] ,
] . forEach ( ( [ scope , id ] ) => {
const button = $ ( id ) ;
if ( ! button ) return ;
button . classList . toggle ( "is-hidden" , ! allowed . includes ( scope ) ) ;
} ) ;
}
function renderSettings ( ) {
@@ -982,9 +1352,13 @@ function renderSettings() {
$ ( "statusCheckMeta" ) . textContent = ` 上次检查: ${ status . checked _at ? formatTime ( status . checked _at ) : "尚未检查" } · 下次: ${ status . next _check _at ? formatTime ( status . next _check _at ) : "--" } ` ;
$ ( "kimiEnabled" ) . checked = Boolean ( kimi . enabled ) ;
$ ( "kimiModel" ) . value = kimi . model || "" ;
$ ( "kimiApiBase " ) . value = kimi . api _base || "" ;
$ ( "kimiApiKey " ) . value = "" ;
$ ( "kimiApiKey" ) . placeholder = kimi . api _key _configured ? "已配置,留空不变" : "粘贴 API Key" ;
$ ( "kimiConcurrency" ) . value = kimi . concurrency || 3 ;
$ ( "kimiMeta" ) . textContent = ` ${ kimi . api _key _configured ? "API Key 已从 .env 读取" : "未配置 API Key" } · ${ kimi . available ? "AI核验已启用" : "AI核验已关闭" } · 并发 ${ kimi . concurrency || 3 } ` ;
$ ( "kimiThinkingEnabled" ) . checked = Boolean ( kimi . thinking _enabled ) ;
const keySource = kimi . api _key _source === "settings" ? "设置" : kimi . api _key _source === "env" ? ".env" : "未配置" ;
$ ( "kimiMeta" ) . textContent = ` API Key: ${ keySource } · ${ kimi . available ? "AI核验已启用" : "AI核验已关闭" } · 并发 ${ kimi . concurrency || 3 } · 默认Thinking ${ kimi . thinking _enabled ? "开" : "关" } ` ;
renderAiActionTable ( kimi ) ;
$ ( "settingsUserCount" ) . textContent = ` ${ settings . users . length } 个用户 ` ;
renderPermissionControls ( "newUserPermissions" , settings . permission _labels || { } , { } ) ;
$ ( "userList" ) . innerHTML = settings . users . map ( ( user ) => renderUserCard ( user , settings . permission _labels || { } ) ) . join ( "" ) ;
@@ -1007,8 +1381,19 @@ async function saveKimiSettings(event) {
body : JSON . stringify ( {
enabled : $ ( "kimiEnabled" ) . checked ,
model : $ ( "kimiModel" ) . value . trim ( ) ,
api _base : $ ( "kimiApiBase " ) . value . trim ( ) ,
api _key : $ ( "kimiApiKey " ) . value . trim ( ) ,
concurrency : Number ( $ ( "kimiConcurrency" ) . value ) || 3 ,
ai _action _modes : {
current : $ ( "aiActionModeCurrent" ) . value ,
five : $ ( "aiActionModeFive" ) . value ,
all : $ ( "aiActionModeAll" ) . value ,
} ,
ai _action _privacy _modes : {
current : $ ( "aiActionPrivacyCurrent" ) . checked ,
five : $ ( "aiActionPrivacyFive" ) . checked ,
all : $ ( "aiActionPrivacyAll" ) . checked ,
} ,
thinking _enabled : $ ( "kimiThinkingEnabled" ) . checked ,
} ) ,
} ) ;
state . settings = data ;
@@ -1019,6 +1404,7 @@ async function saveKimiSettings(event) {
$ ( "kimiMeta" ) . textContent = error . message ;
} finally {
button . disabled = false ;
renderFilterActions ( ) ;
}
}
@@ -1028,17 +1414,51 @@ function setAiButtonsDisabled(disabled) {
} ) ;
}
function renderAiProgress ( job = state . aiJob ) {
const panel = $ ( "aiProgressPanel" ) ;
if ( ! panel ) return ;
if ( ! job || ( ! job . running && ! job . total && ! job . message ) ) {
panel . classList . add ( "is-hidden" ) ;
return ;
}
const total = Number ( job . total || 0 ) ;
const processed = Number ( job . processed || 0 ) ;
const percent = total ? Math . min ( 100 , Math . round ( ( processed / total ) * 100 ) ) : 0 ;
panel . classList . remove ( "is-hidden" ) ;
$ ( "aiProgressTitle" ) . textContent = job . running ? "AI处理中" : displayText ( job . message || "AI处理完成" ) ;
$ ( "aiProgressText" ) . textContent = total ? ` ${ processed } / ${ total } ` : "--" ;
$ ( "aiProgressBar" ) . style . width = ` ${ percent } % ` ;
$ ( "aiProgressMeta" ) . textContent = ` OK ${ job . ok || 0 } · 不OK ${ job . pending || 0 } · 失败 ${ job . failed || 0 } · 并发 ${ job . concurrency || 1 } ` ;
$ ( "aiCancelBtn" ) . disabled = ! job . running ;
}
async function cancelAiReview ( { silent = false } = { } ) {
const job = await api ( "/api/ai/review/cancel" , { method : "POST" } ) ;
state . aiJob = job ;
state . aiJobOwned = false ;
renderAiProgress ( job ) ;
setAiButtonsDisabled ( Boolean ( job . running ) ) ;
if ( ! silent ) showMessage ( displayText ( job . message || "AI核验正在中断..." ) ) ;
return job ;
}
async function pollAiReviewJob ( ) {
const job = await api ( "/api/ai/review/status" ) ;
state . aiJob = job ;
renderAiProgress ( job ) ;
const total = job . total || 0 ;
const processed = job . processed || 0 ;
showMessage ( ` AI核验 中: ${ processed } / ${ total } ,并发 ${ job . concurrency || 1 } , 无问题 ${ job . ok || 0 } , 待确认 ${ job . pending || 0 } ,失败 ${ job . failed || 0 } ` , "" ) ;
showMessage ( ` AI处理 中: ${ processed } / ${ total } ,并发 ${ job . concurrency || 1 } , OK ${ job . ok || 0 } , 不OK ${ job . pending || 0 } ,失败 ${ job . failed || 0 } ` , "" ) ;
if ( job . running ) {
if ( state . aiJobOwned && ! job . cancel _requested && state . activePage !== "review" ) switchPage ( "review" ) ;
setAiButtonsDisabled ( true ) ;
state . aiPolling = setTimeout ( ( ) => pollAiReviewJob ( ) . catch ( ( error ) => showMessage ( error . message , "error" ) ) , 2000 ) ;
return ;
}
state . aiPolling = null ;
state . aiJobOwned = false ;
setAiButtonsDisabled ( false ) ;
showMessage ( ` AI核验完成: 无问题 ${ job . ok || 0 } , 待确认 ${ job . pending || 0 } ,失败 ${ job . failed || 0 } ` , job . failed ? "error" : "ok" ) ;
showMessage ( displayText ( ` ${ job . message || "AI处理完成" } : OK ${ job . ok || 0 } , 不OK ${ job . pending || 0 } ,失败 ${ job . failed || 0 } ` ) , job . failed ? "error" : "ok" ) ;
await loadStatus ( ) . catch ( ( ) => null ) ;
await loadOverview ( ) . catch ( ( ) => null ) ;
await loadRecords ( ) ;
@@ -1047,7 +1467,11 @@ async function pollAiReviewJob() {
async function runAiReview ( scope ) {
if ( ! aiAvailable ( ) ) {
showMessage ( "Kimi AI 核验未启用,请先在设置中开启" , "error" ) ;
showMessage ( "AI 核验未启用,请先在设置中开启" , "error" ) ;
return ;
}
if ( ! aiAllowedScopes ( ) . includes ( scope ) ) {
showMessage ( "当前设置未开放这个 AI 处理范围" , "error" ) ;
return ;
}
if ( ( scope === "current" || scope === "five" ) && ! state . selectedId ) return ;
@@ -1056,10 +1480,14 @@ async function runAiReview(scope) {
setAiButtonsDisabled ( true ) ;
showMessage ( "正在启动 AI 核验..." ) ;
try {
const override = aiActionOverride ( scope ) ;
const job = await api ( "/api/ai/review" , {
method : "POST" ,
body : JSON . stringify ( { scope , record _id : state . selectedId } ) ,
body : JSON . stringify ( { scope , record _id : state . selectedId , ... override } ) ,
} ) ;
state . aiJob = job ;
state . aiJobOwned = true ;
renderAiProgress ( job ) ;
showMessage ( ` AI核验已启动: 0/ ${ job . total || 0 } ` ) ;
await pollAiReviewJob ( ) ;
} catch ( error ) {
@@ -1069,14 +1497,14 @@ async function runAiReview(scope) {
}
async function approveAiNoIssueRecords ( ) {
if ( ! window . confirm ( "确认将所有“AI复核-无问题 ”批量标记为已人工复核?" ) ) return ;
if ( ! window . confirm ( "确认将所有历史 “AI已通过 ”批量标记为已人工复核?" ) ) return ;
const button = $ ( "approveAiNoIssueBtn" ) ;
button . disabled = true ;
showMessage ( "正在批量确认 AI 无问题 记录..." ) ;
showMessage ( "正在批量确认历史 AI 已通过 记录..." ) ;
try {
const data = await api ( "/api/ai/review/approve-no-issue" , { method : "POST" } ) ;
state . status = data . status _snapshot || state . status ;
showMessage ( ` 已批量确认 ${ data . updated || 0 } 条 AI 无问题 记录 ` , "ok" ) ;
showMessage ( ` 已批量确认 ${ data . updated || 0 } 条历史 AI 已通过 记录 ` , "ok" ) ;
await loadStatus ( ) . catch ( ( ) => null ) ;
await loadOverview ( ) . catch ( ( ) => null ) ;
await loadRecords ( ) ;
@@ -1347,6 +1775,7 @@ async function boot() {
$ ( "aiCurrentBtn" ) . addEventListener ( "click" , ( ) => runAiReview ( "current" ) ) ;
$ ( "aiFiveBtn" ) . addEventListener ( "click" , ( ) => runAiReview ( "five" ) ) ;
$ ( "aiAllBtn" ) . addEventListener ( "click" , ( ) => runAiReview ( "all" ) ) ;
$ ( "aiCancelBtn" ) . addEventListener ( "click" , ( ) => cancelAiReview ( ) . catch ( ( error ) => showMessage ( error . message , "error" ) ) ) ;
$ ( "approveAiNoIssueBtn" ) . addEventListener ( "click" , approveAiNoIssueRecords ) ;
$ ( "pdfZoomOutBtn" ) . addEventListener ( "click" , ( ) => changePdfZoom ( - PDF _ZOOM _STEP ) ) ;
$ ( "pdfZoomInBtn" ) . addEventListener ( "click" , ( ) => changePdfZoom ( PDF _ZOOM _STEP ) ) ;
@@ -1365,8 +1794,44 @@ async function boot() {
$ ( "userForm" ) . addEventListener ( "submit" , createUser ) ;
$ ( "systemSettingsForm" ) . addEventListener ( "submit" , saveSystemSettings ) ;
$ ( "kimiSettingsForm" ) . addEventListener ( "submit" , saveKimiSettings ) ;
document . querySelectorAll ( "[data-ai-action-mode]" ) . forEach ( ( select ) => {
select . addEventListener ( "change" , ( ) => renderAiActionTable ( {
... ( state . ai || { } ) ,
ai _action _modes : {
current : $ ( "aiActionModeCurrent" ) . value ,
five : $ ( "aiActionModeFive" ) . value ,
all : $ ( "aiActionModeAll" ) . value ,
} ,
ai _action _privacy _modes : {
current : $ ( "aiActionPrivacyCurrent" ) . checked ,
five : $ ( "aiActionPrivacyFive" ) . checked ,
all : $ ( "aiActionPrivacyAll" ) . checked ,
} ,
} ) ) ;
} ) ;
document . querySelectorAll ( "[data-ai-action-privacy]" ) . forEach ( ( input ) => {
input . addEventListener ( "change" , ( ) => renderAiActionTable ( {
... ( state . ai || { } ) ,
ai _action _modes : {
current : $ ( "aiActionModeCurrent" ) . value ,
five : $ ( "aiActionModeFive" ) . value ,
all : $ ( "aiActionModeAll" ) . value ,
} ,
ai _action _privacy _modes : {
current : $ ( "aiActionPrivacyCurrent" ) . checked ,
five : $ ( "aiActionPrivacyFive" ) . checked ,
all : $ ( "aiActionPrivacyAll" ) . checked ,
} ,
} ) ) ;
} ) ;
$ ( "submitReviewedBtn" ) . addEventListener ( "click" , submitReviewedRecords ) ;
$ ( "statusCheckBtn" ) . addEventListener ( "click" , runStatusCheck ) ;
window . addEventListener ( "beforeunload" , ( event ) => {
if ( ! state . aiJob ? . running || ! state . aiJobOwned ) return ;
fetch ( "/api/ai/review/cancel" , { method : "POST" , keepalive : true } ) . catch ( ( ) => null ) ;
event . preventDefault ( ) ;
event . returnValue = "" ;
} ) ;
wireJsonEditorButtons ( ) ;
try {
const session = await api ( "/api/auth/me" ) ;
@@ -1386,8 +1851,15 @@ async function loadWorkspace() {
await loadStatus ( ) ;
await loadSchema ( ) ;
await loadAiConfig ( ) . catch ( ( ) => null ) ;
const job = await api ( "/api/ai/review/status" ) . catch ( ( ) => null ) ;
if ( job ) {
state . aiJob = job ;
state . aiJobOwned = false ;
renderAiProgress ( job ) ;
}
const page = canOpenPage ( state . activePage ) ? state . activePage : firstAllowedPage ( ) ;
switchPage ( page ) ;
switchPage ( job ? . running && state . aiJobOwned && canOpenPage ( "review" ) ? "review" : page) ;
if ( job ? . running ) pollAiReviewJob ( ) . catch ( ( error ) => showMessage ( error . message , "error" ) ) ;
}
boot ( ) ;