117117 .modal-body .row { margin-bottom : 12px ; }
118118 .modal-body .row label { display : block; color : var (--muted ); font-size : 12px ; margin-bottom : 4px ; }
119119 .modal-body .row .val { font-family : ui-monospace, monospace; font-size : 13px ; word-break : break-all; }
120+ .btn-reveal-fix {
121+ padding : 4px 10px ;
122+ font-size : 12px ;
123+ background : transparent;
124+ color : var (--warn );
125+ border : 1px solid var (--warn );
126+ border-radius : 4px ;
127+ cursor : pointer;
128+ margin-right : 8px ;
129+ }
130+ .btn-reveal-fix : hover { opacity : 0.9 ; }
131+ .btn-copy-cmd {
132+ padding : 4px 10px ;
133+ font-size : 12px ;
134+ background : transparent;
135+ color : var (--accent );
136+ border : 1px solid var (--accent );
137+ border-radius : 4px ;
138+ cursor : pointer;
139+ margin-right : 8px ;
140+ }
141+ .btn-copy-cmd : hover { opacity : 0.9 ; }
120142 .modal-body input , .modal-body select { width : 100% ; padding : 8px 12px ; background : var (--bg ); border : 1px solid var (--border ); border-radius : 6px ; color : var (--text ); margin-top : 4px ; }
121143 .modal-footer { padding : 12px 20px ; border-top : 1px solid var (--border ); }
122144 .modal-footer button { margin-right : 8px ; }
@@ -197,7 +219,7 @@ <h2>Runner 列表</h2>
197219 < td >
198220 < span class ="badge {{.Status}} "> {{.Status}}</ span >
199221 {{if .Running}}< span class ="badge running "> 运行中</ span > {{end}}
200- {{if .ProbeError }}< br > < span class ="probe-err " title ="{{.ProbeError }} "> 探测失败</ span > {{end}}
222+ {{if .Probe }}< br > < span class ="probe-err " title ="{{.Probe.Error }} "> 探测失败{{if .Probe.Type}} ({{.Probe.Type}}){{end}} </ span > {{end}}
201223 </ td >
202224 {{if $.Config.Runners.ContainerMode}}< td > < code > {{.JobDockerBackend}}</ code > </ td > {{end}}
203225 < td >
@@ -222,6 +244,10 @@ <h2>Runner 列表</h2>
222244 < td >
223245 {{if and (eq .Status "installed") (not .Running)}}< button type ="button " class ="btn-start " data-name ="{{.Name}} " title ="启动 Runner "> 启动</ button > {{end}}
224246 {{if .Running}}< button type ="button " class ="btn-stop " data-name ="{{.Name}} " title ="停止 Runner "> 停止</ button > {{end}}
247+ {{if eq .Status "unknown"}}
248+ < button type ="button " class ="btn-start " data-name ="{{.Name}} " title ="状态探测失败,尝试启动 Runner "> 启动</ button >
249+ < button type ="button " class ="btn-stop " data-name ="{{.Name}} " title ="状态探测失败,尝试停止 Runner "> 停止</ button >
250+ {{end}}
225251 < button type ="button " class ="btn-view " data-name ="{{.Name}} " title ="查看配置 "> 查看</ button >
226252 < button type ="button " class ="btn-edit " data-name ="{{.Name}} " title ="编辑配置 "> 编辑</ button >
227253 < button type ="button " class ="btn-del " data-name ="{{.Name}} " title ="从配置中移除 "> 删除</ button >
@@ -285,6 +311,10 @@ <h3 id="modalTitle">Runner 配置</h3>
285311 < div class ="row "> < label > 安装目录</ label > < div class ="val path " id ="vInstallDir "> </ div > </ div >
286312 < div class ="row " id ="vJobDockerBackendRow " style ="display:none "> < label > Docker 后端</ label > < div class ="val "> < code id ="vJobDockerBackend "> </ code > </ div > </ div >
287313 < div class ="row "> < label > 状态</ label > < div class ="val "> < span id ="vStatus "> </ span > < span id ="vRunning "> </ span > </ div > </ div >
314+ < div class ="row "> < label > 探测错误类型</ label > < div class ="val " id ="vProbeErrorType "> </ div > </ div >
315+ < div class ="row "> < label > 建议操作</ label > < div class ="val " id ="vProbeSuggestion "> </ div > </ div >
316+ < div class ="row "> < label > 检查命令(只读)</ label > < div class ="val "> < button type ="button " id ="vCopyCheckCmdBtn " class ="btn-copy-cmd " style ="display:none "> 复制命令</ button > < span id ="vProbeCheckCommand "> </ span > </ div > </ div >
317+ < div class ="row "> < label > 修复命令(有副作用)</ label > < div class ="val "> < button type ="button " id ="vRevealFixCmdBtn " class ="btn-reveal-fix " style ="display:none "> 显示修复命令</ button > < button type ="button " id ="vCopyFixCmdBtn " class ="btn-copy-cmd " style ="display:none "> 复制命令</ button > < span id ="vProbeFixCommand "> </ span > </ div > </ div >
288318 < div class ="row "> < label > 探测错误</ label > < div class ="val " id ="vProbeError "> </ div > </ div >
289319 < div class ="row "> < label > 注册结果</ label > < div class ="val " id ="vRegistrationMessage "> </ div > </ div >
290320 < div class ="row "> < label > 注册检查时间</ label > < div class ="val " id ="vRegistrationCheckedAt "> </ div > </ div >
@@ -443,13 +473,69 @@ <h3 id="modalTitle">Runner 配置</h3>
443473 d . textContent = s ;
444474 return d . innerHTML ;
445475 }
476+ function resolveProbeSuggestion ( data ) {
477+ if ( data && data . probe && data . probe . suggestion ) return data . probe . suggestion ;
478+ return '请先尝试“停止/启动”进行自愈;若仍失败,查看 manager 与 runner 容器日志。' ;
479+ }
480+ function resolveProbeError ( data ) {
481+ if ( data && data . probe && data . probe . error ) return data . probe . error ;
482+ return '' ;
483+ }
484+ function resolveProbeCheckCommand ( data ) {
485+ if ( data && data . probe && data . probe . check_command ) return data . probe . check_command ;
486+ return 'docker compose ps && docker logs --tail=200 runner-manager' ;
487+ }
488+ function resolveProbeFixCommand ( data ) {
489+ if ( data && data . probe && data . probe . fix_command ) return data . probe . fix_command ;
490+ return 'docker compose up -d --force-recreate' ;
491+ }
492+ function resolveProbeType ( data ) {
493+ if ( data && data . probe && data . probe . type ) return data . probe . type ;
494+ return 'unknown' ;
495+ }
446496 const modal = document . getElementById ( 'runnerModal' ) ;
447497 const modalTitle = document . getElementById ( 'modalTitle' ) ;
448498 const modalView = document . getElementById ( 'modalView' ) ;
449499 const modalEditForm = document . getElementById ( 'modalEditForm' ) ;
450500 const modalEditBtn = document . getElementById ( 'modalEditBtn' ) ;
451501 const modalSaveBtn = document . getElementById ( 'modalSaveBtn' ) ;
452502 const modalMsg = document . getElementById ( 'modalMsg' ) ;
503+ const revealFixBtn = document . getElementById ( 'vRevealFixCmdBtn' ) ;
504+ const copyCheckBtn = document . getElementById ( 'vCopyCheckCmdBtn' ) ;
505+ const copyFixBtn = document . getElementById ( 'vCopyFixCmdBtn' ) ;
506+ const probeFixCmdEl = document . getElementById ( 'vProbeFixCommand' ) ;
507+ const probeCheckCmdEl = document . getElementById ( 'vProbeCheckCommand' ) ;
508+ let currentProbeCheckCommand = '' ;
509+ let currentProbeFixCommand = '' ;
510+ let probeFixRevealed = false ;
511+
512+ async function copyCommandText ( text ) {
513+ if ( ! text ) return false ;
514+ try {
515+ if ( navigator . clipboard && navigator . clipboard . writeText ) {
516+ await navigator . clipboard . writeText ( text ) ;
517+ return true ;
518+ }
519+ } catch ( _ ) {
520+ // ignore and fallback below
521+ }
522+ // 回退:在不支持/拒绝 clipboard API 的环境里尝试 execCommand
523+ try {
524+ const ta = document . createElement ( 'textarea' ) ;
525+ ta . value = text ;
526+ ta . setAttribute ( 'readonly' , '' ) ;
527+ ta . style . position = 'fixed' ;
528+ ta . style . top = '-9999px' ;
529+ document . body . appendChild ( ta ) ;
530+ ta . select ( ) ;
531+ ta . setSelectionRange ( 0 , ta . value . length ) ;
532+ const ok = document . execCommand && document . execCommand ( 'copy' ) ;
533+ document . body . removeChild ( ta ) ;
534+ return ! ! ok ;
535+ } catch ( _ ) {
536+ return false ;
537+ }
538+ }
453539
454540 function openModal ( mode , name ) {
455541 modalMsg . style . display = 'none' ;
@@ -479,7 +565,18 @@ <h3 id="modalTitle">Runner 配置</h3>
479565 }
480566 document . getElementById ( 'vStatus' ) . innerHTML = '<span class="badge ' + escapeHtml ( data . status || '' ) + '">' + escapeHtml ( data . status || '' ) + '</span>' ;
481567 document . getElementById ( 'vRunning' ) . innerHTML = data . running ? ' <span class="badge running">运行中</span>' : '' ;
482- document . getElementById ( 'vProbeError' ) . textContent = data . probe_error || '—' ;
568+ const probeError = resolveProbeError ( data ) ;
569+ document . getElementById ( 'vProbeErrorType' ) . textContent = probeError ? resolveProbeType ( data ) : '—' ;
570+ document . getElementById ( 'vProbeSuggestion' ) . textContent = probeError ? resolveProbeSuggestion ( data ) : '—' ;
571+ currentProbeCheckCommand = probeError ? resolveProbeCheckCommand ( data ) : '' ;
572+ probeCheckCmdEl . textContent = currentProbeCheckCommand || '—' ;
573+ copyCheckBtn . style . display = currentProbeCheckCommand ? 'inline-block' : 'none' ;
574+ currentProbeFixCommand = probeError ? resolveProbeFixCommand ( data ) : '' ;
575+ probeFixRevealed = false ;
576+ probeFixCmdEl . textContent = probeError ? '(默认隐藏,点击“显示修复命令”)' : '—' ;
577+ revealFixBtn . style . display = probeError ? 'inline-block' : 'none' ;
578+ copyFixBtn . style . display = 'none' ;
579+ document . getElementById ( 'vProbeError' ) . textContent = probeError || '—' ;
483580 document . getElementById ( 'vRegistrationMessage' ) . textContent = data . registration_message || '—' ;
484581 document . getElementById ( 'vRegistrationCheckedAt' ) . textContent = data . registration_checked_at || '—' ;
485582 var gh = data . registered_on_github ;
@@ -497,6 +594,11 @@ <h3 id="modalTitle">Runner 配置</h3>
497594 startStopSpan . style . display = 'inline-block' ;
498595 startBtn . style . display = data . running ? 'none' : 'inline-block' ;
499596 stopBtn . style . display = data . running ? 'inline-block' : 'none' ;
597+ } else if ( data . status === 'unknown' ) {
598+ // 状态探测失败时允许用户手动尝试启停进行自愈
599+ startStopSpan . style . display = 'inline-block' ;
600+ startBtn . style . display = 'inline-block' ;
601+ stopBtn . style . display = 'inline-block' ;
500602 } else {
501603 startBtn . style . display = 'none' ;
502604 stopBtn . style . display = 'none' ;
@@ -600,14 +702,45 @@ <h3 id="modalTitle">Runner 配置</h3>
600702 const r = await fetch ( '/api/runners/' + encodeURIComponent ( name ) + '/' + action , { method : 'POST' } ) ;
601703 const data = await r . json ( ) . catch ( ( ) => ( { } ) ) ;
602704 if ( r . ok ) {
603- if ( data && data . probe_error ) {
604- alert ( ( data . message || '操作已执行' ) + '\n\n探测错误:' + data . probe_error ) ;
705+ const probeError = resolveProbeError ( data ) ;
706+ if ( probeError ) {
707+ const probeType = resolveProbeType ( data ) ;
708+ const checkMsg =
709+ ( data . message || '操作已执行' ) +
710+ '\n\n探测错误类型:' + probeType +
711+ '\n建议:' + resolveProbeSuggestion ( data ) +
712+ '\n检查命令:' + resolveProbeCheckCommand ( data ) +
713+ '\n探测错误:' + probeError ;
714+ alert ( checkMsg ) ;
715+ if ( confirm ( '是否显示修复命令(有副作用)?建议先执行检查命令确认。' ) ) {
716+ alert ( '修复命令:\n' + resolveProbeFixCommand ( data ) ) ;
717+ }
605718 }
606719 location . reload ( ) ;
607720 }
608721 else { alert ( data . message || r . statusText || '请求失败' ) ; }
609722 } catch ( e ) { alert ( e . message ) ; }
610723 }
724+ revealFixBtn . addEventListener ( 'click' , ( ) => {
725+ if ( ! currentProbeFixCommand ) return ;
726+ if ( ! confirm ( '修复命令可能有副作用,确认显示?' ) ) return ;
727+ probeFixCmdEl . textContent = currentProbeFixCommand ;
728+ probeFixRevealed = true ;
729+ copyFixBtn . style . display = 'inline-block' ;
730+ } ) ;
731+ copyCheckBtn . addEventListener ( 'click' , async ( ) => {
732+ if ( ! currentProbeCheckCommand ) return ;
733+ const ok = await copyCommandText ( currentProbeCheckCommand ) ;
734+ alert ( ok ? '检查命令已复制' : '复制失败,请手动复制' ) ;
735+ } ) ;
736+ copyFixBtn . addEventListener ( 'click' , async ( ) => {
737+ if ( ! probeFixRevealed || ! currentProbeFixCommand ) {
738+ alert ( '请先显示修复命令并确认后再复制' ) ;
739+ return ;
740+ }
741+ const ok = await copyCommandText ( currentProbeFixCommand ) ;
742+ alert ( ok ? '修复命令已复制' : '复制失败,请手动复制' ) ;
743+ } ) ;
611744 document . querySelectorAll ( '.btn-start' ) . forEach ( btn => {
612745 if ( btn . id === 'modalStartBtnFooter' ) return ;
613746 btn . addEventListener ( 'click' , ( ) => runnerAction ( btn . getAttribute ( 'data-name' ) , 'start' ) ) ;
0 commit comments