'use strict'; const $ = id => document.getElementById(id); let token = '', running = false, starting = false, loadingChannels = false, loadedKey = '', lastLog = -1; const labels = {idle:'等待开始',running:'下载中',completed:'已完成',partial:'未达到目标',cancelled:'已停止',error:'任务中断'}; const key = () => $('key').value.trim(); function controls() { const busy = running || starting; $('start').disabled = busy || !loadedKey || loadedKey !== key() || !$('channel').value; $('stop').disabled = !running; $('channels').disabled = busy || loadingChannels; for (const id of ['key','channel','target']) $(id).disabled = busy || (id === 'channel' && !loadedKey); } async function post(path, body) { const response = await fetch('/api/' + path, {method:'POST',headers:{'Content-Type':'application/json','X-Session-Token':token},body:JSON.stringify(body)}); const data = await response.json(); if (!response.ok) throw new Error(data.error || '请求失败'); return data; } $('key').addEventListener('input', () => {loadedKey=''; $('channel').replaceChildren(new Option('请重新获取频道','')); controls();}); $('channels').addEventListener('click', async () => { loadingChannels=true; controls(); $('notice').textContent='正在获取频道…'; const submittedKey = key(); try { const data = await post('channels',{key:submittedKey}); if (key() !== submittedKey) return; $('channel').replaceChildren(...data.channels.map(c => new Option(c,c))); loadedKey=submittedKey; $('notice').textContent=`已获取 ${data.channels.length} 个频道。`; $('debug-channel').value=$('channel').value; } catch(error) {$('notice').textContent=error.message;} finally {loadingChannels=false; controls();} }); $('channel').addEventListener('change', () => {$('debug-channel').value=$('channel').value; controls();}); $('download-form').addEventListener('submit', async event => { event.preventDefault(); starting=true; controls(); $('notice').textContent=''; try {await post('start',{key:key(),channel:$('channel').value,target:Number($('target').value)}); await refresh();} catch(error) {$('notice').textContent=error.message;} finally {starting=false; controls();} }); $('stop').addEventListener('click', async () => { $('stop').disabled=true; try {await post('stop',{}); $('notice').textContent='正在停止,等待当前请求或文章保存结束…';} catch(error) {$('notice').textContent=error.message;} }); $('debug-form').addEventListener('submit', async event => { event.preventDefault(); $('debug-submit').disabled=true; $('debug-output').textContent='正在请求…'; try { const data=await post('debug',{key:key(),endpoint:$('endpoint').value,channel:$('debug-channel').value,num:Number($('debug-num').value),start:Number($('debug-start').value)}); const output=JSON.stringify(data,null,2); $('debug-output').textContent=output.slice(0,100000)+(output.length>100000?'\n…输出超过 100000 字符,已截断。':''); } catch(error) {$('debug-output').textContent=error.message;} finally {$('debug-submit').disabled=false;} }); async function refresh() { const response=await fetch('/api/state'); if (!response.ok) throw new Error('无法读取下载状态'); const data=await response.json(); running=data.status==='running'; controls(); const percent=data.target ? Math.floor(data.saved/data.target*100) : 0; $('status').textContent=labels[data.status] || data.status; $('progress-text').textContent=`下载新闻 ${data.saved}/${data.target} 条`; $('percent').textContent=`已完成 ${percent}%`; $('progress').value=percent; for (const id of ['saved','skipped','failed','requests','message','output']) $(id).textContent=data[id]; const latest=data.logs.length ? data.logs[data.logs.length-1].id : 0; if (latest!==lastLog) { const logs=$('logs'); const bottom=logs.scrollHeight-logs.scrollTop-logs.clientHeight<40; logs.replaceChildren(...data.logs.map(log => {const row=document.createElement('div'); const stamp=document.createElement('time'); stamp.textContent=log.time; row.append(stamp,document.createTextNode(log.message)); return row;})); if (bottom) logs.scrollTop=logs.scrollHeight; lastLog=latest; } } async function poll() { try {await refresh();} catch(error) {$('notice').textContent='与本地服务连接中断,请检查启动窗口。';} setTimeout(poll,700); } (async () => { try {const response=await fetch('/api/session'); token=(await response.json()).token; await poll();} catch(error) {$('notice').textContent='初始化失败,请刷新页面。';} })();