备份现场结果补录、图片修复与跳过文章功能

This commit is contained in:
Longtail committed 2026-10-31 17:08:49 +08:00
1 parent 1e6504b4a2
commit d157c179c8
20 files changed
+597 -47

No files matched your search

+63 -8
View File
@@ -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 };
});