Files
auto_news_upload/browser/runner.js
T

211 lines
12 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.
/* Page-side orchestrator. This file never clicks or submits the publication form. */
(() => {
'use strict';
if (window.DWW11NewsRunner) return;
window.DWW11NewsRunner = true;
const SLOT='auto-news-current';
const owner=sessionStorage.getItem('auto-news-owner') || crypto.randomUUID();
sessionStorage.setItem('auto-news-owner',owner);
let run=null, working=false, halted=true, aborter=null, active=null, tickBusy=false;
let skipBusy=false, preparation=null;
const pending=new Map();
function rpc(route,body={}) {
return new Promise((resolve,reject)=>{
const id=crypto.randomUUID();
const timer=setTimeout(()=>{pending.delete(id);reject(new Error('本地连接无响应'));},65000);
pending.set(id,{resolve,reject,timer});
document.dispatchEvent(new CustomEvent('dww11:local-request',{detail:JSON.stringify({id,route,body:{owner,...body}})}));
});
}
document.addEventListener('dww11:local-response',event=>{
let data;try{data=JSON.parse(event.detail);}catch{return;}
const waiter=pending.get(data.id);if(!waiter)return;
clearTimeout(waiter.timer);pending.delete(data.id);
data.error?waiter.reject(new Error(data.error)):waiter.resolve(data.value);
});
const status=message=>document.dispatchEvent(new CustomEvent('dww11:connection-status',{detail:message}));
document.addEventListener('dww11:auto-stop',()=>{halted=true;aborter?.abort(new Error('流程已终止'));});
function account() {
const element=document.querySelector('#mumucms_username');
if(!element)return null;
const name=[...element.childNodes].filter(n=>n.nodeType===Node.TEXT_NODE).map(n=>n.textContent).join('').trim();
const links=[element,...element.querySelectorAll('a')].filter(e=>e.href);
let uid=null;
for(const link of links) {
const href=link.href;
uid=new URL(href,location.href).searchParams.get('uid') || href.match(/space-uid-(\d+)/)?.[1];
if(uid)break;
}
return uid&&name?{uid,name}:null;
}
function extension() {
try{return JSON.parse(document.querySelector('#dww11-extension-panel')?.getAttribute('data-auto-news-state') || '{}');}catch{return {};}
}
function assertLive(epoch) {
if(halted||!run?.running||run.epoch!==epoch)throw new Error('流程已暂停或终止');
if(active?.account?.uid!==account()?.uid)throw new Error('当前账号与任务不一致');
}
function editorSnapshot() {
const body=document.querySelector('#e_iframe')?.contentDocument?.body;
if(!body)throw new Error('找不到可视化编辑器');
return {title:document.querySelector('#subject')?.value || '',
images:[...body.querySelectorAll('img')].map(x=>({id:x.id,aid:x.getAttribute('aid'),src:x.getAttribute('src')})),
// Snapshot only final submitted content; never includes hidden tokens or the full form.
html:body.innerHTML.slice(0,500000), text:body.innerText.slice(0,100000)};
}
function saveLocal(value) {sessionStorage.setItem(SLOT,JSON.stringify(value));}
function loadLocal(){try{return JSON.parse(sessionStorage.getItem(SLOT)||'null');}catch{return null;}}
async function event(state,extra={}) {return rpc('event',{id:active.id,state,epoch:run.epoch,...extra});}
async function fail(error) {
if(skipBusy)return;
status(error.message);
if(active)try{await event('needs_attention',{error:error.message});}catch{}
halted=true;aborter?.abort();document.dispatchEvent(new CustomEvent('dww11:auto-stop'));
}
function editorPage(job) {
const u=new URL(location.href);
return u.searchParams.get('mod')==='post'&&u.searchParams.get('action')==='newthread'&&u.searchParams.get('fid')===String(job.fid);
}
async function prepare(job) {
working=true;active=job;aborter=new AbortController();const epoch=run.epoch;
try {
assertLive(epoch);
if(!editorPage(job)) {
saveLocal({id:job.id});
location.assign(`https://www.dww11.com/forum.php?mod=post&action=newthread&fid=${job.fid}`);return;
}
if(!window.DWW11Importer||!window.DWW11Formatter)throw new Error('导入或排版模块尚未就绪,请检查安装');
status('正在检查文章包…');
const pack=await rpc('package',{id:job.id});assertLive(epoch);
const prepared=await window.DWW11Importer.stagePackage(pack);assertLive(epoch);
if(prepared.warnings?.length)throw new Error('文章需要人工处理:'+prepared.warnings.join(';'));
await event('importing');assertLive(epoch);status('上传图片并填写文章…');
const result=await window.DWW11Importer.importPrepared({automatic:true,signal:aborter.signal});assertLive(epoch);
if(result?.cancelled)throw new Error('导入已取消');
await event('formatting');assertLive(epoch);
window.DWW11Formatter.format(job.source);assertLive(epoch);
const snapshot=editorSnapshot();
if(snapshot.title!==job.title)throw new Error('标题校验失败');
await window.DWW11Importer.verifyEditorImages(
document.querySelector('#e_iframe').contentDocument.body, result.attachmentOrder, aborter.signal);
assertLive(epoch);
await event('awaiting_user_review',{snapshot});assertLive(epoch);
active={...job,state:'awaiting_user_review'};saveLocal({id:job.id});
status('已完成排版,请核对内容并亲手点击“发表帖子”。');
}catch(error){await fail(error);}finally{working=false;}
}
// Record only trusted user activation. Never replay a click, call submit(), or send a post request.
document.addEventListener('click',event=>{
if(skipBusy&&event.target.closest?.('#postsubmit')) {
event.preventDefault();event.stopImmediatePropagation();return;
}
if(!event.isTrusted||!event.target.closest?.('#postsubmit')||active?.state!=='awaiting_user_review')return;
try {
const snapshot=editorSnapshot();
const local={id:active.id,snapshot,clickedAt:Date.now(),observed:false};saveLocal(local);
rpc('event',{id:active.id,state:'user_submitting',epoch:run?.epoch,snapshot})
.then(()=>{const latest=loadLocal();if(latest?.clickedAt===local.clickedAt)saveLocal({...local,observed:true});})
.catch(error=>status('提交记录待恢复:'+error.message));
active.state='user_submitting';status('已观察到人工点击,等待结果页核对。');
}catch(error){status('提交快照记录失败:'+error.message);}
},true);
document.addEventListener('submit',event=>{
if(skipBusy&&event.target.id==='postform') {event.preventDefault();event.stopImmediatePropagation();}
},true);
document.addEventListener('dww11:skip-article',async()=>{
if(skipBusy)return;
if(!active||!run||run.owner!==owner) {status('当前标签没有可跳过的文章');return;}
if(!['preparing','importing','formatting','awaiting_user_review','needs_attention'].includes(active.state)) {
status('文章已提交或结果待核对,请先人工核对');return;
}
skipBusy=true;halted=true;
document.dispatchEvent(new CustomEvent('dww11:skip-busy',{detail:true}));
const jobId=active.id,epoch=run.epoch;
aborter?.abort(new Error('正在跳过当前文章'));
status('正在跳过当前文章…');
try {
// Let cancelled imports finish their rollback before leaving the editor.
await preparation;
const result=await rpc('skip',{id:jobId,epoch});
sessionStorage.removeItem(SLOT);active=null;run=result.run;
status('已跳过,正在打开下一篇文章的发帖页…');
// Always load a fresh editor, including when the fid and URL are unchanged.
location.assign(`https://www.dww11.com/forum.php?mod=post&action=newthread&fid=${result.fid}`);
}catch(error){
try{await rpc('stop');}catch{}
status('跳过未完成,流程已暂停:'+error.message);
skipBusy=false;
document.dispatchEvent(new CustomEvent('dww11:skip-busy',{detail:false}));
}
});
async function observeResult(job) {
const local=loadLocal();
if(local?.id===job.id&&local.snapshot&&job.state==='awaiting_user_review') {
await rpc('event',{id:job.id,state:'user_submitting',epoch:run.epoch,snapshot:local.snapshot});
job.state='user_submitting';
}
if(job.state==='user_submitting') {
// Result selectors intentionally await user-provided HTML; URL alone is not proof.
if(!editorPage(job) || (local?.clickedAt && Date.now()-local.clickedAt>45000)) {
await rpc('event',{id:job.id,state:'submit_unknown',error:'等待结果页 HTML 适配;候选页面:'+location.href});
await rpc('stop');
status('已记录候选结果页;尚不能自动核对。流程已暂停,请补充结果页 HTML。');
}
}else if(job.state==='submit_unknown')status('发布结果待核对;不会重发、计数或换号。');
}
async function switchAccount() {
if(working||halted)return;
if(location.origin!=='https://www.dww11.com'||location.pathname!=='/') {
location.assign('https://www.dww11.com/');return;
}
const ext=extension(),button=document.querySelector('#dww11-extension-next');
if(!ext.ready||!ext.idle||!button||button.disabled) {status('请安装并重新加载带联动接口的换号插件');return;}
await rpc('switch-begin');
if(halted||!run?.running)return;
button.click();status('正在换号;等待插件完成并核对新账号');
}
async function tick() {
if(tickBusy||skipBusy)return;tickBusy=true;
try {
run=await rpc('heartbeat',{account:account(),url:location.href,extension:extension()});
if(skipBusy)return;
halted=!run.running;
if(!run.running) {aborter?.abort();document.dispatchEvent(new CustomEvent('dww11:auto-stop'));}
if(working)return;
// Passive result recording is permitted after Stop, but no new business actions.
if(run.active) {
const local=loadLocal();
if(local?.id===run.active&&local.snapshot&&!run.running)status('流程已暂停;提交结果仍需核对');
}
if(!run.running) {
if(run.active && run.owner===owner) {const saved=await rpc('current');if(saved.job) {active=saved.job;await observeResult(saved.job);}}
status(run.reason || '流程已暂停');return;
}
const next=await rpc('next',{account:account(),extension:extension()});run=next.run;
if(skipBusy)return;
if(next.job)active=next.job;
if(next.action==='prepare'||(next.action==='resume'&&next.job.state==='preparing'))preparation=prepare(next.job);
else if(next.action==='resume') {
await observeResult(next.job);
if(next.job.state==='awaiting_user_review') {
if(!editorPage(next.job)||!document.querySelector('#subject')?.value) await fail(new Error('待审稿页面已丢失,请人工核对旧稿,不自动重新导入'));
else status('等待你亲手发表;程序不再修改编辑器');
}
else if(['importing','formatting'].includes(next.job.state))await fail(new Error('导入阶段发生中断,请检查编辑器和已上传图片后处理任务'));
else if(next.job.state==='needs_attention')status(next.job.error);
}else if(next.action==='switch')await switchAccount();
else if(next.action==='switch_wait')status('等待换号结果;若插件已暂停,请人工处理后继续');
else if(next.action==='login')status('未能读取当前账号 UID,请登录并确认账号元素');
else if(next.action==='other_tab')status('另一个标签持有任务;如需接管请先暂停');
else if(next.action==='interval')status('等待设定的处理间隔');
else status(run.reason || '等待任务');
}catch(error){status(error.message);halted=true;aborter?.abort();document.dispatchEvent(new CustomEvent('dww11:auto-stop'));}
finally{tickBusy=false;}
}
const takeover=document.createElement('button');takeover.type='button';takeover.textContent='接管已暂停任务';
takeover.style.cssText='position:fixed;left:16px;top:16px;z-index:2147483646;padding:8px;background:#fff;border:1px solid #aab;border-radius:6px';
takeover.onclick=async()=>{try{await rpc('takeover');status('已接管;核对当前页面后在工作台继续');}catch(error){status(error.message);}};
document.body.append(takeover);
setInterval(tick,2000);tick();
})();