备份现场结果补录、图片修复与跳过文章功能
This commit is contained in:
1 parent
1e6504b4a2
commit
d157c179c8
20 files changed
+597
-47
No files matched your search
+107
-15
@@ -1,7 +1,7 @@
|
||||
// ==UserScript==
|
||||
// @name 新闻工作台 · 导入排版与流程
|
||||
// @namespace local.dww11.auto-news
|
||||
// @version 0.1.0
|
||||
// @version 0.1.2
|
||||
// @match https://www.dww11.com/*
|
||||
// @match https://dww11.com/*
|
||||
// @match https://house.dww11.com/*
|
||||
@@ -18,7 +18,7 @@
|
||||
else core.mount(root);
|
||||
})(typeof window === 'undefined' ? null : window, function () {
|
||||
'use strict';
|
||||
const VERSION = '1.0.0';
|
||||
const VERSION = '1.0.1';
|
||||
const MAX_IMAGE_SIZE = 20 * 1024 * 1024;
|
||||
const IMAGE_EXT = /\.(?:jpe?g|png|gif)$/i;
|
||||
|
||||
@@ -49,10 +49,11 @@
|
||||
const image = text.startsWith('![', start);
|
||||
if (!image && text[start] !== '[') return null;
|
||||
const left = start + (image ? 2 : 1);
|
||||
let end = left;
|
||||
let end = left, brackets = 1;
|
||||
for (; end < text.length; end++) {
|
||||
if (text[end] === '\\') { end++; continue; }
|
||||
if (text[end] === ']') break;
|
||||
if (text[end] === '[') brackets++;
|
||||
if (text[end] === ']' && --brackets === 0) break;
|
||||
}
|
||||
if (text.slice(end, end + 2) !== '](') return null;
|
||||
let pos = end + 2, depth = 1, quoted = '', angle = false;
|
||||
@@ -110,7 +111,7 @@
|
||||
let list = '', quote = false, code = false, fence = '';
|
||||
const closeList = () => { if (list) output.push('[/list]'); list = ''; };
|
||||
for (let index = 0; index < lines.length; index++) {
|
||||
const line = lines[index];
|
||||
let line = lines[index];
|
||||
const codeMark = line.match(/^\s*(`{3,}|~{3,})/);
|
||||
if (codeMark && (!code || codeMark[1][0] === fence)) {
|
||||
closeList();
|
||||
@@ -118,6 +119,19 @@
|
||||
code = !code; fence = codeMark[1][0]; continue;
|
||||
}
|
||||
if (code) { output.push(line.replace(/\[\/code\]/gi, '[/code]')); continue; }
|
||||
// A linked image/caption may span multiple Markdown lines. Keep the complete
|
||||
// construct together so its inner image is visited and uploaded exactly once.
|
||||
const remainder = lines.slice(index).join('\n');
|
||||
for (let pos = 0; pos < line.length; pos++) {
|
||||
const link = readLink(remainder, pos);
|
||||
if (!link) continue;
|
||||
if (link.end > line.length) {
|
||||
const endLine = remainder.slice(0, link.end).split('\n').length - 1;
|
||||
line = lines.slice(index, index + endLine + 1).join('\n');
|
||||
}
|
||||
pos = link.end - 1;
|
||||
}
|
||||
index += line.split('\n').length - 1;
|
||||
const quoted = /^\s*>\s?/.test(line);
|
||||
if (quoted !== quote) { closeList(); output.push(quoted ? '[quote]' : '[/quote]'); quote = quoted; }
|
||||
const content = quoted ? line.replace(/^\s*>\s?/, '') : line;
|
||||
@@ -217,6 +231,43 @@
|
||||
return value.replace(/"/g, '');
|
||||
}
|
||||
|
||||
function attachmentId(image) {
|
||||
// The live Discuz editor uses aid="attachimg_123", not necessarily an id.
|
||||
for (const value of [image.getAttribute('aid'), image.id]) {
|
||||
const match = String(value || '').match(/^attachimg_([1-9]\d*)$/);
|
||||
if (match) return match[1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function verifyEditorImages(body, expected, signal) {
|
||||
signal?.throwIfAborted();
|
||||
if (!body?.querySelectorAll) throw new Error('无法读取可视化编辑器图片');
|
||||
const images = Array.from(body.querySelectorAll('img')).filter(image => attachmentId(image));
|
||||
if (JSON.stringify(images.map(attachmentId)) !== JSON.stringify(expected.map(String))) {
|
||||
throw new Error('正文配图数量或顺序不符,已停止;上传成功不代表正文插图成功。');
|
||||
}
|
||||
await Promise.all(images.map(image => new Promise((resolve, reject) => {
|
||||
let timer;
|
||||
const finish = error => {
|
||||
clearTimeout(timer);
|
||||
image.removeEventListener('load', onLoad);
|
||||
image.removeEventListener('error', onError);
|
||||
signal?.removeEventListener('abort', onAbort);
|
||||
error ? reject(error) : resolve();
|
||||
};
|
||||
const onLoad = () => image.naturalWidth > 0 ? finish() : onError();
|
||||
const onError = () => finish(new Error('网站附件图片无法显示(编号 ' + attachmentId(image) + ')。请核对附件缩略图及图片请求的 HTTP 错误;不会自动重传或发布。'));
|
||||
const onAbort = () => finish(new Error('流程已终止'));
|
||||
image.addEventListener('load', onLoad);
|
||||
image.addEventListener('error', onError);
|
||||
signal?.addEventListener('abort', onAbort, { once: true });
|
||||
timer = setTimeout(() => finish(new Error('等待正文图片加载超时(编号 ' + attachmentId(image) + ')')), 25000);
|
||||
if (signal?.aborted) onAbort();
|
||||
else if (image.complete) onLoad();
|
||||
})));
|
||||
}
|
||||
|
||||
function mount(win) {
|
||||
const doc = win.document;
|
||||
const params = new URLSearchParams(win.location.search);
|
||||
@@ -423,7 +474,8 @@
|
||||
setEditor(selected.title, bbcode);
|
||||
const raw = win.getEditorContents();
|
||||
if (Number(win.wysiwyg) === 1) {
|
||||
for (const id of new Set(aids.values())) if (!raw.includes('attachimg_' + id)) throw new Error('富文本图片转换未通过检查。');
|
||||
const expected = Array.from(bbcode.matchAll(/\[attachimg\](\d+)\[\/attachimg\]/g), match => match[1]);
|
||||
await verifyEditorImages(doc.querySelector('#e_iframe')?.contentDocument?.body || win.editdoc?.body, expected, signal);
|
||||
} else if (raw !== bbcode) throw new Error('正文填写校验失败。');
|
||||
if (doc.querySelector('#subject').value !== selected.title) throw new Error('标题填写校验失败。');
|
||||
committed = true;
|
||||
@@ -436,7 +488,9 @@
|
||||
const switcher = doc.querySelector('#e_switchercheck'); if (switcher) switcher.checked = !backup.mode;
|
||||
throw error;
|
||||
}
|
||||
lastResult = { title: selected.title, images: Object.fromEntries(aids), completedAt: new Date().toISOString() };
|
||||
lastResult = { title: selected.title, images: Object.fromEntries(aids),
|
||||
attachmentOrder: Array.from(bbcode.matchAll(/\[attachimg\](\d+)\[\/attachimg\]/g), match => match[1]),
|
||||
completedAt: new Date().toISOString() };
|
||||
notify('complete', `已填写标题、正文和 ${aids.size} 张配图。请检查排版,随后手动发表。`);
|
||||
return lastResult;
|
||||
} catch (error) {
|
||||
@@ -457,6 +511,7 @@
|
||||
// Public v1 interface. Staging is read-only; importPrepared is the explicit upload/fill step.
|
||||
// No postMessage listener, localhost service, publishing endpoint or saved credentials.
|
||||
const api = Object.freeze({ version: VERSION, schemaVersion: 1, stageFiles, stagePackage,
|
||||
attachmentId, verifyEditorImages,
|
||||
getState: summary, openPicker: () => { if (!busy) $('folder').click(); }, importPrepared,
|
||||
getBackup: () => savedBackup });
|
||||
win.DWW11Importer = api;
|
||||
@@ -476,7 +531,7 @@
|
||||
$('diagnostics').onclick = () => downloadJSON({ ...summary(), logs }, 'dww11-import-diagnostics.json');
|
||||
win.addEventListener('pagehide', releasePreviews, { once: true });
|
||||
}
|
||||
return { VERSION, normalizePath, imagePath, readLink, markdownToBBCode, compile, prepareFiles, preparePackage, parseAid, mount };
|
||||
return { VERSION, normalizePath, imagePath, readLink, markdownToBBCode, compile, prepareFiles, preparePackage, parseAid, attachmentId, verifyEditorImages, mount };
|
||||
});
|
||||
|
||||
// Extracted from dww11-wechat-oneclick.user.js v1.0.9; package source is authoritative.
|
||||
@@ -660,12 +715,12 @@ window.DWW11Formatter = Object.freeze({
|
||||
const footer = existingSource(body);
|
||||
const expected = `来源:${expectedSource},如有侵权,请联系删除。`;
|
||||
if (!footer || footer.text !== expected) throw new Error('文末来源与文章包不一致,请人工检查');
|
||||
const images = [...body.querySelectorAll('img')].map(x => x.id + ':' + x.getAttribute('src'));
|
||||
const images = [...body.querySelectorAll('img')].map(x => [x.id, x.getAttribute('aid'), x.getAttribute('src')].join(':'));
|
||||
const backup = body.innerHTML;
|
||||
try {
|
||||
footer.remove();
|
||||
formatBody(body, expectedSource, expected);
|
||||
const after = [...body.querySelectorAll('img')].map(x => x.id + ':' + x.getAttribute('src'));
|
||||
const after = [...body.querySelectorAll('img')].map(x => [x.id, x.getAttribute('aid'), x.getAttribute('src')].join(':'));
|
||||
if (JSON.stringify(images) !== JSON.stringify(after)) throw new Error('排版后图片标识或顺序改变');
|
||||
if (body.querySelectorAll('[data-dww11-wx-source="1"]').length !== 1) throw new Error('来源尾注数量异常');
|
||||
body.dispatchEvent(new Event('input', {bubbles:true}));
|
||||
@@ -684,6 +739,7 @@ window.DWW11Formatter = Object.freeze({
|
||||
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)=>{
|
||||
@@ -725,7 +781,7 @@ window.DWW11Formatter = Object.freeze({
|
||||
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,src:x.getAttribute('src')})),
|
||||
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)};
|
||||
}
|
||||
@@ -733,6 +789,7 @@ window.DWW11Formatter = Object.freeze({
|
||||
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'));
|
||||
@@ -761,8 +818,9 @@ window.DWW11Formatter = Object.freeze({
|
||||
window.DWW11Formatter.format(job.source);assertLive(epoch);
|
||||
const snapshot=editorSnapshot();
|
||||
if(snapshot.title!==job.title)throw new Error('标题校验失败');
|
||||
const aids=new Set(Object.values(result.images));
|
||||
for(const aid of aids) if(!snapshot.images.some(x=>x.id==='attachimg_'+aid))throw new Error('配图附件缺失:'+aid);
|
||||
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('已完成排版,请核对内容并亲手点击“发表帖子”。');
|
||||
@@ -770,6 +828,9 @@ window.DWW11Formatter = Object.freeze({
|
||||
}
|
||||
// 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();
|
||||
@@ -780,6 +841,35 @@ window.DWW11Formatter = Object.freeze({
|
||||
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') {
|
||||
@@ -807,9 +897,10 @@ window.DWW11Formatter = Object.freeze({
|
||||
button.click();status('正在换号;等待插件完成并核对新账号');
|
||||
}
|
||||
async function tick() {
|
||||
if(tickBusy)return;tickBusy=true;
|
||||
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;
|
||||
@@ -823,8 +914,9 @@ window.DWW11Formatter = Object.freeze({
|
||||
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'))void prepare(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') {
|
||||
|
||||
@@ -179,12 +179,12 @@ window.DWW11Formatter = Object.freeze({
|
||||
const footer = existingSource(body);
|
||||
const expected = `来源:${expectedSource},如有侵权,请联系删除。`;
|
||||
if (!footer || footer.text !== expected) throw new Error('文末来源与文章包不一致,请人工检查');
|
||||
const images = [...body.querySelectorAll('img')].map(x => x.id + ':' + x.getAttribute('src'));
|
||||
const images = [...body.querySelectorAll('img')].map(x => [x.id, x.getAttribute('aid'), x.getAttribute('src')].join(':'));
|
||||
const backup = body.innerHTML;
|
||||
try {
|
||||
footer.remove();
|
||||
formatBody(body, expectedSource, expected);
|
||||
const after = [...body.querySelectorAll('img')].map(x => x.id + ':' + x.getAttribute('src'));
|
||||
const after = [...body.querySelectorAll('img')].map(x => [x.id, x.getAttribute('aid'), x.getAttribute('src')].join(':'));
|
||||
if (JSON.stringify(images) !== JSON.stringify(after)) throw new Error('排版后图片标识或顺序改变');
|
||||
if (body.querySelectorAll('[data-dww11-wx-source="1"]').length !== 1) throw new Error('来源尾注数量异常');
|
||||
body.dispatchEvent(new Event('input', {bubbles:true}));
|
||||
|
||||
+63
-8
@@ -1,7 +1,7 @@
|
||||
// ==UserScript==
|
||||
// @name 广谈大湾网 · 本地文章目录导入
|
||||
// @namespace local.dww11.article-importer
|
||||
// @version 1.0.0
|
||||
// @version 1.0.1
|
||||
// @description 选择整个文章目录,导入 Markdown 标题、正文与本地配图;保留人工发布与程序接口。
|
||||
// @match https://www.dww11.com/forum.php*
|
||||
// @match https://dww11.com/forum.php*
|
||||
@@ -18,7 +18,7 @@
|
||||
else core.mount(root);
|
||||
})(typeof window === 'undefined' ? null : window, function () {
|
||||
'use strict';
|
||||
const VERSION = '1.0.0';
|
||||
const VERSION = '1.0.1';
|
||||
const MAX_IMAGE_SIZE = 20 * 1024 * 1024;
|
||||
const IMAGE_EXT = /\.(?:jpe?g|png|gif)$/i;
|
||||
|
||||
@@ -49,10 +49,11 @@
|
||||
const image = text.startsWith('![', start);
|
||||
if (!image && text[start] !== '[') return null;
|
||||
const left = start + (image ? 2 : 1);
|
||||
let end = left;
|
||||
let end = left, brackets = 1;
|
||||
for (; end < text.length; end++) {
|
||||
if (text[end] === '\\') { end++; continue; }
|
||||
if (text[end] === ']') break;
|
||||
if (text[end] === '[') brackets++;
|
||||
if (text[end] === ']' && --brackets === 0) break;
|
||||
}
|
||||
if (text.slice(end, end + 2) !== '](') return null;
|
||||
let pos = end + 2, depth = 1, quoted = '', angle = false;
|
||||
@@ -110,7 +111,7 @@
|
||||
let list = '', quote = false, code = false, fence = '';
|
||||
const closeList = () => { if (list) output.push('[/list]'); list = ''; };
|
||||
for (let index = 0; index < lines.length; index++) {
|
||||
const line = lines[index];
|
||||
let line = lines[index];
|
||||
const codeMark = line.match(/^\s*(`{3,}|~{3,})/);
|
||||
if (codeMark && (!code || codeMark[1][0] === fence)) {
|
||||
closeList();
|
||||
@@ -118,6 +119,19 @@
|
||||
code = !code; fence = codeMark[1][0]; continue;
|
||||
}
|
||||
if (code) { output.push(line.replace(/\[\/code\]/gi, '[/code]')); continue; }
|
||||
// A linked image/caption may span multiple Markdown lines. Keep the complete
|
||||
// construct together so its inner image is visited and uploaded exactly once.
|
||||
const remainder = lines.slice(index).join('\n');
|
||||
for (let pos = 0; pos < line.length; pos++) {
|
||||
const link = readLink(remainder, pos);
|
||||
if (!link) continue;
|
||||
if (link.end > line.length) {
|
||||
const endLine = remainder.slice(0, link.end).split('\n').length - 1;
|
||||
line = lines.slice(index, index + endLine + 1).join('\n');
|
||||
}
|
||||
pos = link.end - 1;
|
||||
}
|
||||
index += line.split('\n').length - 1;
|
||||
const quoted = /^\s*>\s?/.test(line);
|
||||
if (quoted !== quote) { closeList(); output.push(quoted ? '[quote]' : '[/quote]'); quote = quoted; }
|
||||
const content = quoted ? line.replace(/^\s*>\s?/, '') : line;
|
||||
@@ -217,6 +231,43 @@
|
||||
return value.replace(/"/g, '');
|
||||
}
|
||||
|
||||
function attachmentId(image) {
|
||||
// The live Discuz editor uses aid="attachimg_123", not necessarily an id.
|
||||
for (const value of [image.getAttribute('aid'), image.id]) {
|
||||
const match = String(value || '').match(/^attachimg_([1-9]\d*)$/);
|
||||
if (match) return match[1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function verifyEditorImages(body, expected, signal) {
|
||||
signal?.throwIfAborted();
|
||||
if (!body?.querySelectorAll) throw new Error('无法读取可视化编辑器图片');
|
||||
const images = Array.from(body.querySelectorAll('img')).filter(image => attachmentId(image));
|
||||
if (JSON.stringify(images.map(attachmentId)) !== JSON.stringify(expected.map(String))) {
|
||||
throw new Error('正文配图数量或顺序不符,已停止;上传成功不代表正文插图成功。');
|
||||
}
|
||||
await Promise.all(images.map(image => new Promise((resolve, reject) => {
|
||||
let timer;
|
||||
const finish = error => {
|
||||
clearTimeout(timer);
|
||||
image.removeEventListener('load', onLoad);
|
||||
image.removeEventListener('error', onError);
|
||||
signal?.removeEventListener('abort', onAbort);
|
||||
error ? reject(error) : resolve();
|
||||
};
|
||||
const onLoad = () => image.naturalWidth > 0 ? finish() : onError();
|
||||
const onError = () => finish(new Error('网站附件图片无法显示(编号 ' + attachmentId(image) + ')。请核对附件缩略图及图片请求的 HTTP 错误;不会自动重传或发布。'));
|
||||
const onAbort = () => finish(new Error('流程已终止'));
|
||||
image.addEventListener('load', onLoad);
|
||||
image.addEventListener('error', onError);
|
||||
signal?.addEventListener('abort', onAbort, { once: true });
|
||||
timer = setTimeout(() => finish(new Error('等待正文图片加载超时(编号 ' + attachmentId(image) + ')')), 25000);
|
||||
if (signal?.aborted) onAbort();
|
||||
else if (image.complete) onLoad();
|
||||
})));
|
||||
}
|
||||
|
||||
function mount(win) {
|
||||
const doc = win.document;
|
||||
const params = new URLSearchParams(win.location.search);
|
||||
@@ -423,7 +474,8 @@
|
||||
setEditor(selected.title, bbcode);
|
||||
const raw = win.getEditorContents();
|
||||
if (Number(win.wysiwyg) === 1) {
|
||||
for (const id of new Set(aids.values())) if (!raw.includes('attachimg_' + id)) throw new Error('富文本图片转换未通过检查。');
|
||||
const expected = Array.from(bbcode.matchAll(/\[attachimg\](\d+)\[\/attachimg\]/g), match => match[1]);
|
||||
await verifyEditorImages(doc.querySelector('#e_iframe')?.contentDocument?.body || win.editdoc?.body, expected, signal);
|
||||
} else if (raw !== bbcode) throw new Error('正文填写校验失败。');
|
||||
if (doc.querySelector('#subject').value !== selected.title) throw new Error('标题填写校验失败。');
|
||||
committed = true;
|
||||
@@ -436,7 +488,9 @@
|
||||
const switcher = doc.querySelector('#e_switchercheck'); if (switcher) switcher.checked = !backup.mode;
|
||||
throw error;
|
||||
}
|
||||
lastResult = { title: selected.title, images: Object.fromEntries(aids), completedAt: new Date().toISOString() };
|
||||
lastResult = { title: selected.title, images: Object.fromEntries(aids),
|
||||
attachmentOrder: Array.from(bbcode.matchAll(/\[attachimg\](\d+)\[\/attachimg\]/g), match => match[1]),
|
||||
completedAt: new Date().toISOString() };
|
||||
notify('complete', `已填写标题、正文和 ${aids.size} 张配图。请检查排版,随后手动发表。`);
|
||||
return lastResult;
|
||||
} catch (error) {
|
||||
@@ -457,6 +511,7 @@
|
||||
// Public v1 interface. Staging is read-only; importPrepared is the explicit upload/fill step.
|
||||
// No postMessage listener, localhost service, publishing endpoint or saved credentials.
|
||||
const api = Object.freeze({ version: VERSION, schemaVersion: 1, stageFiles, stagePackage,
|
||||
attachmentId, verifyEditorImages,
|
||||
getState: summary, openPicker: () => { if (!busy) $('folder').click(); }, importPrepared,
|
||||
getBackup: () => savedBackup });
|
||||
win.DWW11Importer = api;
|
||||
@@ -476,5 +531,5 @@
|
||||
$('diagnostics').onclick = () => downloadJSON({ ...summary(), logs }, 'dww11-import-diagnostics.json');
|
||||
win.addEventListener('pagehide', releasePreviews, { once: true });
|
||||
}
|
||||
return { VERSION, normalizePath, imagePath, readLink, markdownToBBCode, compile, prepareFiles, preparePackage, parseAid, mount };
|
||||
return { VERSION, normalizePath, imagePath, readLink, markdownToBBCode, compile, prepareFiles, preparePackage, parseAid, attachmentId, verifyEditorImages, mount };
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
// ==UserScript==
|
||||
// @name 新闻工作台 · 本地连接
|
||||
// @namespace local.dww11.auto-news
|
||||
// @version 0.1.0
|
||||
// @version 0.1.1
|
||||
// @match https://www.dww11.com/*
|
||||
// @match https://dww11.com/*
|
||||
// @match https://house.dww11.com/*
|
||||
@@ -15,7 +15,7 @@
|
||||
// ==/UserScript==
|
||||
(() => {
|
||||
'use strict';
|
||||
const allowed = new Set(['heartbeat','next','package','event','accept','switch-begin','takeover','stop','current']);
|
||||
const allowed = new Set(['heartbeat','next','package','event','accept','switch-begin','takeover','stop','current','skip']);
|
||||
let token = GM_getValue('pairToken',''), port = GM_getValue('pairPort',8770);
|
||||
let stopRevision = 0;
|
||||
const signalStop = () => { stopRevision++; document.dispatchEvent(new CustomEvent('dww11:auto-stop')); };
|
||||
@@ -56,6 +56,10 @@
|
||||
const shadow=host.attachShadow({mode:'closed'});
|
||||
shadow.innerHTML='<style>section{font:13px/1.6 system-ui;background:white;color:#24364a;border:1px solid #ccd7e0;border-radius:10px;padding:12px;width:250px;box-shadow:0 3px 20px #0002}button,input{font:inherit;box-sizing:border-box;padding:7px;margin:4px 0;width:100%}button{cursor:pointer}#halt{background:#ad302d;color:white;border:0;border-radius:6px}small{display:block}</style><section><strong>新闻工作台 · 本地连接</strong><button id="halt">终止流程</button><details><summary>连接设置</summary><label>本地端口<input id="port" type="number" min="1" max="65535"></label><label>连接码<input id="token" type="password" autocomplete="off"></label><button id="save">保存连接设置</button></details><small id="status">请启动本地工作台并配对</small></section>';
|
||||
const $=id=>shadow.getElementById(id);$('port').value=port;
|
||||
const skip=document.createElement('button');skip.id='skip';skip.type='button';skip.textContent='跳过本文章';
|
||||
$('halt').before(skip);
|
||||
skip.onclick=()=>document.dispatchEvent(new CustomEvent('dww11:skip-article'));
|
||||
document.addEventListener('dww11:skip-busy',event=>{skip.disabled=Boolean(event.detail);});
|
||||
$('save').onclick=()=>{
|
||||
const value=Number($('port').value);if(!Number.isInteger(value)||value<1||value>65535){$('status').textContent='端口无效';return;}
|
||||
if($('token').value.trim()) token=$('token').value.trim();port=value;
|
||||
|
||||
+42
-5
@@ -7,6 +7,7 @@
|
||||
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)=>{
|
||||
@@ -48,7 +49,7 @@
|
||||
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,src:x.getAttribute('src')})),
|
||||
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)};
|
||||
}
|
||||
@@ -56,6 +57,7 @@
|
||||
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'));
|
||||
@@ -84,8 +86,9 @@
|
||||
window.DWW11Formatter.format(job.source);assertLive(epoch);
|
||||
const snapshot=editorSnapshot();
|
||||
if(snapshot.title!==job.title)throw new Error('标题校验失败');
|
||||
const aids=new Set(Object.values(result.images));
|
||||
for(const aid of aids) if(!snapshot.images.some(x=>x.id==='attachimg_'+aid))throw new Error('配图附件缺失:'+aid);
|
||||
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('已完成排版,请核对内容并亲手点击“发表帖子”。');
|
||||
@@ -93,6 +96,9 @@
|
||||
}
|
||||
// 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();
|
||||
@@ -103,6 +109,35 @@
|
||||
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') {
|
||||
@@ -130,9 +165,10 @@
|
||||
button.click();status('正在换号;等待插件完成并核对新账号');
|
||||
}
|
||||
async function tick() {
|
||||
if(tickBusy)return;tickBusy=true;
|
||||
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;
|
||||
@@ -146,8 +182,9 @@
|
||||
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'))void prepare(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') {
|
||||
|
||||
Reference in new issue
Block a user