// ==UserScript== // @name 新闻工作台 · 导入排版与流程 // @namespace local.dww11.auto-news // @version 0.2.1 // @match https://www.dww11.com/* // @match https://dww11.com/* // @match https://house.dww11.com/* // @grant none // @run-at document-idle // @noframes // ==/UserScript== (function (root, factory) { 'use strict'; const core = factory(); if (typeof module === 'object' && module.exports) module.exports = core; else core.mount(root); })(typeof window === 'undefined' ? null : window, function () { 'use strict'; const VERSION = '1.0.1'; const MAX_IMAGE_SIZE = 20 * 1024 * 1024; const IMAGE_EXT = /\.(?:jpe?g|png|gif)$/i; function normalizePath(path) { const parts = []; const value = String(path).replace(/\\/g, '/').normalize('NFC'); if (/^(?:\/|[a-z]:|[a-z]+:)/i.test(value)) throw new Error('请使用目录内的相对路径:' + path); for (const part of value.split('/')) { if (!part || part === '.') continue; if (part === '..') { if (!parts.length) throw new Error('图片路径超出所选目录:' + path); parts.pop(); } else parts.push(part); } return parts.join('/'); } function imagePath(mdPath, target) { if (/^(?:[a-z][\w+.-]*:|\/\/|\/)/i.test(target)) throw new Error('图片必须在所选目录内,请先下载:' + target); let decoded; try { decoded = decodeURIComponent(target); } catch { throw new Error('图片路径编码无效:' + target); } return normalizePath(mdPath.slice(0, mdPath.lastIndexOf('/') + 1) + decoded); } // A small, intentionally bounded Markdown reader. Raw HTML is never executed. // Links/images support balanced parentheses, angle destinations and optional titles. function readLink(text, start) { const image = text.startsWith('![', start); if (!image && text[start] !== '[') return null; const left = start + (image ? 2 : 1); let end = left, brackets = 1; for (; end < text.length; end++) { if (text[end] === '\\') { end++; continue; } 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; for (; pos < text.length; pos++) { const ch = text[pos]; if (ch === '\\') { pos++; continue; } if (quoted) { if (ch === quoted) quoted = ''; continue; } if (!angle && depth === 1 && /\s/.test(text[pos - 1]) && (ch === '"' || ch === "'")) { quoted = ch; continue; } if (ch === '<') { angle = true; continue; } if (ch === '>') { angle = false; continue; } if (!angle && ch === '(') depth++; if (!angle && ch === ')' && --depth === 0) break; } if (depth) return null; let target = text.slice(end + 2, pos).trim(); const destination = target.match(/^<([^>]+)>\s*(?:["'][\s\S]*["'])?$/); target = destination ? destination[1] : target.replace(/\s+["'][\s\S]*["']\s*$/, ''); target = target.replace(/\\([()\\ ])/g, '$1'); return { image, label: text.slice(left, end), target, end: pos + 1 }; } function inline(text, onImage) { let out = ''; for (let pos = 0; pos < text.length;) { if (text[pos] === '\\' && pos + 1 < text.length) { out += text[pos + 1]; pos += 2; continue; } const link = readLink(text, pos); if (link) { if (link.image) out += onImage(link.target, link.label); else if (/^(https?:\/\/|mailto:)/i.test(link.target) && !/[\[\]\r\n"']/.test(link.target)) { out += '[url=' + link.target + ']' + inline(link.label, onImage) + '[/url]'; } else out += link.label + ' (' + link.target + ')'; pos = link.end; continue; } if (text[pos] === '`') { const end = text.indexOf('`', pos + 1); if (end > pos + 1) { out += text.slice(pos + 1, end); pos = end + 1; continue; } } let matched = false; for (const [mark, tag] of [['**', 'b'], ['__', 'b'], ['~~', 's'], ['*', 'i'], ['_', 'i']]) { if (!text.startsWith(mark, pos) || (mark === '_' && /\w/.test(text[pos - 1] || ''))) continue; const end = text.indexOf(mark, pos + mark.length); if (end <= pos + mark.length) continue; out += '[' + tag + ']' + inline(text.slice(pos + mark.length, end), onImage) + '[/' + tag + ']'; pos = end + mark.length; matched = true; break; } if (!matched) out += text[pos++]; } return out; } function markdownToBBCode(markdown, onImage) { const lines = markdown.replace(/\r\n?/g, '\n').split('\n'); const output = []; let list = '', quote = false, code = false, fence = ''; const closeList = () => { if (list) output.push('[/list]'); list = ''; }; for (let index = 0; index < lines.length; index++) { let line = lines[index]; const codeMark = line.match(/^\s*(`{3,}|~{3,})/); if (codeMark && (!code || codeMark[1][0] === fence)) { closeList(); output.push(code ? '[/code]' : '[code]'); 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; const item = content.match(/^\s*(?:([-+*])|\d+[.)])\s+(.+)$/); if (item) { const type = item[1] ? '[list]' : '[list=1]'; if (list !== type) { closeList(); output.push(type); list = type; } output.push('[*]' + inline(item[2], onImage)); continue; } closeList(); const heading = content.match(/^\s{0,3}(#{1,6})\s+(.+?)\s*#*$/); if (heading) output.push('[b]' + inline(heading[2], onImage) + '[/b]'); else if (/^\s*(?:---+|\*\*\*+|___+)\s*$/.test(content)) output.push('[hr]'); else output.push(inline(content, onImage)); } closeList(); if (quote) output.push('[/quote]'); if (code) output.push('[/code]'); return output.join('\n').trim(); } function shortTitle(title, budget = 75) { const size = ch => { const code = ch.codePointAt(0); return code <= 0x7f ? 1 : code <= 0x7ff ? 2 : code <= 0xffff ? 3 : 4; }; // A generated prefix plus "..." (up to 78 bytes) is already safe. const total = Array.from(title).reduce((sum, ch) => sum + size(ch), 0); if (total <= budget || (title.endsWith('...') && total <= budget + 3)) return title; let prefix = '', used = 0; for (const ch of title) { const bytes = size(ch); if (used + bytes > budget) break; prefix += ch; used += bytes; } return prefix + '...'; } function compile(markdown, mdPath, fileMap, titleOverride) { let body = String(markdown).replace(/^\uFEFF/, '').replace(/\r\n?/g, '\n'); const firstHeading = body.match(/^\s*#\s+([^\n]+)\n?/); const originalTitle = String(titleOverride || firstHeading?.[1] || mdPath.split('/').pop().replace(/\.md$/i, '')).trim(); const title = shortTitle(originalTitle); if (firstHeading) body = body.slice(firstHeading[0].length).trim(); if (!title) throw new Error('标题为空,请修改 Markdown 标题后重试。'); if (!body.trim()) throw new Error('正文为空。'); const warnings = []; if (/<\/?[a-z][^>]*>/i.test(body)) warnings.push('存在 HTML,将作为文字保留;请检查排版。'); if (/^\s*\|.*\|\s*$/m.test(body)) warnings.push('表格暂按原文保留,需手动检查。'); if (/!\[[^\]]*\]\s*\[[^\]]*\]/.test(body) || /^\s*\[[^\]]+\]:/m.test(body)) { throw new Error('暂不支持引用式 Markdown 图片/链接,请改成 ![](图片路径) 或 [文字](网址)。'); } const images = new Map(); // Validate every image before any network request; repeated references upload once. markdownToBBCode(body, (target) => { const path = imagePath(mdPath, target); const file = fileMap.get(path); if (!file) throw new Error('找不到配图:' + path); if (!IMAGE_EXT.test(path)) throw new Error('网站仅支持 JPG、JPEG、PNG、GIF:' + path); if (!file.size || file.size >= MAX_IMAGE_SIZE) throw new Error('配图为空或达到 20 MB:' + path); images.set(path, file); return '[图片:' + path + ']'; }); return { title, body, mdPath, images, warnings, render(aids) { return markdownToBBCode(body, target => { const path = imagePath(mdPath, target); const aid = aids.get(path); if (!/^[1-9]\d*$/.test(String(aid))) throw new Error('配图尚未上传:' + path); return '[attachimg]' + aid + '[/attachimg]'; }); } }; } async function prepareFiles(files, markdownPath) { const fileMap = new Map(); for (const file of Array.from(files)) { const path = normalizePath(file.webkitRelativePath || file.name); if (fileMap.has(path)) throw new Error('文件路径重复:' + path); fileMap.set(path, file); } const candidates = Array.from(fileMap.keys()).filter(p => /\.md$/i.test(p)).sort(); if (!candidates.length) throw new Error('所选目录中没有 .md 文件。'); if (!markdownPath && candidates.length > 1) return { candidates, files: Array.from(files) }; const mdPath = markdownPath || candidates[0]; const md = fileMap.get(mdPath); if (!md) throw new Error('指定的 Markdown 文件不存在。'); return compile(await md.text(), mdPath, fileMap); } async function preparePackage(data) { if (!data || data.schemaVersion !== 1 || typeof data.markdown !== 'string' || !Array.isArray(data.images)) { throw new Error('文章包格式错误,需要 schemaVersion: 1、markdown 和 images。'); } const map = new Map(); for (const item of data.images) { const path = normalizePath(item.path); if (map.has(path)) throw new Error('文章包图片路径重复:' + path); let blob = item.blob; if (!blob && typeof item.base64 === 'string') { const binary = atob(item.base64); blob = new Blob([Uint8Array.from(binary, ch => ch.charCodeAt(0))], { type: item.mime || 'application/octet-stream' }); } if (!blob || typeof blob.arrayBuffer !== 'function') throw new Error('文章包缺少图片数据:' + path); map.set(path, blob); } return compile(data.markdown, normalizePath(data.markdownPath || 'article.md'), map, data.title); } function parseAid(text) { const value = String(text).trim(); // The site's actual WebUploader adapter uses a positive integer response. if (!/^(?:[1-9]\d*|"[1-9]\d*")$/.test(value)) throw new Error('服务器未返回有效附件编号(可能登录过期、超出权限或上传失败)。'); 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); if (params.get('mod') !== 'post' || params.get('action') !== 'newthread' || !doc.querySelector('#subject') || !doc.querySelector('#e_textarea')) return; if (win.DWW11Importer) return; let article = null, busy = false, candidates = null, lastResult = null, savedBackup = null, stageRevision = 0; const cache = new Map(), urls = [], logs = []; const host = doc.createElement('div'); host.id = 'dww11-importer'; host.style.cssText = 'position:fixed;right:18px;bottom:18px;z-index:2147483000'; const shadow = host.attachShadow({ mode: 'open' }); shadow.innerHTML = `
本地文章导入 · 1.0
选择整个文章文件夹,检查后上传配图并填写。发布由你操作。网站自带的自动保存仍可能运行。

尚未选择目录

`; doc.body.appendChild(host); const $ = id => shadow.getElementById(id); const summary = () => ({ version: VERSION, busy, ready: !!article, title: article?.title || null, markdownPath: article?.mdPath || null, imageCount: article?.images.size || 0, warnings: article?.warnings || [], result: lastResult }); function notify(phase, message, extra = {}) { $('status').textContent = message; $('status').className = phase === 'error' ? 'error' : phase === 'complete' ? 'success' : ''; logs.push({ time: new Date().toISOString(), phase, message, ...extra }); $('log').hidden = false; $('log').textContent = logs.slice(-8).map(l => l.message).join('\n'); win.dispatchEvent(new CustomEvent('dww11-importer:progress', { detail: { phase, message, ...extra } })); } function releasePreviews() { while (urls.length) URL.revokeObjectURL(urls.pop()); } function clearStage() { article = null; lastResult = null; candidates = null; releasePreviews(); $('preview').hidden = true; $('md').hidden = true; $('import').disabled = true; } function showArticle(prepared) { article = prepared; lastResult = null; releasePreviews(); $('preview').hidden = false; $('title').textContent = article.title; $('body').textContent = article.body; $('warnings').textContent = article.warnings.join(';'); $('images').replaceChildren(); for (const [path, blob] of article.images) { const img = doc.createElement('img'); img.src = URL.createObjectURL(blob); urls.push(img.src); img.alt = path; img.title = path; $('images').appendChild(img); } $('import').disabled = false; notify('ready', `已检查:${article.images.size} 张图片。点击“上传配图并填写”开始导入。`); return summary(); } async function stageFiles(files, options = {}) { if (busy) throw new Error('正在导入,请稍候。'); clearStage(); const revision = ++stageRevision; const list = Array.from(files); const prepared = await prepareFiles(list, options.markdownPath); if (revision !== stageRevision) return { cancelled: true }; if (prepared.candidates) { candidates = prepared.files; $('md').replaceChildren(new Option('请选择目录中的 Markdown 文件', '')); for (const path of prepared.candidates) $('md').appendChild(new Option(path, path)); $('md').hidden = false; notify('choose-markdown', '目录中有多个 Markdown 文件,请选择一篇。'); return { candidates: prepared.candidates }; } return showArticle(prepared); } async function stagePackage(data) { if (busy) throw new Error('正在导入,请稍候。'); clearStage(); const revision = ++stageRevision; const prepared = await preparePackage(data); if (revision !== stageRevision) return { cancelled: true }; return showArticle(prepared); } const snapshot = () => ({ title: doc.querySelector('#subject').value, mode: Number(win.wysiwyg), text: win.getEditorContents() }); function assertEditor() { for (const name of ['getEditorContents', 'switchEditor']) { if (typeof win[name] !== 'function') throw new Error('未识别到网站编辑器:' + name + '。请刷新页面后重试。'); } if (!win.imgUpload?.settings || typeof win.updateImageList !== 'function') throw new Error('网站图片上传组件尚未就绪,请刷新后重试。'); if (Number(win.allowbbcode) === 0 || Number(win.allowimgcode) === 0) throw new Error('当前版块或账号不支持正文格式/图片。'); } async function uploadImage(blob, path, settings, signal) { signal?.throwIfAborted(); const url = new URL(settings.upload_url, win.location.href); if (url.origin !== win.location.origin || url.pathname !== '/misc.php' || url.searchParams.get('operation') !== 'upload') { throw new Error('上传地址与预期不符,已停止。'); } const hash = Array.from(new Uint8Array(await win.crypto.subtle.digest('SHA-256', await blob.arrayBuffer())), n => n.toString(16).padStart(2, '0')).join(''); const key = url.href + ':' + settings.post_params.uid + ':' + hash; if (cache.has(key)) { notify('upload', '复用已上传图片:' + path); return cache.get(key); } const data = new FormData(); for (const [key, value] of Object.entries(settings.post_params)) data.append(key, String(value)); data.set('type', 'image'); data.set('filetype', blob.type || (/\.png$/i.test(path) ? 'image/png' : /\.gif$/i.test(path) ? 'image/gif' : 'image/jpeg')); data.append(settings.file_post_name || 'Filedata', blob, path.split('/').pop()); const control = new AbortController(), timer = setTimeout(() => control.abort(), 90000); try { signal?.throwIfAborted(); signal?.addEventListener('abort', () => control.abort(), { once: true }); const response = await win.fetch(url.href, { method: 'POST', body: data, credentials: 'same-origin', redirect: 'error', signal: control.signal }); if (!response.ok) throw new Error('图片上传请求失败:HTTP ' + response.status); const aid = parseAid(await response.text()); cache.set(key, aid); return aid; } catch (error) { if (error.name === 'AbortError') throw new Error('上传超时;该图片可能已到达服务器,请检查网站图片列表后再重试。'); throw error; } finally { clearTimeout(timer); } } async function registerImages(aids, signal) { signal?.throwIfAborted(); if (!aids.length) return; const ids = new Set(aids); doc.querySelectorAll('#imgattachlist [id^="image_td_"],#imgattachlist [id^="image_"]').forEach(el => { const id = el.id.match(/^(?:image_td_|image_)(\d+)$/)?.[1]; if (id) ids.add(id); }); const ready = () => aids.every(id => doc.getElementById('image_' + id)); if (ready()) return; // Use the site's own list loader, which registers attachment form fields too. win.updateImageList(1, Array.from(ids).join('|')); await new Promise((resolve, reject) => { const timer = setTimeout(() => { observer.disconnect(); reject(new Error('图片已上传,但网站图片列表加载超时。重试会复用本页已上传图片。')); }, 25000); signal?.addEventListener('abort', () => { clearTimeout(timer); observer.disconnect(); reject(new Error('流程已终止')); }, { once: true }); const check = () => { if (ready()) { clearTimeout(timer); observer.disconnect(); resolve(); } }; const observer = new MutationObserver(check); observer.observe(doc.body, { childList: true, subtree: true }); check(); }); } function setEditor(title, bbcode) { win.switchEditor(0); if (Number(win.wysiwyg) !== 0) throw new Error('无法切换编辑器为纯文本模式。'); const textarea = doc.querySelector('#e_textarea'); textarea.value = bbcode; doc.querySelector('#subject').value = title; for (const el of [textarea, doc.querySelector('#subject')]) { el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); } // Let Discuz generate the WYSIWYG content with native attachment metadata. win.switchEditor(1); const switcher = doc.querySelector('#e_switchercheck'); if (switcher) switcher.checked = Number(win.wysiwyg) === 0; } async function importPrepared(options = {}) { const { signal, automatic = false } = options; signal?.throwIfAborted(); if (busy) throw new Error('正在导入,请勿重复点击。'); if (!article) throw new Error('请先选择文章目录或文章包。'); assertEditor(); for (const id of ['bbcodeoff', 'htmlon']) { if (doc.getElementById(id)?.checked) throw new Error('请先关闭“禁用 Discuz! 代码”和“HTML 代码”,再导入。'); } const before = snapshot(); if ((before.title.trim() || before.text.replace(/<[^>]*>/g, '').trim() || / { throw new Error('编辑器已有内容,请人工处理旧稿后重试'); })() : !win.confirm('当前编辑器已有内容。导入将替换标题和正文,是否继续?'))) return { cancelled: true }; const selected = article; busy = true; const controls = ['choose', 'package', 'import', 'md'].map($); controls.forEach(el => { el.disabled = true; }); // Prevent accidental submission of a partly imported article. const blockSubmit = event => { event.preventDefault(); event.stopImmediatePropagation(); notify('busy', '正在导入,请等待完成后再操作发布。'); }; const form = doc.querySelector('#postform'); form.addEventListener('submit', blockSubmit, true); const submitButtons = Array.from(form.querySelectorAll('button[type="submit"],button[name],input[type="submit"]')); const previousDisabled = submitButtons.map(el => el.disabled); submitButtons.forEach(el => { el.disabled = true; }); let committed = false; try { const aids = new Map(); const settings = win.imgUpload.settings; const max = Math.min(MAX_IMAGE_SIZE, Number(settings.file_size_limit) * 1024 || MAX_IMAGE_SIZE); for (const [path, blob] of selected.images) if (blob.size >= max) throw new Error('配图超过当前网站限制:' + path); let index = 0; for (const [path, blob] of selected.images) { notify('upload', `上传 ${++index}/${selected.images.size}:${path}`, { completed: index - 1, total: selected.images.size }); aids.set(path, await uploadImage(blob, path, settings, signal)); } notify('register', '正在加载网站图片列表…'); await registerImages(Array.from(new Set(aids.values())), signal); signal?.throwIfAborted(); const now = snapshot(); if ((now.title !== before.title || now.text !== before.text) && (automatic ? (() => { throw new Error('上传期间编辑器被修改,已保留当前内容'); })() : !win.confirm('上传期间你修改了编辑器。是否用导入文章替换最新内容?'))) { notify('cancelled', '已保留当前正文;图片已上传,可稍后再次导入。'); return { cancelled: true }; } const backup = snapshot(); const bbcode = selected.render(aids); // Preserve a local, downloadable snapshot before changing title or body. savedBackup = { title: backup.title, content: backup.text, mode: backup.mode, createdAt: new Date().toISOString() }; try { for (const id of ['bbcodeoff', 'htmlon']) { const el = doc.getElementById(id); if (el && id === 'bbcodeoff' && el.checked) throw new Error('请先取消“禁用 Discuz! 代码”,再重试。'); if (el && id === 'htmlon' && el.checked) throw new Error('请先关闭“HTML 代码”,再重试。'); } signal?.throwIfAborted(); setEditor(selected.title, bbcode); const raw = win.getEditorContents(); if (Number(win.wysiwyg) === 1) { 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; } catch (error) { // Restore the exact editor representation saved immediately before filling. win.switchEditor(backup.mode); if (backup.mode) win.editdoc.body.innerHTML = backup.text; else doc.querySelector('#e_textarea').value = backup.text; doc.querySelector('#subject').value = backup.title; const switcher = doc.querySelector('#e_switchercheck'); if (switcher) switcher.checked = !backup.mode; throw error; } 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) { notify('error', error.message + (committed ? '' : ' 标题和正文未替换;已上传的图片会保留。')); throw error; } finally { busy = false; form.removeEventListener('submit', blockSubmit, true); submitButtons.forEach((el, index) => { el.disabled = previousDisabled[index]; }); controls.forEach(el => { el.disabled = false; }); $('import').disabled = !article; } } function downloadJSON(value, filename) { const url = URL.createObjectURL(new Blob([JSON.stringify(value, null, 2)], { type: 'application/json' })); const link = doc.createElement('a'); link.href = url; link.download = filename; link.click(); setTimeout(() => URL.revokeObjectURL(url), 1000); } // 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; $('choose').onclick = () => { $('folder').value = ''; $('folder').click(); }; $('package').onclick = () => { $('json').value = ''; $('json').click(); }; const handleError = error => notify('error', error.message || String(error)); $('folder').onchange = () => stageFiles($('folder').files).catch(handleError); $('json').onchange = async () => { try { const file = $('json').files[0]; if (file) await stagePackage(JSON.parse(await file.text())); } catch (error) { handleError(error); } }; $('md').onchange = () => { const files = candidates, path = $('md').value; if (files && path) stageFiles(files, { markdownPath: path }).catch(handleError); }; $('import').onclick = () => importPrepared().catch(handleError); $('diagnostics').onclick = () => downloadJSON({ ...summary(), logs }, 'dww11-import-diagnostics.json'); win.addEventListener('pagehide', releasePreviews, { once: true }); } return { VERSION, shortTitle, 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. (() => { 'use strict'; const cleanText = value => String(value || '').replace(/\s+/g, ' ').trim(); function flattenParagraphs(body) { const selector = 'p,div,section,article,figure,figcaption,h1,h2,h3,h4,h5,h6'; const marker = () => { const br = body.ownerDocument.createElement('br'); br.dataset.dww11Boundary = '1'; return br; }; for (const block of Array.from(body.querySelectorAll(selector)).reverse()) { if (block.closest('table,ul,ol,blockquote')) continue; block.replaceWith(marker(), ...block.childNodes, marker()); } let previous = false, pending = []; function finish() { const actual = pending.filter(br => !br.hasAttribute('data-dww11-boundary')); let retained = false; for (const br of pending) { if (!br.hasAttribute('data-dww11-boundary')) continue; if (previous && !actual.length && !retained) { br.removeAttribute('data-dww11-boundary'); retained = true; } else br.remove(); } pending = []; } const walker = body.ownerDocument.createTreeWalker(body, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT); const nodes = []; while (walker.nextNode()) nodes.push(walker.currentNode); for (const node of nodes) { if (node.nodeType === 1 && node.tagName === 'BR') pending.push(node); else if ((node.nodeType === 3 && node.textContent.trim()) || (node.nodeType === 1 && ['IMG', 'VIDEO', 'HR', 'IFRAME'].includes(node.tagName))) { finish(); previous = true; } } // 末尾不保留由容器边界制造的额外换行。 for (const br of pending) if (br.hasAttribute('data-dww11-boundary')) br.remove(); } function spaceImages(body) { let previous = null, gapNodes = [], blankLines = 0; const changes = []; function content(kind, node) { if (previous && previous.kind !== kind) { let anchor = node; while (anchor.parentElement && anchor.parentElement !== body && !['TD', 'TH', 'LI', 'BLOCKQUOTE'].includes(anchor.parentElement.tagName) && !anchor.parentElement.contains(previous.node)) anchor = anchor.parentElement; changes.push({ anchor, nodes: gapNodes.slice(), count: Math.max(2, blankLines) }); } previous = { kind, node }; gapNodes = []; blankLines = 0; } function visit(node) { if (node.nodeType === 3) { if (node.textContent.trim()) content('text', node); else gapNodes.push(node); return; } if (node.nodeType !== 1 || ['SCRIPT', 'STYLE', 'NOSCRIPT'].includes(node.tagName)) return; if (node.tagName === 'BR') { gapNodes.push(node); blankLines++; return; } if (node.tagName === 'IMG') { content('image', node); return; } for (const child of node.childNodes) visit(child); } for (const child of body.childNodes) visit(child); for (const { anchor, nodes, count } of changes) { for (const node of nodes) node.remove(); const fragment = body.ownerDocument.createDocumentFragment(); for (let i = 0; i < count; i++) fragment.append(body.ownerDocument.createElement('br')); anchor.parentNode.insertBefore(fragment, anchor); } } function formatBody(body, author, sourceText = null) { const doc = body.ownerDocument; body.querySelectorAll('[data-dww11-wx-source="1"]').forEach(el => el.remove()); // Discuz 会把内联 color 值直接转为 [color=…]。 // !important 不能出现在该值中;颜色和字号只用原生 FONT 属性。 for (const el of [body, ...body.querySelectorAll('*')]) { if (['SCRIPT', 'STYLE'].includes(el.tagName)) continue; for (const property of ['font', 'font-size', 'color', '-webkit-text-fill-color']) { el.style.removeProperty(property); } // 其他排版属性也不保留 CSS 优先级,避免它泄漏进论坛标签。 for (const property of Array.from(el.style)) { if (el.style.getPropertyPriority(property)) { el.style.setProperty(property, el.style.getPropertyValue(property)); } } if (!el.style.cssText) el.removeAttribute('style'); } // 去掉采集器或旧版脚本生成的嵌套 FONT,保留其中的内容和图片。 for (const font of Array.from(body.querySelectorAll('font'))) { font.replaceWith(...font.childNodes); } flattenParagraphs(body); spaceImages(body); // 所有正文内容和换行放在同一 FONT 中。不能使用只有 BR 的空 FONT: // 论坛转换器可能清除空格式标签,导致空行退回默认1号或直接丢失。 const articleFont = doc.createElement('font'); articleFont.size = '4'; articleFont.color = '#000000'; articleFont.dataset.dww11WxBody = '1'; articleFont.append(...body.childNodes); body.append(articleFont); // A 自身的浏览器默认蓝色会覆盖外层继承色,链接文字另设黑色。 for (const link of articleFont.querySelectorAll('a')) { const linkFont = doc.createElement('font'); linkFont.size = '4'; linkFont.color = '#000000'; linkFont.append(...link.childNodes); link.append(linkFont); } // 只用于编辑器图片适配;帖子展示仍交给网站原有图片/附件规则。 for (const img of articleFont.querySelectorAll('img')) { img.style.maxWidth = '100%'; img.style.height = 'auto'; } const source = doc.createElement('p'); source.dataset.dww11WxSource = '1'; const font = doc.createElement('font'); font.size = '1'; font.color = '#c0c0c0'; const lines = (sourceText ?? `来源:${author},如有侵权,请联系删除。`).split('\n'); lines.forEach((line, index) => { if (index) font.append(doc.createElement('br')); font.append(doc.createTextNode(line)); }); source.append(font); body.append(source); } function existingSource(body) { // 按段落和 BR 读取末尾逻辑行,兼容嵌套 FONT、纯文本及末尾空段落。 const lines = []; let parts = []; function flush() { const text = cleanText(parts.map(part => part.node.textContent.slice(part.start, part.end)).join('')); if (text) lines.push({ text, parts }); parts = []; } function visit(node) { if (node.nodeType === 3) { // 保留文本中的显式换行,并记录精确位置,避免删除同容器中的正文。 const value = node.textContent; let start = 0; for (let i = 0; i <= value.length; i++) { if (i !== value.length && value[i] !== '\n') continue; // HTML 缩进换行通常只是空白;只有后续是来源或版权行才分行。 if (i < value.length && !/^\s*(?:(?:来源|文章来源|本文来源|图文来源|文字来源|作者|作者信息|来源作者|转自|转载自|出处)\s*[::]|如有侵权|若有侵权|侵权请|转载请|版权归)/.test(value.slice(i + 1))) continue; if (i > start) parts.push({ node, start, end: i }); if (i < value.length) flush(); start = i + 1; } return; } if (node.nodeType !== 1 || ['SCRIPT', 'STYLE', 'NOSCRIPT'].includes(node.tagName)) return; if (node.tagName === 'BR') { flush(); return; } if (['IMG', 'VIDEO', 'AUDIO', 'IFRAME', 'HR'].includes(node.tagName)) { flush(); lines.push({ text: '\ufffc', parts: [] }); return; } const block = /^(P|DIV|SECTION|ARTICLE|FIGURE|FIGCAPTION|H[1-6]|LI|TR|BLOCKQUOTE|PRE)$/.test(node.tagName); if (block) flush(); for (const child of node.childNodes) visit(child); if (block) flush(); } for (const child of body.childNodes) visit(child); flush(); let first = -1; for (let i = lines.length - 1; i >= 0; i--) { if (/^(?:来源|文章来源|本文来源|图文来源|文字来源|作者|作者信息|来源作者|转自|转载自|出处)\s*[::]\s*\S/.test(lines[i].text)) { first = i; } else if (!/^(?:如有侵权|若有侵权|侵权请|转载请|版权归)/.test(lines[i].text)) { break; } } if (first < 0) return null; const start = lines[first].parts[0]; const range = body.ownerDocument.createRange(); range.setStart(start.node, start.start); range.setEnd(body, body.childNodes.length); return { text: lines.slice(first).map(line => line.text).join('\n'), remove: () => range.deleteContents() }; } window.DWW11Formatter = Object.freeze({ format(expectedSource) { if (Number(window.wysiwyg) !== 1) throw new Error('请切换到可视化编辑器后排版'); const body = document.querySelector('#e_iframe')?.contentDocument?.body; if (!body) throw new Error('编辑器未就绪'); 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('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('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})); return {images: after, source: expected}; } catch (error) { body.innerHTML = backup; throw error; } } }); })(); /* Read-only result adapter. Login state and URLs alone are never acceptance evidence. */ (function(root,factory){ const api=factory(); if(typeof module==='object'&&module.exports)module.exports=api; else root.DWW11Results=api; })(typeof window==='undefined'?null:window,()=>{ 'use strict'; const clean=value=>String(value||'').replace(/\s+/g,' ').trim(); function parameter(href,key,base){try{return new URL(href,base).searchParams.get(key);}catch{return null;}} function aid(image){return String(image.getAttribute('aid')||image.id||'').match(/(?:attachimg_|aimg_)?(\d+)$/)?.[1]||null;} function inspect(doc,url,snapshot={}) { const prompt=doc.querySelector('#messagetext, .messagetext'); const text=clean(prompt?.textContent); const message='新主题需要审核,您的帖子通过审核后才能显示'; if(text.includes(message))return { kind:'accepted_prompt',state:'accepted_pending_moderation', evidence:{adapter:'moderation_prompt',message,observed_text:text,observed_url:url} }; if(/^(?:抱歉[,,]\s*)?(?:您填写的)?(?:帖子)?标题.{0,40}(?:超过|超出).{0,40}(?:字符|长度|限制)/.test(text))return {kind:'rejected',error:text}; return {kind:'waiting',prompt:text}; // 暂停详情页核对:按用户要求注释保留,待恢复时启用。 // const titleNode=doc.querySelector('#thread_subject'); // if(!titleNode) { // const prompt=doc.querySelector('#messagetext, .messagetext'); // const text=clean(prompt?.textContent); // if(/^(?:抱歉[,,]\s*)?(?:您填写的)?(?:帖子)?标题.{0,40}(?:超过|超出).{0,40}(?:字符|长度|限制)/.test(text))return {kind:'rejected',error:text}; // return {kind:'waiting',prompt:text,candidate:prompt?.querySelector('a[href]')?.href||''}; // } // const region=titleNode.closest('td')||titleNode.closest('h1')?.parentElement; // if(!region)return {kind:'incomplete',error:'缺少标题状态区域'}; // const ids=new Set([...region.querySelectorAll('a[href]')].filter(a=>parameter(a.href,'mod',url)==='viewthread').map(a=>parameter(a.href,'tid',url)).filter(x=>/^\d+$/.test(x))); // if(ids.size!==1)return {kind:'incomplete',error:'详情页真实主题编号缺失或冲突'}; // const thread_id=[...ids][0],body=doc.querySelector('[id^="postmessage_"]'); // const pid=body?.id.match(/^postmessage_(\d+)$/)?.[1]; // const post=pid&&doc.querySelector('#post_'+pid); // if(!post||clean(post.querySelector('#postnum'+pid)?.textContent)!=='楼主')return {kind:'incomplete',error:'无法确认当前正文属于首帖'}; // const authors=post?[...post.querySelectorAll('.plc .pti .authi a[href], .pls .authi a[href]')]:[]; // const uids=new Set(authors.map(a=>parameter(a.href,'uid',url)).filter(x=>/^\d+$/.test(x))); // const fids=new Set([...doc.querySelectorAll('#pt a[href], .mumucms_pt a[href]')].filter(a=>parameter(a.href,'mod',url)==='forumdisplay').map(a=>parameter(a.href,'fid',url)).filter(x=>/^\d+$/.test(x))); // if(!body||!pid||uids.size!==1||fids.size!==1)return {kind:'incomplete',error:'首帖作者、正文或版块证据不足'}; // const moderation=[...region.querySelectorAll('span.xg1')].some(n=>/\(\s*审核中\s*\)|(\s*审核中\s*)/.test(n.textContent)); // // A public-page sample is not available yet. Absence of moderation is not proof of publication. // if(!moderation)return {kind:'incomplete',error:'尚无可验证的公开或审核中状态'}; // const allImages=[...body.querySelectorAll('img')]; // const images=allImages.filter(img=>aid(img)).map(img=>({aid:aid(img),src:img.getAttribute('src'),loaded:img.complete&&img.naturalWidth>0&&!/\/none\.gif(?:[?#]|$)/.test(img.src||img.getAttribute('src')||''),file:img.getAttribute('file')||img.getAttribute('zoomfile')})); // const expected=(snapshot.images||[]).map(img=>String(img.aid||img.id||'').match(/(?:attachimg_|aimg_)?(\d+)$/)?.[1]).filter(Boolean); // const issues=[]; // if(allImages.some(img=>!aid(img)))issues.push('正文存在无法关联附件的图片'); // if(expected.length!==(snapshot.images||[]).length)issues.push('提交快照存在无法识别附件编号的图片'); // if(JSON.stringify(expected)!==JSON.stringify(images.map(img=>img.aid)))issues.push('首帖正文图片引用或顺序与提交快照不一致'); // if(images.some(img=>!img.loaded))issues.push('结果页正文图片尚未加载或加载失败'); // if(snapshot.text&&clean(body.innerText||body.textContent)!==clean(snapshot.text))issues.push('结果正文与最终提交快照存在文字差异'); // const attachments=post?[...post.querySelectorAll('.pattl img, .attm img')].map(img=>({aid:aid(img),src:img.getAttribute('src')})):[]; // if(attachments.length)issues.push('结果页存在附件区图片,请核对正文位置'); // return {kind:'accepted',thread_id,first_post_pid:pid,post_url:url,title:clean(titleNode.textContent),author_uid:[...uids][0],fid:Number([...fids][0]),state:'accepted_pending_moderation', // evidence:{adapter:1,thread_id,first_post_pid:pid,title:clean(titleNode.textContent),author_uid:[...uids][0],fid:Number([...fids][0]),moderation:true,images,attachments,integrity_issues:issues}}; } return {inspect,clean}; }); /* 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, stopRevision=0 /*, resultSeenAt=0 */; 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',()=>{stopRevision++;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({...latest,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';job.snapshot=local.snapshot; } if(!['user_submitting','submit_unknown'].includes(job.state))return; const snapshot=job.snapshot||local?.snapshot||{}; const result=(local?.id===job.id&&local.promptResult)||window.DWW11Results?.inspect(document,location.href,snapshot); if(result?.kind==='rejected'&&job.state==='user_submitting') { await rpc('event',{id:job.id,state:'rejected',error:result.error}); status('网站明确拒绝本次提交,已暂停;核对并处理稿件后仍需亲手发表');return; } // 暂停详情页标题、作者、版块和图文核对;保留原代码。 // if(result?.kind==='accepted') { // if(result.author_uid!==job.account.uid||result.fid!==job.fid||result.title!==window.DWW11Results.clean(snapshot.title)) { // if(job.state==='user_submitting')await rpc('event',{id:job.id,state:'submit_unknown',error:'结果页标题、首帖作者或版块与提交任务不符'}); // await rpc('stop');status('结果不匹配,已暂停,请人工核对');return; // } // resultSeenAt ||= Date.now(); // if(result.evidence.integrity_issues.some(x=>x.includes('尚未加载'))&&Date.now()-resultSeenAt<15000) { // status('已看到审核中详情,等待正文图片加载后核对');return; // } if(result?.kind==='accepted_prompt') { const revision=stopRevision,epoch=run.epoch; await rpc('accept',{id:job.id,attempt:job.attempt,...result}); sessionStorage.removeItem(SLOT);active=null; // Re-read persistent control after accounting; Stop must suppress navigation and subsequent jobs. run=await rpc('heartbeat',{account:account(),url:location.href,extension:extension()}); halted=!run.running; if(!halted&&revision===stopRevision&&run.epoch===epoch)location.assign('https://www.dww11.com/'); else status('受理结果已保存;流程已暂停,请核对后明确继续'); return; } const clickedAt=local?.clickedAt||Number(job.submitted_at||0)*1000; if(clickedAt&&Date.now()-clickedAt<45000) { status(result?.prompt||result?.error||'等待网站提交结果,不会重复发表');return; } if(job.state==='user_submitting')await rpc('event',{id:job.id,state:'submit_unknown',error:result?.error||'未观察到网站新主题审核提示,请人工核对是否已受理'}); if(run.running)await rpc('stop'); 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) {await rpc('stop');status('换号插件未就绪,流程已暂停,请检查插件后继续');return;} const revision=stopRevision,epoch=run.epoch; await rpc('switch-begin',{epoch}); if(halted||!run?.running||revision!==stopRevision)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; } if(run.active&&run.owner===owner) { const saved=await rpc('current'); if(saved.job&&['user_submitting','submit_unknown'].includes(saved.job.state)) {active=saved.job;await observeResult(saved.job);return;} } const revision=stopRevision,epoch=run.epoch; const next=await rpc('next',{account:account(),extension:extension(),url:location.href});run=next.run; if(revision!==stopRevision||run.epoch!==epoch)return; 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==='home') { if(!halted&&run.running)location.assign('https://www.dww11.com/'); }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); // 提示可能短暂出现后跳转;及时保存到本次任务,避免两秒轮询漏掉。 function capturePrompt() { const local=loadLocal(); if(!local?.id||!local.snapshot||!local.clickedAt||local.promptResult)return false; if(active&&(active.id!==local.id||!['user_submitting','submit_unknown'].includes(active.state)))return false; const result=window.DWW11Results?.inspect(document,location.href,local.snapshot); if(result?.kind!=='accepted_prompt')return false; saveLocal({...local,promptResult:result}); return true; } if(typeof MutationObserver!=='undefined')new MutationObserver(()=>{ if(capturePrompt())void tick(); }).observe(document.body,{childList:true,subtree:true,characterData:true}); // 跳转后的提示页可能已经渲染完成,首次轮询前也捕获一次。 capturePrompt(); setInterval(tick,2000);tick(); })();