Files
multi-simadmin/public/app.js
T

111 lines
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { escapeHtml, value, formatBytes } from './domain/formatters.js'
import { createApiClient, normalizeTargetUrl } from './infrastructure/api-client.js'
import { RequestCoordinator } from './state/request-coordinator.js'
import { filterAndSortFleet, fleetStatusKind, emptyFleetReason } from './state/fleet-view-model.js'
import { instanceDraftPayload, requestDraft, isWriteMethod } from './state/operation-draft.js'
import { createViewRouter, resolveView } from './router/view-router.js'
let instances = [], statuses = new Map(), catalog = [], activeId = null, activeFilter = 'all', activeSort = 'name'
let lastSuccessfulRefresh = null, currentView = 'home', outputText = '', outputPrettyText = '', pendingDeleteId = null, pendingApiRequest = null
let instanceDialogEpoch = 0, deleteDialogEpoch = 0, loginDialogEpoch = 0
const $ = id => document.getElementById(id), $$ = (selector, root = document) => [...root.querySelectorAll(selector)]
const requests = new RequestCoordinator(), api = createApiClient()
const routeView = createViewRouter({ controls: () => $$('[data-view]'), panels: () => $$('.view') })
const modules = [
{ name: '设备与网络', endpoints: ['/api/device','/api/sim','/api/network','/api/stats','/api/connectivity','/api/data','/api/roaming','/api/airplane-mode','/api/radio-mode','/api/band-lock','/api/cell-lock','/api/apn'] },
{ name: '短信与通话', endpoints: ['/api/sms/stats','/api/sms/list','/api/sms/conversation','/api/sms/send','/api/calls','/api/call/history'] },
{ name: '系统服务', endpoints: ['/api/work-mode','/api/esim/config','/api/notifications/config','/api/automation/config','/api/ota/status'] },
]
const firstValue = (...items) => items.find(x => x !== undefined && x !== null && x !== '') ?? null
const activeInstance = () => instances.find(x => x.id === activeId)
const activeStatus = () => statuses.get(activeId)
const tagsFromInput = text => String(text || '').split(',').map(x => x.trim()).filter(Boolean)
const statusText = s => ({ online: `在线${s?.latencyMs != null ? ` · ${s.latencyMs}ms` : ''}`, auth: '需登录', offline: '离线', unknown: '未知' })[fleetStatusKind(s)]
const showToast = message => { $('toast').textContent = message; $('toast').hidden = false; clearTimeout(showToast.timer); showToast.timer = setTimeout(() => { $('toast').hidden = true }, 3000) }
function persistentError(message = '') { $('persistentErrorText').textContent = message; $('persistentError').hidden = !message }
function setPending(button, pending, label) { button.disabled = pending; if (label) button.textContent = pending ? label : button.dataset.idleLabel }
function invalidateDialog(id) {
if (id === 'instanceDialog') { instanceDialogEpoch += 1; setPending($('saveInstanceBtn'), false, '保存中…') }
if (id === 'deleteDialog') { deleteDialogEpoch += 1; setPending($('confirmDeleteBtn'), false, '删除中…') }
if (id === 'loginDialog') { loginDialogEpoch += 1; requests.invalidate('login'); $('submitLogin').disabled = false; $('passwordInput').value = '' }
if (id === 'apiDeleteDialog') pendingApiRequest = null
}
function closeDialog(id) { invalidateDialog(id); const dialog=$(id); if(dialog.open)dialog.close() }
function frameClear() { $('frame').hidden = true; $('frame').removeAttribute('src') }
function switchView(requested) {
const view = resolveView(requested, Boolean(activeId)); currentView = view
if (view !== 'rawpage') frameClear()
if (view === 'rawpage') { const item = activeInstance(); $('frame').src = item.url; $('frame').hidden = false }
routeView(view)
const item = activeInstance(), titles = { home: '设备首页', detail: item ? `${item.name || item.id} · 设备详情` : '设备详情', rawpage: item ? `${item.name || item.id} · 原始页面` : '原始页面', api: item ? `${item.name || item.id} · API 工作台` : 'API 工作台' }
$('pageHeading').textContent = titles[view]; $('pageSubtitle').textContent = view === 'home' ? '集中巡检你的 SimAdmin 设备' : item?.url || ''
document.title = `${titles[view]} · Multi SimAdmin Island`
}
function formatSpeed(speed) {
if (!speed || typeof speed !== 'object') return '-'
if (Array.isArray(speed.interfaces)) return speed.interfaces.map(x => `${x.interface || 'net'}${formatBytes(firstValue(x.rx_bytes_per_sec,x.rx))}/s · ↑ ${formatBytes(firstValue(x.tx_bytes_per_sec,x.tx))}/s`).join(' ')
return `↓ ${formatBytes(firstValue(speed.rx,speed.download,speed.rx_bytes_per_sec))}/s · ↑ ${formatBytes(firstValue(speed.tx,speed.upload,speed.tx_bytes_per_sec))}/s`
}
function formatTemperature(temp) { if (temp == null || temp === '') return '-'; if (typeof temp === 'object') return Object.entries(temp).map(([k,v]) => `${k}: ${v}${typeof v === 'number' ? '℃' : ''}`).join(' · '); return `${temp}${typeof temp === 'number' ? '℃' : ''}` }
function renderFleetStats() {
const counts = { all: instances.length, online: 0, auth: 0, offline: 0, unknown: 0 }; for (const item of instances) counts[fleetStatusKind(statuses.get(item.id))]++
$('fleetStats').innerHTML = Object.entries({ all:'全部', online:'在线', auth:'需登录', offline:'离线', unknown:'未知' }).map(([k,label]) => `<button type="button" data-stat-filter="${k}" aria-pressed="${activeFilter === k}"><b>${counts[k]}</b><span>${label}</span></button>`).join('')
$$('[data-stat-filter]').forEach(btn => btn.addEventListener('click', () => setFilter(btn.dataset.statFilter)))
}
function setFilter(filter) { activeFilter = filter; $$('[data-filter]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.filter === filter))); renderHomeCards() }
function deviceSummary(item) { const s = statuses.get(item.id), sum = s?.summary || {}; return { s, device:sum.device||{}, sim:sum.sim||{}, network:sum.network||{}, data:sum.data||{}, sms:sum.sms||{}, system:sum.system||{}, ota:sum.ota||{} } }
function renderHomeCards() {
renderFleetStats(); const query = $('instanceSearch').value, list = filterAndSortFleet(instances, statuses, { filter:activeFilter, query, sort:activeSort }), reason = emptyFleetReason({ total:instances.length, query, filter:activeFilter, visible:list.length })
if (reason) { const copy = { config:['未配置设备','添加第一个 SimAdmin 地址,开始建设你的设备岛。'], search:['搜索无结果','没有设备匹配当前搜索词,请修改或清空搜索。'], filter:['筛选无结果','当前状态下没有设备,可切换到“全部”。'] }[reason]; $('homeDeviceGrid').innerHTML = `<section class="empty-state"><h2>${copy[0]}</h2><p>${copy[1]}</p></section>`; return }
$('homeDeviceGrid').innerHTML = list.map(item => { const { s,device,sim,network,data,sms,system,ota } = deviceSummary(item); return `<article class="sim-device-card ${fleetStatusKind(s)}"><div class="sim-card-head"><div><p class="eyebrow">${escapeHtml(item.id)}</p><h2>${escapeHtml(item.name || item.id)}</h2></div><span class="status-pill">${escapeHtml(statusText(s))}</span></div><p class="instance-url">${escapeHtml(item.url)}</p><p>${escapeHtml(item.description || '暂无描述')}</p><div class="tag-row">${(item.tags||[]).map(tag=>`<span>${escapeHtml(tag)}</span>`).join('')}</div><div class="sim-card-model">${escapeHtml([device.manufacturer,device.model].filter(Boolean).join(' ') || device.imei || '未知设备')}</div><dl class="sim-card-kpis"><div><dt>ICCID / SIM</dt><dd>${escapeHtml(sim.present === false ? '未插卡' : value(sim.iccid))}</dd></div><div><dt>信号</dt><dd>${escapeHtml(value(firstValue(network.signal,sim.signal)))}</dd></div><div><dt>温度</dt><dd>${escapeHtml(formatTemperature(system.temperature))}</dd></div><div><dt>实时流量</dt><dd>${escapeHtml(formatSpeed(system.networkSpeed))}</dd></div><div><dt>短信</dt><dd>${escapeHtml(sms.total != null ? `总 ${sms.total}` : '-')}</dd></div><div><dt>版本 / Commit</dt><dd>${escapeHtml(value(ota.currentVersion || device.firmware || device.revision))}${ota.currentCommit ? ` · ${escapeHtml(ota.currentCommit)}` : ''}</dd></div></dl><div class="card-actions"><button class="animal-btn primary-btn" type="button" data-open-detail="${escapeHtml(item.id)}">查看详情</button><button class="animal-btn default-btn" type="button" data-edit-instance="${escapeHtml(item.id)}">编辑设备</button></div></article>` }).join('')
$$('[data-open-detail]').forEach(b => b.addEventListener('click', () => openDeviceDetail(b.dataset.openDetail))); $$('[data-edit-instance]').forEach(b => b.addEventListener('click', () => openInstanceDialog(b.dataset.editInstance)))
}
function detailCard(title, rows) { return `<article class="device-info-card"><h4>${escapeHtml(title)}</h4><dl>${rows.map(([k,v])=>`<div><dt>${escapeHtml(k)}</dt><dd>${escapeHtml(value(v))}</dd></div>`).join('')}</dl></article>` }
function renderDeviceDetails() { renderDetail() }
function renderDetail() {
const item=activeInstance(), s=activeStatus(); if (!item) return; const {device,sim,network,data,sms,system,ota}=deviceSummary(item)
$('activeKicker').textContent=`${item.id} · ${statusText(s)}`; $('activeName').textContent=item.name||item.id; $('activeMeta').textContent=`${item.url}${item.description ? ` · ${item.description}` : ''}`
const signal=[['状态',statusText(s)],['运营商',network.operator||sim.operator],['信号',firstValue(network.signal,sim.signal)],['温度',formatTemperature(system.temperature)],['流量',formatSpeed(system.networkSpeed)],['数据连接',value(firstValue(data.active,data.connected,data.enabled))]]; $('signalPanel').innerHTML=signal.map(([k,v])=>`<div><span>${k}</span><b>${escapeHtml(value(v))}</b></div>`).join('')
$('overview').innerHTML=[['延迟',s?.latencyMs!=null?`${s.latencyMs}ms`:'-'],['SIM',sim.present===false?'未插卡':sim.iccid],['短信',sms.total],['版本',ota.currentVersion||device.firmware]].map(([k,v])=>`<div class="metric"><span>${k}</span><b>${escapeHtml(value(v))}</b></div>`).join('')
$('detailDeviceGrid').innerHTML=[detailCard('设备信息',[['名称',item.name],['ID',item.id],['地址',item.url],['型号',device.model],['厂商',device.manufacturer],['IMEI',device.imei],['版本',ota.currentVersion||device.firmware],['Commit',ota.currentCommit]]),detailCard('SIM 卡',[['ICCID',sim.iccid],['IMSI',sim.imsi],['号码',sim.phoneNumber],['短信中心',sim.smsCenter]]),detailCard('蜂窝网络',[['运营商',network.operator],['注册状态',network.registration],['网络制式',network.accessTechnology],['信号',network.signal]]),detailCard('系统资源',[['温度',formatTemperature(system.temperature)],['CPU',Array.isArray(system.cpuLoad)?system.cpuLoad.join(' / '):system.cpuLoad],['内存',JSON.stringify(system.memory||{})],['磁盘',JSON.stringify(system.disk||{})]])].join('')
const eps=['/api/device','/api/sim','/api/network','/api/stats','/api/sms/stats','/api/ota/status']; $('detailActions').innerHTML=`<button type="button" data-view="rawpage">打开原始管理页</button>${eps.map(ep=>`<button type="button" data-detail-ep="${ep}"><span>在工作台打开</span><code>${ep}</code></button>`).join('')}`; $$('[data-detail-ep]').forEach(b=>b.addEventListener('click',()=>{ switchView('api'); $('endpointInput').value=b.dataset.detailEp; $('methodSelect').value='GET'; updateWriteUI() })); $$('[data-view]',$('detailActions')).forEach(b=>b.addEventListener('click',()=>switchView(b.dataset.view)))
}
function openDeviceDetail(id) { if (!instances.some(x=>x.id===id)) return; if(activeId!==id){ requests.invalidate('api-console'); requests.invalidate('login'); loginDialogEpoch += 1; $('submitLogin').disabled=false; pendingApiRequest=null; $('runEndpoint').disabled=false; $('runEndpoint').textContent='发送请求'; frameClear(); clearApiOutput() } activeId=id; localStorage.setItem('multi-simadmin-active',id); const item=activeInstance(); $('openExternal').href=item.url; renderDetail(); switchView('detail') }
function setActiveHome() { requests.invalidate('api-console'); requests.invalidate('login'); loginDialogEpoch += 1; $('submitLogin').disabled=false; pendingApiRequest=null; $('runEndpoint').disabled=false; $('runEndpoint').textContent='发送请求'; activeId=null; localStorage.removeItem('multi-simadmin-active'); frameClear(); switchView('home'); renderHomeCards() }
function openInstanceDialog(id='') { instanceDialogEpoch += 1; const item=instances.find(x=>x.id===id); $('instanceDialogTitle').textContent=item?'编辑设备':'添加设备'; $('instanceOriginalId').value=item?.id||''; $('instanceIdInput').value=item?.id||''; $('instanceNameInput').value=item?.name||''; $('instanceUrlInput').value=item?.url||''; $('instanceDescInput').value=item?.description||''; $('instanceTagsInput').value=(item?.tags||[]).join(', '); $('instancePasswordInput').value=''; $('clearPasswordConfirm').checked=false; $('passwordPreserve').checked=Boolean(item); $('passwordClear').checked=!item; $('passwordPreserve').disabled=!item; $('deleteInstanceBtn').hidden=!item; $('instanceFormError').hidden=true; updatePasswordUI(); $('instanceDialog').showModal() }
function updatePasswordUI(){ const action=$('instanceForm').elements.passwordAction.value; $('newPasswordLabel').hidden=action!=='set'; $('instancePasswordInput').required=action==='set'; $('clearPasswordLabel').hidden=action!=='clear' || !$('instanceOriginalId').value }
async function saveInstanceFromForm(){ const epoch=instanceDialogEpoch, editing=Boolean($('instanceOriginalId').value), btn=$('saveInstanceBtn'); if(btn.disabled)return; btn.dataset.idleLabel='保存'; setPending(btn,true,'保存中…'); $('instanceFormError').hidden=true; try { const payload=instanceDraftPayload({ id:$('instanceIdInput').value,name:$('instanceNameInput').value,url:normalizeTargetUrl($('instanceUrlInput').value),description:$('instanceDescInput').value,tags:tagsFromInput($('instanceTagsInput').value),passwordAction:$('instanceForm').elements.passwordAction.value,password:$('instancePasswordInput').value,clearConfirmed:$('clearPasswordConfirm').checked },editing); const data=editing?await api.updateInstance($('instanceOriginalId').value,payload):await api.createInstance(payload); if(epoch!==instanceDialogEpoch)return; $('instanceDialog').close(); await reload({ selectId:data.instance?.id||payload.id }); showToast(editing?'设备已更新':'设备已添加') } catch(error){ if(epoch!==instanceDialogEpoch)return; $('instanceFormError').textContent=error.message; $('instanceFormError').hidden=false } finally { if(epoch===instanceDialogEpoch)setPending(btn,false,'保存中…') } }
function openDeleteDialog(){ const item=instances.find(x=>x.id===$('instanceOriginalId').value); deleteDialogEpoch += 1; if(!item)return; pendingDeleteId=item.id; $('deleteDetails').innerHTML=`<dl><div><dt>名称</dt><dd>${escapeHtml(item.name||item.id)}</dd></div><div><dt>ID</dt><dd>${escapeHtml(item.id)}</dd></div><div><dt>URL</dt><dd>${escapeHtml(item.url)}</dd></div></dl>`; $('deleteError').hidden=true; $('deleteDialog').showModal() }
async function confirmDelete(){ const epoch=deleteDialogEpoch, btn=$('confirmDeleteBtn'); if(btn.disabled)return; btn.dataset.idleLabel='确认删除'; setPending(btn,true,'删除中…'); try{ await api.deleteInstance(pendingDeleteId); if(epoch!==deleteDialogEpoch)return; $('deleteDialog').close(); $('instanceDialog').close(); await reload({selectId:null}); setActiveHome(); showToast('设备已删除') }catch(error){if(epoch!==deleteDialogEpoch)return;$('deleteError').textContent=error.message;$('deleteError').hidden=false}finally{if(epoch===deleteDialogEpoch)setPending(btn,false,'删除中…')} }
function renderEndpointList(){ const q=$('endpointSearch').value.trim().toLowerCase(), groups=catalog.length?catalog:modules; $('endpointList').innerHTML=groups.map(g=>{ const eps=(g.endpoints||[]).filter(ep=>ep.toLowerCase().includes(q)); return eps.length?`<section><h3>${escapeHtml(g.name||g.id)}</h3>${eps.map(ep=>`<button type="button" class="endpoint-row" data-ep="${escapeHtml(ep)}">${escapeHtml(ep)}</button>`).join('')}</section>`:'' }).join('')||'<p>没有匹配接口</p>'; $$('.endpoint-row').forEach(b=>b.addEventListener('click',()=>{ $('endpointInput').value=b.dataset.ep; $('methodSelect').value='GET'; updateWriteUI(); $$('.endpoint-row').forEach(x=>x.setAttribute('aria-pressed',String(x===b))) })) }
function updateWriteUI(){ const write=isWriteMethod($('methodSelect').value); $('writeWarning').hidden=!write; $('requestBody').disabled=$('methodSelect').value==='GET' }
function clearApiOutput(){ outputText='';outputPrettyText='';$('rawOutput').textContent='设备已切换。选择接口并点击“发送请求”。' }
function parseBody(){ const raw=$('requestBody').value.trim(); return raw?JSON.parse(raw):undefined }
async function runEndpoint(confirmed=false){
if(!activeId)return
let draft
try {
if(confirmed){ draft=pendingApiRequest; pendingApiRequest=null; if(!draft||draft.owner!==activeId)return }
else {
const method=$('methodSelect').value, endpoint=$('endpointInput').value.trim()
if(!endpoint.startsWith('/')){persistentError('接口路径必须以 / 开头');return}
const body=method==='GET'?undefined:parseBody(); draft={...requestDraft(method,endpoint,body),owner:activeId,body}
if(isWriteMethod(method)){
pendingApiRequest=draft
$('apiDeleteDetails').textContent=`${activeInstance().name||activeId} (${activeId}) · ${method} ${endpoint}`
$('apiDeleteBody').textContent=body===undefined?'(无请求体)':JSON.stringify(body,null,2)
$('apiDeleteDialog').showModal(); return
}
}
} catch(error){ persistentError(`请求参数无效:${error.message||error}`); return }
const {owner,method,path:endpoint,body}=draft, btn=$('runEndpoint'), token=requests.next('api-console'); if(btn.disabled)return
btn.dataset.idleLabel='发送请求'; setPending(btn,true,'发送中…'); $('rawOutput').textContent='请求进行中…'; persistentError(''); const started=performance.now()
try{ const init={method}; if(isWriteMethod(method)){ const prepared=await api.prepareConfirmation({instanceId:owner,method,path:endpoint,body}); if(!requests.isCurrent('api-console',token)||owner!==activeId)return; init.headers={'content-type':'application/json','x-confirmation-token':prepared.token}; if(body!==undefined)init.body=JSON.stringify(body) } const res=await api.proxy(owner,endpoint,init), text=await res.text(); if(!requests.isCurrent('api-console',token)||owner!==activeId)return; const elapsed=Math.round(performance.now()-started); outputText=text; try{outputPrettyText=JSON.stringify(JSON.parse(text),null,2)}catch{outputPrettyText=text} $('rawOutput').textContent=`HTTP ${res.status} ${res.statusText} · ${elapsed}ms · ${draft.method} ${draft.path}\n\n${$('outputPretty').getAttribute('aria-pressed')==='true'?outputPrettyText:outputText}`; if(!res.ok)persistentError(`API 请求失败:HTTP ${res.status} ${res.statusText}\n${text}`) }catch(error){ if(requests.isCurrent('api-console',token)&&owner===activeId){$('rawOutput').textContent=error.message||String(error);persistentError(`API 请求失败:${error.message||error}`)} }finally{ if(requests.isCurrent('api-console',token)) setPending(btn,false,'发送中…') }
}
async function refreshStatus({silent=false}={}){ const btn=$('refreshBtn'), token=requests.next('fleet-status'); btn.disabled=true; try{const data=await api.status(); if(!requests.isCurrent('fleet-status',token))return; statuses=new Map((data.instances||[]).map(x=>[x.id,x]));lastSuccessfulRefresh=new Date();$('staleBanner').hidden=true;persistentError('');renderHomeCards();if(activeId)renderDetail();if(!silent)showToast(`已刷新 ${statuses.size} 个设备`)}catch(error){if(!requests.isCurrent('fleet-status',token))return;$('staleBanner').textContent=`刷新失败,当前显示上次成功数据。上次成功:${lastSuccessfulRefresh?lastSuccessfulRefresh.toLocaleString():'尚无'}${error.message}`;$('staleBanner').hidden=false;persistentError(`状态刷新失败:${error.message}`)}finally{if(requests.isCurrent('fleet-status',token))btn.disabled=false} }
async function loadConfig(){ const [data,cat]=await Promise.all([api.config(),api.catalog().catch(()=>({groups:[]}))]);instances=data.instances||[];$('configPath').textContent=data.configPath||'config.json';catalog=cat.groups||[];renderEndpointList();const saved=localStorage.getItem('multi-simadmin-active');activeId=saved&&instances.some(x=>x.id===saved)?saved:null;renderHomeCards() }
async function reload({selectId=activeId}={}){await loadConfig();await refreshStatus({silent:true});if(selectId&&instances.some(x=>x.id===selectId))openDeviceDetail(selectId);else setActiveHome()}
async function initialize(){ $('initialLoading').hidden=false;$('fatalState').hidden=true;$('homeContent').hidden=true;persistentError('');try{await loadConfig();await refreshStatus({silent:true});$('initialLoading').hidden=true;$('homeContent').hidden=false;if(activeId){renderDetail();switchView(resolveView(localStorage.getItem('multi-simadmin-view')||'home',true))}else switchView('home')}catch(error){$('initialLoading').hidden=true;$('fatalText').textContent=error.message;$('fatalState').hidden=false} }
function bindEvents(){ $$('[data-dialog-cancel]').forEach(button=>button.addEventListener('click',()=>closeDialog(button.dataset.dialogCancel))); for(const id of ['instanceDialog','deleteDialog','loginDialog','apiDeleteDialog']) $(id).addEventListener('cancel',()=>invalidateDialog(id)); $('refreshBtn').addEventListener('click',()=>refreshStatus());$('addInstanceBtn').addEventListener('click',()=>openInstanceDialog());$('backToFleetBtn').addEventListener('click',setActiveHome);$('editActiveBtn').addEventListener('click',()=>openInstanceDialog(activeId));$('instanceSearch').addEventListener('input',renderHomeCards);$('sortSelect').addEventListener('change',()=>{activeSort=$('sortSelect').value;renderHomeCards()});$('densityBtn').addEventListener('click',()=>{const compact=document.body.classList.toggle('compact');$('densityBtn').setAttribute('aria-pressed',String(compact))});$$('[data-filter]').forEach(b=>b.addEventListener('click',()=>setFilter(b.dataset.filter)));$$('[data-view]').forEach(b=>b.addEventListener('click',e=>{e.preventDefault();switchView(b.dataset.view)}));$$('[name=passwordAction]').forEach(r=>r.addEventListener('change',updatePasswordUI));$('instanceForm').addEventListener('submit',e=>{e.preventDefault();saveInstanceFromForm()});$('deleteInstanceBtn').addEventListener('click',openDeleteDialog);$('confirmDeleteBtn').addEventListener('click',e=>{e.preventDefault();confirmDelete()});$('endpointSearch').addEventListener('input',renderEndpointList);$('methodSelect').addEventListener('change',updateWriteUI);$('runEndpoint').addEventListener('click',()=>runEndpoint());$('confirmApiDelete').addEventListener('click',e=>{e.preventDefault();$('apiDeleteDialog').close();runEndpoint(true)});$('outputPretty').addEventListener('click',()=>{$('outputPretty').setAttribute('aria-pressed','true');$('outputRaw').setAttribute('aria-pressed','false');if(outputText)$('rawOutput').textContent=$('rawOutput').textContent.split('\n\n')[0]+'\n\n'+outputPrettyText});$('outputRaw').addEventListener('click',()=>{$('outputPretty').setAttribute('aria-pressed','false');$('outputRaw').setAttribute('aria-pressed','true');if(outputText)$('rawOutput').textContent=$('rawOutput').textContent.split('\n\n')[0]+'\n\n'+outputText});$('copyOutput').addEventListener('click',async()=>{try{await navigator.clipboard.writeText($('rawOutput').textContent);showToast('结果已复制')}catch{showToast('复制不可用,请手动选择文本')}});$('dismissError').addEventListener('click',()=>persistentError(''));$('retryInit').addEventListener('click',initialize);$('loginBtn').addEventListener('click',()=>{loginDialogEpoch+=1;$('submitLogin').disabled=false;$('loginDialog').dataset.instanceId=activeId;$('passwordInput').value='';$('loginError').textContent='';$('loginError').hidden=true;$('loginDialog').showModal()});$('loginForm').addEventListener('submit',async e=>{e.preventDefault();const epoch=loginDialogEpoch,id=$('loginDialog').dataset.instanceId,btn=$('submitLogin');btn.disabled=true;$('loginError').hidden=true;const password=$('passwordInput').value;const result=await requests.run('login',id,()=>api.login(id,password));$('passwordInput').value='';if(epoch!==loginDialogEpoch)return;if(result.accepted&&id===activeId){if(result.error){$('loginError').textContent=result.error.message;$('loginError').hidden=false}else if(!result.value.authenticated){$('loginError').textContent='登录失败,请检查密码';$('loginError').hidden=false}else{$('loginDialog').close();await refreshStatus({silent:true});showToast('会话已刷新')}}if(epoch===loginDialogEpoch)btn.disabled=false});updateWriteUI() }
bindEvents(); await initialize()