'use strict'; const $=id=>document.getElementById(id); let token='',state=null,loadedKey='',refreshing=false; const labels={queued:'待处理',preparing:'准备中',importing:'上传与导入',formatting:'排版中',awaiting_user_review:'等待人工审核',user_submitting:'已观察到人工提交',submit_unknown:'发布结果待核对',accepted_pending_moderation:'已受理 · 审核中',published:'已确认公开',needs_attention:'需要处理',cancelled:'已移出',skipped:'已跳过',rejected:'网站明确拒绝'}; function notice(message){$('notice').hidden=!message;$('notice').textContent=message;} async function api(route,body={}) { const response=await fetch('/api/'+route,{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; } function action(id,fn){$(id).addEventListener('click',async()=>{const button=$(id);button.disabled=true;try{await fn();await refresh();}catch(error){notice(error.message);}finally{button.disabled=false;}});} function text(tag,value,className){const e=document.createElement(tag);e.textContent=value;if(className)e.className=className;return e;} function render(data){ state=data;const {jobs,run,download}=data; $('total').textContent=jobs.length;$('review').textContent=jobs.filter(j=>j.state==='awaiting_user_review').length; $('accepted').textContent=jobs.filter(j=>['published','accepted_pending_moderation'].includes(j.state)).length; $('batch').textContent=run.batch?`${run.batch.count} / ${run.batch.target}`:'—';$('account').textContent=run.batch?run.batch.account.name:'尚未绑定账号'; $('run-state').textContent=run.running?'流程运行中':'流程已暂停';$('run-reason').textContent=run.reason; $('run-light').classList.toggle('gray',!run.running);$('empty').hidden=jobs.length>0; $('jobs').replaceChildren(...jobs.map(job=>{ const tr=document.createElement('tr'),title=document.createElement('td');title.append(text('div',job.title),text('small',`${job.image_count} 张配图 · ${job.source}`));if(job.original_title!==job.title)title.append(text('small','已缩短标题,原题:'+job.original_title)); const status=document.createElement('td');status.append(text('span',labels[job.state]||job.state,'badge'));if(job.error)status.append(text('small',job.error)); for(const issue of job.integrity_issues||[])status.append(text('small',issue)); const account=text('td',job.account?`${job.account.name} / ${job.fid}`:'待领取');const result=document.createElement('td'); if(job.post_url){const link=text('a','查看帖子');link.href=job.post_url;link.target='_blank';link.rel='noopener';result.append(link);} if(['user_submitting','submit_unknown'].includes(job.state)) { const input=document.createElement('input');input.type='file';input.accept='.json,application/json';input.hidden=true; const reconcile=text('button','补核已受理结果');reconcile.className='secondary';reconcile.onclick=()=>input.click(); input.onchange=async()=>{ try { if(state.run.running)throw new Error('请先暂停流程'); const file=input.files[0];if(!file)return;if(file.size>2*1024*1024)throw new Error('记录文件过大'); const record=JSON.parse(await file.text()); if(!confirm(`确认该观察记录属于“${job.title}”?补核会阻止重发,不增加当前批次计数。`))return; await api('reconcile',{id:job.id,article_key:job.article_key,confirmed:true,thread_id:String(record.threadId||record.thread_id||''),first_post_pid:String(record.postId||record.first_post_pid||''),post_url:record.url||record.post_url,title:record.title,author_uid:String(record.uid||record.author_uid||''),fid:Number(record.fid),state:record.status||record.state,evidence:{thread_id:String(record.threadId||record.thread_id||''),prompt:record.observedPrompt||'',historical:true,integrity_issues:['历史补核尚未完成发布后图文完整性检查']}}); notice('历史受理已登记,未增加当前批次计数');await refresh(); }catch(error){notice(error.message);}finally{input.value='';} }; result.append(reconcile,input); } if(!['published','accepted_pending_moderation'].includes(job.state)){ const retry=text('button','人工核对 / 处理');retry.className='secondary';retry.onclick=async()=>{ if(state.run.running){notice('请先暂停流程,再处理任务');return;} if(!confirm('只有确认网站未受理这篇文章,且已清理编辑器/草稿后才能继续。\n已受理、审核中或结果不明的文章请取消。\n你是否已经确认未受理?'))return; const again=confirm('重新加入队列?\n确定:重新排队。取消:移出队列。'); try{await api('resolve',{id:job.id,confirmed_not_accepted:true,retry:again});await refresh();}catch(error){notice(error.message);} };result.append(retry); } tr.append(title,status,account,result);return tr; })); const dlLabels={idle:'等待开始',running:'下载中',completed:'下载完成',partial:'未达到目标',cancelled:'已停止',error:'下载出错'}; $('download-state').textContent=dlLabels[download.status]||download.status;$('download-progress').textContent=`已保存 ${download.saved}/${download.target} 篇 · 跳过 ${download.skipped} · 失败 ${download.failed}`; $('progress').value=download.target?Math.floor(download.saved/download.target*100):0; $('download').disabled=download.status==='running';$('logs').textContent=download.logs.map(x=>`${x.time} ${x.message}`).join('\n')||'暂无日志'; $('browser-state').textContent=data.clients.length?`${data.clients.length} 个标签在线`:'尚未连接'; $('clients').textContent=data.clients.map(c=>`${c.account?.name||'未识别账号'} · 换号插件${c.extension?.ready?'已就绪':'未连接或需更新'}`).join('\n')||'没有在线执行标签'; } async function refresh(){if(refreshing||!token)return;refreshing=true;try{render(await api('state'));}catch(error){notice(error.message);$('run-state').textContent='本地服务连接中断';}finally{refreshing=false;}} action('pair',async()=>{const value=await api('pair');$('pair-code').hidden=false;$('pair-code').type='text';$('pair-code').value=value.token;$('pair-code').select();}); action('channels',async()=>{const value=await api('channels',{key:$('key').value.trim()});loadedKey=$('key').value.trim();$('channel').replaceChildren(...value.channels.map(c=>new Option(c,c)));}); $('key').oninput=()=>{loadedKey='';$('channel').replaceChildren(new Option('请重新获取频道',''));}; $('download-form').onsubmit=async event=>{event.preventDefault();try{if(!loadedKey||loadedKey!==$('key').value.trim())throw new Error('请先获取频道');await api('download',{key:loadedKey,channel:$('channel').value,target:Number($('target').value)});notice('下载已开始,完整保存的文章会自动入队');await refresh();}catch(error){notice(error.message);}}; action('download-stop',()=>api('download-stop')); action('scan',async()=>{notice('正在检查文章目录…');const value=await api('scan',{root:$('root').value.trim()});$('scan-result').textContent=value.results.map(x=>`${x.directory}:${x.status}`).join('\n')||'目录中未找到文章';notice('目录检查完成');}); $('config-form').onsubmit=async event=>{event.preventDefault();try{await api('configure',{fid:Number($('fid').value),minimum:Number($('minimum').value),maximum:Number($('maximum').value),interval:Number($('interval').value),history_checked:$('history-checked').checked});notice('处理规则已保存');await refresh();}catch(error){notice(error.message);}}; for(const name of ['start','pause','stop'])action(name,async()=>{await api(name);notice(name==='stop'?'流程已终止;浏览器将在下一次连接检查时停止后续操作。已发出的请求可能仍已完成。':'');}); $('history-file').onchange=async()=>{try{const file=$('history-file').files[0];if(!file)return;if(file.size>2*1024*1024)throw new Error('记录文件过大');const value=JSON.parse(await file.text());await api('history',{records:value.records});notice('历史记录已导入;仅使用标记为 block_repost 的记录');await refresh();}catch(error){notice(error.message);}}; (async()=>{try{const response=await fetch('/api/session');const session=await response.json();if(!response.ok)throw new Error(session.error);token=session.token;$('port').value=location.port;await refresh();if(state?.run.config?.fid){for(const id of ['fid','minimum','maximum','interval'])$(id).value=state.run.config[id];$('history-checked').checked=state.run.config.history_checked;}}catch(error){notice(error.message);}setInterval(refresh,2500);})();