Compare commits
1
Commits
7ab0bc3485
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd9474d0e1
|
No files matched your search
@@ -1,8 +1,16 @@
|
|||||||
# 新闻工作台
|
# 新闻工作台
|
||||||
|
|
||||||
|
## 2026-11-01 换号联动修复
|
||||||
|
|
||||||
|
已定位启动即提示“换号已暂停、超时或账号未改变”的两个原因:换号扩展缺少 `data-auto-news-state` 联动状态,且后端将尚未点击的旧 `pending` 换号任务也按 120 秒超时处理。现已补齐扩展生命周期状态及终止事件监听;待点击任务不再按登录超时处理,缺少联动接口时给出明确提示。
|
||||||
|
|
||||||
|
应用修复需要重启本地服务,在扩展管理页重新加载原安装目录 `dww11-account-switch-browser-extension/` 中的扩展,再刷新论坛。不要卸载重装、清空账号库或删除任务库。原来已手动换到新账号时,接口就绪后会核对新 UID 和插件空闲状态,再建立新批次,不会为了恢复旧任务再次抽号。此次未改动运行数据库或真实账号,尚需重新加载后的实机验证。
|
||||||
|
|
||||||
|
离线回归:`python3 -m unittest discover -s test -p test_queue.py -v`;`node test/extension-bridge.test.cjs`。本机旧版 Node 的 `node --test` 汇总模式解析中文测试名称失败,可逐个直接执行 `.test.cjs` 文件。
|
||||||
|
|
||||||
将新闻下载、文章打包、浏览器导入、排版与换号联动整合到本地控制台。
|
将新闻下载、文章打包、浏览器导入、排版与换号联动整合到本地控制台。
|
||||||
|
|
||||||
**当前版本:0.2.1。下载 → 入队 → 导入 → 排版 → 人工发表 → 网站审核提示 → 受理记账 → 回首页 → 下一篇或换号。** 暂时只以网站提示“新主题需要审核,您的帖子通过审核后才能显示”判定发表成功,状态仍记为已受理、审核中。原详情页标题、作者、版块与图文核对代码已注释保留,当前不执行。
|
**当前版本:0.2.2。下载 → 入队 → 导入 → 排版 → 人工发表 → 网站审核提示 → 受理记账 → 回首页 → 下一篇或换号。** 暂时只以网站提示“新主题需要审核,您的帖子通过审核后才能显示”判定发表成功,状态仍记为已受理、审核中。原详情页标题、作者、版块与图文核对代码已注释保留,当前不执行。
|
||||||
|
|
||||||
## 启动
|
## 启动
|
||||||
|
|
||||||
@@ -123,6 +131,12 @@ node test/browser.test.cjs
|
|||||||
|
|
||||||
更新油猴中的 `browser/auto-news.user.js` 至 0.2.1,并重启本地服务、重新配对后刷新论坛页面。连接脚本无需更新。结果适配器和执行器中的旧详情页核对代码以行注释保留;后端旧核对接口继续保留供人工历史补核使用。自动受理使用当前任务的人工提交批次校验,审核提示可不含帖子链接,不创建虚构帖子 ID。
|
更新油猴中的 `browser/auto-news.user.js` 至 0.2.1,并重启本地服务、重新配对后刷新论坛页面。连接脚本无需更新。结果适配器和执行器中的旧详情页核对代码以行注释保留;后端旧核对接口继续保留供人工历史补核使用。自动受理使用当前任务的人工提交批次校验,审核提示可不含帖子链接,不创建虚构帖子 ID。
|
||||||
|
|
||||||
|
## 0.2.2 悬浮窗控制与跳过图片检查
|
||||||
|
|
||||||
|
自动流程不再等待编辑器图片加载,也不检查排版前后图片标识和顺序。原检查函数保留,独立手动导入仍沿用默认检查。图片上传与本地文件格式限制继续用于构建可导入的文章。人工发表后只认网站指定审核提示,持久记账后回首页,按已配置间隔继续下一篇或换号。
|
||||||
|
|
||||||
|
论坛悬浮窗新增“开始 / 继续”和“暂停”,与工作台控制相同。开始前须在工作台保存处理规则;暂停会立即取消本标签可取消的操作,并通知本地服务与其他标签。更新两个油猴脚本:导入排版 0.2.2、本地连接 0.1.2;重启本地服务并重新配对。
|
||||||
|
|
||||||
## 标题自动截断
|
## 标题自动截断
|
||||||
|
|
||||||
自动队列、手动目录和文章包导入均应用上述规则;已截断的标题不会重复添加省略号。已有队列在领取未开始的任务时更新标题,已导入、待审核或已提交任务保持现场标题。
|
自动队列、手动目录和文章包导入均应用上述规则;已截断的标题不会重复添加省略号。已有队列在领取未开始的任务时更新标题,已导入、待审核或已提交任务保持现场标题。
|
||||||
|
|||||||
@@ -105,9 +105,10 @@ class Controller:
|
|||||||
methods = {'heartbeat': self.store.heartbeat, 'next': self.store.next, 'package': self.store.package,
|
methods = {'heartbeat': self.store.heartbeat, 'next': self.store.next, 'package': self.store.package,
|
||||||
'event': self.store.event, 'accept': self.store.accept, 'switch-begin': self.store.switch_begin,
|
'event': self.store.event, 'accept': self.store.accept, 'switch-begin': self.store.switch_begin,
|
||||||
'takeover': self.store.takeover, 'current': self.store.current, 'skip': self.store.skip}
|
'takeover': self.store.takeover, 'current': self.store.current, 'skip': self.store.skip}
|
||||||
if route == 'stop':
|
if route in ('start', 'pause', 'stop'):
|
||||||
self.manager.stop.set()
|
if route == 'stop':
|
||||||
return self.store.control('stop')
|
self.manager.stop.set()
|
||||||
|
return self.store.control(route)
|
||||||
if route not in methods:
|
if route not in methods:
|
||||||
raise ValueError('未知浏览器操作')
|
raise ValueError('未知浏览器操作')
|
||||||
return methods[route](body)
|
return methods[route](body)
|
||||||
|
|||||||
+11
-10
@@ -1,7 +1,7 @@
|
|||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name 新闻工作台 · 导入排版与流程
|
// @name 新闻工作台 · 导入排版与流程
|
||||||
// @namespace local.dww11.auto-news
|
// @namespace local.dww11.auto-news
|
||||||
// @version 0.2.1
|
// @version 0.2.2
|
||||||
// @match https://www.dww11.com/*
|
// @match https://www.dww11.com/*
|
||||||
// @match https://dww11.com/*
|
// @match https://dww11.com/*
|
||||||
// @match https://house.dww11.com/*
|
// @match https://house.dww11.com/*
|
||||||
@@ -439,7 +439,7 @@
|
|||||||
if (switcher) switcher.checked = Number(win.wysiwyg) === 0;
|
if (switcher) switcher.checked = Number(win.wysiwyg) === 0;
|
||||||
}
|
}
|
||||||
async function importPrepared(options = {}) {
|
async function importPrepared(options = {}) {
|
||||||
const { signal, automatic = false } = options;
|
const { signal, automatic = false, verifyImages = true } = options;
|
||||||
signal?.throwIfAborted();
|
signal?.throwIfAborted();
|
||||||
if (busy) throw new Error('正在导入,请勿重复点击。');
|
if (busy) throw new Error('正在导入,请勿重复点击。');
|
||||||
if (!article) throw new Error('请先选择文章目录或文章包。');
|
if (!article) throw new Error('请先选择文章目录或文章包。');
|
||||||
@@ -492,10 +492,10 @@
|
|||||||
signal?.throwIfAborted();
|
signal?.throwIfAborted();
|
||||||
setEditor(selected.title, bbcode);
|
setEditor(selected.title, bbcode);
|
||||||
const raw = win.getEditorContents();
|
const raw = win.getEditorContents();
|
||||||
if (Number(win.wysiwyg) === 1) {
|
if (Number(win.wysiwyg) === 1 && verifyImages) {
|
||||||
const expected = Array.from(bbcode.matchAll(/\[attachimg\](\d+)\[\/attachimg\]/g), match => match[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);
|
await verifyEditorImages(doc.querySelector('#e_iframe')?.contentDocument?.body || win.editdoc?.body, expected, signal);
|
||||||
} else if (raw !== bbcode) throw new Error('正文填写校验失败。');
|
} else if (Number(win.wysiwyg) !== 1 && raw !== bbcode) throw new Error('正文填写校验失败。');
|
||||||
if (doc.querySelector('#subject').value !== selected.title) throw new Error('标题填写校验失败。');
|
if (doc.querySelector('#subject').value !== selected.title) throw new Error('标题填写校验失败。');
|
||||||
committed = true;
|
committed = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -727,7 +727,7 @@ const cleanText = value => String(value || '').replace(/\s+/g, ' ').trim();
|
|||||||
}
|
}
|
||||||
|
|
||||||
window.DWW11Formatter = Object.freeze({
|
window.DWW11Formatter = Object.freeze({
|
||||||
format(expectedSource) {
|
format(expectedSource, {verifyImages = true} = {}) {
|
||||||
if (Number(window.wysiwyg) !== 1) throw new Error('请切换到可视化编辑器后排版');
|
if (Number(window.wysiwyg) !== 1) throw new Error('请切换到可视化编辑器后排版');
|
||||||
const body = document.querySelector('#e_iframe')?.contentDocument?.body;
|
const body = document.querySelector('#e_iframe')?.contentDocument?.body;
|
||||||
if (!body) throw new Error('编辑器未就绪');
|
if (!body) throw new Error('编辑器未就绪');
|
||||||
@@ -740,7 +740,7 @@ window.DWW11Formatter = Object.freeze({
|
|||||||
footer.remove();
|
footer.remove();
|
||||||
formatBody(body, expectedSource, expected);
|
formatBody(body, expectedSource, expected);
|
||||||
const after = [...body.querySelectorAll('img')].map(x => [x.id, x.getAttribute('aid'), x.getAttribute('src')].join(':'));
|
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 (verifyImages && JSON.stringify(images) !== JSON.stringify(after)) throw new Error('排版后图片标识或顺序改变');
|
||||||
if (body.querySelectorAll('[data-dww11-wx-source="1"]').length !== 1) throw new Error('来源尾注数量异常');
|
if (body.querySelectorAll('[data-dww11-wx-source="1"]').length !== 1) throw new Error('来源尾注数量异常');
|
||||||
body.dispatchEvent(new Event('input', {bubbles:true}));
|
body.dispatchEvent(new Event('input', {bubbles:true}));
|
||||||
return {images: after, source: expected};
|
return {images: after, source: expected};
|
||||||
@@ -892,14 +892,15 @@ window.DWW11Formatter = Object.freeze({
|
|||||||
const prepared=await window.DWW11Importer.stagePackage(pack);assertLive(epoch);
|
const prepared=await window.DWW11Importer.stagePackage(pack);assertLive(epoch);
|
||||||
if(prepared.warnings?.length)throw new Error('文章需要人工处理:'+prepared.warnings.join(';'));
|
if(prepared.warnings?.length)throw new Error('文章需要人工处理:'+prepared.warnings.join(';'));
|
||||||
await event('importing');assertLive(epoch);status('上传图片并填写文章…');
|
await event('importing');assertLive(epoch);status('上传图片并填写文章…');
|
||||||
const result=await window.DWW11Importer.importPrepared({automatic:true,signal:aborter.signal});assertLive(epoch);
|
const result=await window.DWW11Importer.importPrepared({automatic:true,verifyImages:false,signal:aborter.signal});assertLive(epoch);
|
||||||
if(result?.cancelled)throw new Error('导入已取消');
|
if(result?.cancelled)throw new Error('导入已取消');
|
||||||
await event('formatting');assertLive(epoch);
|
await event('formatting');assertLive(epoch);
|
||||||
window.DWW11Formatter.format(job.source);assertLive(epoch);
|
window.DWW11Formatter.format(job.source,{verifyImages:false});assertLive(epoch);
|
||||||
const snapshot=editorSnapshot();
|
const snapshot=editorSnapshot();
|
||||||
if(snapshot.title!==job.title)throw new Error('标题校验失败');
|
if(snapshot.title!==job.title)throw new Error('标题校验失败');
|
||||||
await window.DWW11Importer.verifyEditorImages(
|
// 按用户要求暂停图片检查;保留原调用以便恢复。
|
||||||
document.querySelector('#e_iframe').contentDocument.body, result.attachmentOrder, aborter.signal);
|
// await window.DWW11Importer.verifyEditorImages(
|
||||||
|
// document.querySelector('#e_iframe').contentDocument.body, result.attachmentOrder, aborter.signal);
|
||||||
assertLive(epoch);
|
assertLive(epoch);
|
||||||
await event('awaiting_user_review',{snapshot});assertLive(epoch);
|
await event('awaiting_user_review',{snapshot});assertLive(epoch);
|
||||||
active={...job,state:'awaiting_user_review'};saveLocal({id:job.id});
|
active={...job,state:'awaiting_user_review'};saveLocal({id:job.id});
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ const cleanText = value => String(value || '').replace(/\s+/g, ' ').trim();
|
|||||||
}
|
}
|
||||||
|
|
||||||
window.DWW11Formatter = Object.freeze({
|
window.DWW11Formatter = Object.freeze({
|
||||||
format(expectedSource) {
|
format(expectedSource, {verifyImages = true} = {}) {
|
||||||
if (Number(window.wysiwyg) !== 1) throw new Error('请切换到可视化编辑器后排版');
|
if (Number(window.wysiwyg) !== 1) throw new Error('请切换到可视化编辑器后排版');
|
||||||
const body = document.querySelector('#e_iframe')?.contentDocument?.body;
|
const body = document.querySelector('#e_iframe')?.contentDocument?.body;
|
||||||
if (!body) throw new Error('编辑器未就绪');
|
if (!body) throw new Error('编辑器未就绪');
|
||||||
@@ -185,7 +185,7 @@ window.DWW11Formatter = Object.freeze({
|
|||||||
footer.remove();
|
footer.remove();
|
||||||
formatBody(body, expectedSource, expected);
|
formatBody(body, expectedSource, expected);
|
||||||
const after = [...body.querySelectorAll('img')].map(x => [x.id, x.getAttribute('aid'), x.getAttribute('src')].join(':'));
|
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 (verifyImages && JSON.stringify(images) !== JSON.stringify(after)) throw new Error('排版后图片标识或顺序改变');
|
||||||
if (body.querySelectorAll('[data-dww11-wx-source="1"]').length !== 1) throw new Error('来源尾注数量异常');
|
if (body.querySelectorAll('[data-dww11-wx-source="1"]').length !== 1) throw new Error('来源尾注数量异常');
|
||||||
body.dispatchEvent(new Event('input', {bubbles:true}));
|
body.dispatchEvent(new Event('input', {bubbles:true}));
|
||||||
return {images: after, source: expected};
|
return {images: after, source: expected};
|
||||||
|
|||||||
+3
-3
@@ -439,7 +439,7 @@
|
|||||||
if (switcher) switcher.checked = Number(win.wysiwyg) === 0;
|
if (switcher) switcher.checked = Number(win.wysiwyg) === 0;
|
||||||
}
|
}
|
||||||
async function importPrepared(options = {}) {
|
async function importPrepared(options = {}) {
|
||||||
const { signal, automatic = false } = options;
|
const { signal, automatic = false, verifyImages = true } = options;
|
||||||
signal?.throwIfAborted();
|
signal?.throwIfAborted();
|
||||||
if (busy) throw new Error('正在导入,请勿重复点击。');
|
if (busy) throw new Error('正在导入,请勿重复点击。');
|
||||||
if (!article) throw new Error('请先选择文章目录或文章包。');
|
if (!article) throw new Error('请先选择文章目录或文章包。');
|
||||||
@@ -492,10 +492,10 @@
|
|||||||
signal?.throwIfAborted();
|
signal?.throwIfAborted();
|
||||||
setEditor(selected.title, bbcode);
|
setEditor(selected.title, bbcode);
|
||||||
const raw = win.getEditorContents();
|
const raw = win.getEditorContents();
|
||||||
if (Number(win.wysiwyg) === 1) {
|
if (Number(win.wysiwyg) === 1 && verifyImages) {
|
||||||
const expected = Array.from(bbcode.matchAll(/\[attachimg\](\d+)\[\/attachimg\]/g), match => match[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);
|
await verifyEditorImages(doc.querySelector('#e_iframe')?.contentDocument?.body || win.editdoc?.body, expected, signal);
|
||||||
} else if (raw !== bbcode) throw new Error('正文填写校验失败。');
|
} else if (Number(win.wysiwyg) !== 1 && raw !== bbcode) throw new Error('正文填写校验失败。');
|
||||||
if (doc.querySelector('#subject').value !== selected.title) throw new Error('标题填写校验失败。');
|
if (doc.querySelector('#subject').value !== selected.title) throw new Error('标题填写校验失败。');
|
||||||
committed = true;
|
committed = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name 新闻工作台 · 本地连接
|
// @name 新闻工作台 · 本地连接
|
||||||
// @namespace local.dww11.auto-news
|
// @namespace local.dww11.auto-news
|
||||||
// @version 0.1.1
|
// @version 0.1.2
|
||||||
// @match https://www.dww11.com/*
|
// @match https://www.dww11.com/*
|
||||||
// @match https://dww11.com/*
|
// @match https://dww11.com/*
|
||||||
// @match https://house.dww11.com/*
|
// @match https://house.dww11.com/*
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
// ==/UserScript==
|
// ==/UserScript==
|
||||||
(() => {
|
(() => {
|
||||||
'use strict';
|
'use strict';
|
||||||
const allowed = new Set(['heartbeat','next','package','event','accept','switch-begin','takeover','stop','current','skip']);
|
const allowed = new Set(['heartbeat','next','package','event','accept','switch-begin','takeover','start','pause','stop','current','skip']);
|
||||||
let token = GM_getValue('pairToken',''), port = GM_getValue('pairPort',8770);
|
let token = GM_getValue('pairToken',''), port = GM_getValue('pairPort',8770);
|
||||||
let stopRevision = 0;
|
let stopRevision = 0;
|
||||||
const signalStop = () => { stopRevision++; document.dispatchEvent(new CustomEvent('dww11:auto-stop')); };
|
const signalStop = () => { stopRevision++; document.dispatchEvent(new CustomEvent('dww11:auto-stop')); };
|
||||||
@@ -35,13 +35,14 @@
|
|||||||
try {message=JSON.parse(event.detail);} catch {return;}
|
try {message=JSON.parse(event.detail);} catch {return;}
|
||||||
if (!allowed.has(message.route) || typeof message.id !== 'string') return;
|
if (!allowed.has(message.route) || typeof message.id !== 'string') return;
|
||||||
try {
|
try {
|
||||||
if (message.route==='stop') { GM_setValue('halted',true); signalStop(); }
|
if (message.route==='stop'||message.route==='pause') { GM_setValue('halted',true); signalStop(); }
|
||||||
const revision = stopRevision;
|
const revision = stopRevision;
|
||||||
let value=await request(message.route,message.body || {});
|
let value=await request(message.route,message.body || {});
|
||||||
if (message.route==='heartbeat') {
|
if (message.route==='heartbeat') {
|
||||||
if (revision !== stopRevision) value = {...value, running:false, reason:'终止后忽略旧连接响应'};
|
if (revision !== stopRevision) value = {...value, running:false, reason:'终止后忽略旧连接响应'};
|
||||||
GM_setValue('halted',!value.running);
|
GM_setValue('halted',!value.running);
|
||||||
if (!value.running) signalStop();
|
if (!value.running) signalStop();
|
||||||
|
document.dispatchEvent(new CustomEvent('dww11:run-state',{detail:Boolean(value.running)}));
|
||||||
}
|
}
|
||||||
document.dispatchEvent(new CustomEvent('dww11:local-response',{detail:JSON.stringify({id:message.id,value})}));
|
document.dispatchEvent(new CustomEvent('dww11:local-response',{detail:JSON.stringify({id:message.id,value})}));
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
@@ -56,6 +57,32 @@
|
|||||||
const shadow=host.attachShadow({mode:'closed'});
|
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>';
|
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 $=id=>shadow.getElementById(id);$('port').value=port;
|
||||||
|
const start=document.createElement('button');start.id='start';start.type='button';start.textContent='开始 / 继续';
|
||||||
|
const pause=document.createElement('button');pause.id='pause';pause.type='button';pause.textContent='暂停';
|
||||||
|
$('halt').before(start);$('halt').before(pause);
|
||||||
|
let starting=false;
|
||||||
|
start.onclick=async()=>{
|
||||||
|
if(starting||start.disabled)return;
|
||||||
|
starting=true;start.disabled=true;
|
||||||
|
const revision=stopRevision;
|
||||||
|
try {
|
||||||
|
await request('start',{});
|
||||||
|
if(revision!==stopRevision) {
|
||||||
|
await request('pause',{});
|
||||||
|
$('status').textContent='已暂停;忽略较早的开始响应';
|
||||||
|
}else {
|
||||||
|
GM_setValue('halted',false);
|
||||||
|
$('status').textContent='已开始;等待执行标签继续流程';
|
||||||
|
}
|
||||||
|
}catch(error){$('status').textContent='开始失败:'+error.message;}
|
||||||
|
finally{starting=false;start.disabled=false;}
|
||||||
|
};
|
||||||
|
pause.onclick=async()=>{
|
||||||
|
GM_setValue('halted',true);signalStop();
|
||||||
|
$('status').textContent='已暂停;在途请求仍可能完成';
|
||||||
|
try {await request('pause',{});}catch(error){$('status').textContent+='。'+error.message;}
|
||||||
|
};
|
||||||
|
document.addEventListener('dww11:run-state',event=>{start.disabled=starting||Boolean(event.detail);});
|
||||||
const skip=document.createElement('button');skip.id='skip';skip.type='button';skip.textContent='跳过本文章';
|
const skip=document.createElement('button');skip.id='skip';skip.type='button';skip.textContent='跳过本文章';
|
||||||
$('halt').before(skip);
|
$('halt').before(skip);
|
||||||
skip.onclick=()=>document.dispatchEvent(new CustomEvent('dww11:skip-article'));
|
skip.onclick=()=>document.dispatchEvent(new CustomEvent('dww11:skip-article'));
|
||||||
|
|||||||
+5
-4
@@ -80,14 +80,15 @@
|
|||||||
const prepared=await window.DWW11Importer.stagePackage(pack);assertLive(epoch);
|
const prepared=await window.DWW11Importer.stagePackage(pack);assertLive(epoch);
|
||||||
if(prepared.warnings?.length)throw new Error('文章需要人工处理:'+prepared.warnings.join(';'));
|
if(prepared.warnings?.length)throw new Error('文章需要人工处理:'+prepared.warnings.join(';'));
|
||||||
await event('importing');assertLive(epoch);status('上传图片并填写文章…');
|
await event('importing');assertLive(epoch);status('上传图片并填写文章…');
|
||||||
const result=await window.DWW11Importer.importPrepared({automatic:true,signal:aborter.signal});assertLive(epoch);
|
const result=await window.DWW11Importer.importPrepared({automatic:true,verifyImages:false,signal:aborter.signal});assertLive(epoch);
|
||||||
if(result?.cancelled)throw new Error('导入已取消');
|
if(result?.cancelled)throw new Error('导入已取消');
|
||||||
await event('formatting');assertLive(epoch);
|
await event('formatting');assertLive(epoch);
|
||||||
window.DWW11Formatter.format(job.source);assertLive(epoch);
|
window.DWW11Formatter.format(job.source,{verifyImages:false});assertLive(epoch);
|
||||||
const snapshot=editorSnapshot();
|
const snapshot=editorSnapshot();
|
||||||
if(snapshot.title!==job.title)throw new Error('标题校验失败');
|
if(snapshot.title!==job.title)throw new Error('标题校验失败');
|
||||||
await window.DWW11Importer.verifyEditorImages(
|
// 按用户要求暂停图片检查;保留原调用以便恢复。
|
||||||
document.querySelector('#e_iframe').contentDocument.body, result.attachmentOrder, aborter.signal);
|
// await window.DWW11Importer.verifyEditorImages(
|
||||||
|
// document.querySelector('#e_iframe').contentDocument.body, result.attachmentOrder, aborter.signal);
|
||||||
assertLive(epoch);
|
assertLive(epoch);
|
||||||
await event('awaiting_user_review',{snapshot});assertLive(epoch);
|
await event('awaiting_user_review',{snapshot});assertLive(epoch);
|
||||||
active={...job,state:'awaiting_user_review'};saveLocal({id:job.id});
|
active={...job,state:'awaiting_user_review'};saveLocal({id:job.id});
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ ROOT = Path(__file__).resolve().parent
|
|||||||
header = '''// ==UserScript==
|
header = '''// ==UserScript==
|
||||||
// @name 新闻工作台 · 导入排版与流程
|
// @name 新闻工作台 · 导入排版与流程
|
||||||
// @namespace local.dww11.auto-news
|
// @namespace local.dww11.auto-news
|
||||||
// @version 0.2.1
|
// @version 0.2.2
|
||||||
// @match https://www.dww11.com/*
|
// @match https://www.dww11.com/*
|
||||||
// @match https://dww11.com/*
|
// @match https://dww11.com/*
|
||||||
// @match https://house.dww11.com/*
|
// @match https://house.dww11.com/*
|
||||||
|
|||||||
@@ -165,13 +165,19 @@ class Store:
|
|||||||
self.run['owner'] = owner
|
self.run['owner'] = owner
|
||||||
if self.run['switch']:
|
if self.run['switch']:
|
||||||
switch = self.run['switch']
|
switch = self.run['switch']
|
||||||
if account['uid'] != switch['from'] and data.get('extension', {}).get('idle') is True:
|
ext = data.get('extension') or {}
|
||||||
|
if ext.get('ready') is not True:
|
||||||
|
self.run.update(running=False, reason='换号插件联动接口未就绪,请在原安装目录更新并重新加载插件、刷新网站后继续;未重新抽号')
|
||||||
|
self._persist(db)
|
||||||
|
return dict(action='idle', run=self.run)
|
||||||
|
if account['uid'] != switch['from'] and ext.get('idle') is True:
|
||||||
self.run.update(batch=self._batch(account), switch=None)
|
self.run.update(batch=self._batch(account), switch=None)
|
||||||
self._persist(db)
|
self._persist(db)
|
||||||
else:
|
else:
|
||||||
ext = data.get('extension', {})
|
# Pending means no click was issued. Time spent stopped, or
|
||||||
|
# waiting for the plugin, is not a failed login attempt.
|
||||||
elapsed = time.time() - switch.get('clicked_at', switch['created_at'])
|
elapsed = time.time() - switch.get('clicked_at', switch['created_at'])
|
||||||
if ext.get('paused') or (switch['state'] == 'clicked' and elapsed > 10 and ext.get('idle')) or elapsed > 120:
|
if ext.get('paused') or (switch['state'] == 'clicked' and (elapsed > 120 or (elapsed > 10 and ext.get('idle')))):
|
||||||
self.run.update(running=False, reason='换号已暂停、超时或账号未改变,请检查插件并人工恢复,禁止重复抽号')
|
self.run.update(running=False, reason='换号已暂停、超时或账号未改变,请检查插件并人工恢复,禁止重复抽号')
|
||||||
self._persist(db)
|
self._persist(db)
|
||||||
return dict(action='idle', run=self.run)
|
return dict(action='idle', run=self.run)
|
||||||
|
|||||||
+20
-1
@@ -12,7 +12,7 @@ test('终止后较早的 heartbeat 响应不能恢复流程',async()=>{
|
|||||||
requests[1].onload({status:200,responseText:JSON.stringify({running:false,epoch:2})});await stop;
|
requests[1].onload({status:200,responseText:JSON.stringify({running:false,epoch:2})});await stop;
|
||||||
});
|
});
|
||||||
|
|
||||||
test('悬浮窗跳过按钮发送页面事件并防止忙碌时重复点击',()=>{
|
test('悬浮窗开始、暂停、跳过与开始响应竞态',async()=>{
|
||||||
const handlers=new Map(),elements=new Map(),emitted=[],requests=[];
|
const handlers=new Map(),elements=new Map(),emitted=[],requests=[];
|
||||||
const element=()=>({style:{},before(child){elements.set(child.id,child);}});
|
const element=()=>({style:{},before(child){elements.set(child.id,child);}});
|
||||||
for(const id of ['halt','port','token','save','status'])elements.set(id,element());
|
for(const id of ['halt','port','token','save','status'])elements.set(id,element());
|
||||||
@@ -33,4 +33,23 @@ test('悬浮窗跳过按钮发送页面事件并防止忙碌时重复点击',()=
|
|||||||
doc.dispatchEvent(new Event('dww11:local-request',{detail:JSON.stringify({id:'skip-test',route:'skip',body:{id:'job'}})}));
|
doc.dispatchEvent(new Event('dww11:local-request',{detail:JSON.stringify({id:'skip-test',route:'skip',body:{id:'job'}})}));
|
||||||
assert.equal(requests[0].url,'http://127.0.0.1:8770/bridge/skip');
|
assert.equal(requests[0].url,'http://127.0.0.1:8770/bridge/skip');
|
||||||
requests[0].onload({status:200,responseText:'{"ok":true}'});
|
requests[0].onload({status:200,responseText:'{"ok":true}'});
|
||||||
|
const start=elements.get('start'),pause=elements.get('pause');
|
||||||
|
assert.equal(start.textContent,'开始 / 继续');assert.equal(pause.textContent,'暂停');
|
||||||
|
let started=start.onclick();assert.equal(start.disabled,true);
|
||||||
|
assert.ok(requests.at(-1).url.endsWith('/bridge/start'));
|
||||||
|
requests.at(-1).onload({status:200,responseText:'{"running":true}'});await started;
|
||||||
|
doc.dispatchEvent(new Event('dww11:run-state',{detail:true}));assert.equal(start.disabled,true);
|
||||||
|
const paused=pause.onclick();assert.equal(emitted.at(-1).type,'dww11:auto-stop');
|
||||||
|
assert.ok(requests.at(-1).url.endsWith('/bridge/pause'));
|
||||||
|
requests.at(-1).onload({status:200,responseText:'{"running":false}'});await paused;
|
||||||
|
doc.dispatchEvent(new Event('dww11:run-state',{detail:false}));assert.equal(start.disabled,false);
|
||||||
|
// Pause while Start is still in flight: a late Start must be followed by Pause.
|
||||||
|
started=start.onclick();const lateStart=requests.at(-1);
|
||||||
|
const pausing=pause.onclick();requests.at(-1).onload({status:200,responseText:'{"running":false}'});await pausing;
|
||||||
|
lateStart.onload({status:200,responseText:'{"running":true}'});
|
||||||
|
await new Promise(resolve=>setImmediate(resolve));
|
||||||
|
assert.ok(requests.at(-1).url.endsWith('/bridge/pause'));
|
||||||
|
requests.at(-1).onload({status:200,responseText:'{"running":false}'});await started;
|
||||||
|
assert.match(elements.get('status').textContent,/已暂停/);
|
||||||
|
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
'use strict';
|
||||||
|
const {test}=require('node:test');
|
||||||
|
const assert=require('node:assert/strict');
|
||||||
|
const fs=require('node:fs');
|
||||||
|
const path=require('node:path');
|
||||||
|
const vm=require('node:vm');
|
||||||
|
const flush=async()=>{for(let i=0;i<12;i++)await new Promise(resolve=>setImmediate(resolve));};
|
||||||
|
|
||||||
|
function fixture({holdStart=false,holdResume=false,flow=null,helloError=false}={}) {
|
||||||
|
const elements=[],handlers=new Map(),routes=[];
|
||||||
|
let finishStart,finishResume;
|
||||||
|
function element(){
|
||||||
|
const attrs=new Map();
|
||||||
|
const el={style:{},hidden:false,disabled:false,children:[],
|
||||||
|
append(...nodes){this.children.push(...nodes);},prepend(){},before(){},remove(){},
|
||||||
|
addEventListener(type,fn){this[type]=fn;},removeAttribute(key){attrs.delete(key);},
|
||||||
|
setAttribute(key,value){attrs.set(key,value);},getAttribute:key=>attrs.get(key)??null,
|
||||||
|
attachShadow:()=>element()};
|
||||||
|
elements.push(el);return el;
|
||||||
|
}
|
||||||
|
const doc={body:element(),createElement:element,querySelector:()=>null,querySelectorAll:()=>[],
|
||||||
|
addEventListener(type,fn){handlers.set(type,fn);}};
|
||||||
|
const context={document:doc,location:{origin:'https://www.dww11.com',hostname:'www.dww11.com',href:'https://www.dww11.com/'},
|
||||||
|
URL,Node:{TEXT_NODE:3},MutationObserver:class{observe(){}},setTimeout,
|
||||||
|
chrome:{runtime:{sendMessage:async({route})=>{
|
||||||
|
routes.push(route);
|
||||||
|
if(route==='/hello')return helloError?{ok:false,error:'unavailable'}:{ok:true,data:{flow,total:1}};
|
||||||
|
if(route==='/start'&&holdStart)return new Promise(resolve=>{finishStart=()=>resolve({ok:true,data:{flow:{id:'test',phase:'logout',paused:false}}});});
|
||||||
|
if(route==='/resume'&&holdResume)return new Promise(resolve=>{finishResume=()=>resolve({ok:true,data:{flow:{id:'test',phase:'logout',paused:false}}});});
|
||||||
|
if(route==='/suspend')return {ok:true,data:{}};
|
||||||
|
throw new Error('Unexpected route '+route);
|
||||||
|
}}}};
|
||||||
|
vm.runInNewContext(fs.readFileSync(path.join(__dirname,'../../dww11-account-switch-browser-extension/content.js'),'utf8'),context);
|
||||||
|
return {routes,state:()=>JSON.parse(elements.find(e=>e.id==='dww11-extension-panel').getAttribute('data-auto-news-state')),
|
||||||
|
click:async(text)=>{elements.find(e=>e.textContent===text).click();await flush();},
|
||||||
|
stop:()=>handlers.get('dww11:auto-stop')(),finishStart:()=>finishStart(),finishResume:()=>finishResume()};
|
||||||
|
}
|
||||||
|
|
||||||
|
test('插件公开生命周期状态,不包含账号和凭据',async()=>{
|
||||||
|
const f=fixture();await flush();
|
||||||
|
assert.deepEqual(f.state(),{ready:true,idle:true,paused:false});
|
||||||
|
const failed=fixture({helloError:true});await flush();
|
||||||
|
assert.deepEqual(failed.state(),{ready:false,idle:false,paused:false});
|
||||||
|
});
|
||||||
|
test('抽号响应返回前终止,保留该次抽号并暂停,不执行登录',async()=>{
|
||||||
|
const f=fixture({holdStart:true});await flush();await f.click('登陆下一帐号');
|
||||||
|
assert.equal(f.state().idle,false);f.stop();f.finishStart();await flush();
|
||||||
|
assert.deepEqual(f.routes,['/hello','/start','/suspend']);
|
||||||
|
assert.deepEqual(f.state(),{ready:true,idle:false,paused:true});
|
||||||
|
});
|
||||||
|
test('恢复响应返回前终止,不执行登录或再次抽号',async()=>{
|
||||||
|
const f=fixture({holdResume:true,flow:{id:'test',paused:true,phase:'logout'}});await flush();
|
||||||
|
await f.click('继续未完成流程');f.stop();f.finishResume();await flush();
|
||||||
|
assert.deepEqual(f.routes,['/hello','/resume','/suspend']);
|
||||||
|
assert.equal(f.state().paused,true);
|
||||||
|
});
|
||||||
@@ -182,6 +182,11 @@ test('真实站点 aid 属性:上传成功但图片响应失败不得报告完
|
|||||||
await assert.rejects(h.api.importPrepared({automatic:true}),/网站附件图片无法显示/);
|
await assert.rejects(h.api.importPrepared({automatic:true}),/网站附件图片无法显示/);
|
||||||
assert.equal(h.ids.get('subject').value,'');assert.equal(h.api.getState().result,null);
|
assert.equal(h.ids.get('subject').value,'');assert.equal(h.api.getState().result,null);
|
||||||
});
|
});
|
||||||
|
test('自动流程关闭图片检查后,不等待或拒绝无法显示的图片', async () => {
|
||||||
|
const h=harness({brokenImages:true});await h.api.stageFiles(articleFiles());
|
||||||
|
const result=await h.api.importPrepared({automatic:true,verifyImages:false});
|
||||||
|
assert.ok(result.attachmentOrder.length);assert.ok(h.ids.get('subject').value);
|
||||||
|
});
|
||||||
test('链接内图片、多行图文链接均参与上传并按原位置渲染', () => {
|
test('链接内图片、多行图文链接均参与上传并按原位置渲染', () => {
|
||||||
const md='# 标题\n\n[\n\n### 宣传标题\n\n说明](https://example.com/promo)\n\n';
|
const md='# 标题\n\n[\n\n### 宣传标题\n\n说明](https://example.com/promo)\n\n';
|
||||||
const article=core.compile(md,'a.md',new Map([['image004.jpg',new Blob(['x'])],['image001.jpg',new Blob(['y'])]]));
|
const article=core.compile(md,'a.md',new Map([['image004.jpg',new Blob(['x'])],['image001.jpg',new Blob(['y'])]]));
|
||||||
|
|||||||
@@ -49,10 +49,11 @@ function fixture({holdImport=false,holdHeartbeat=false,rejectSkip=false,result=n
|
|||||||
location:{href:'https://www.dww11.com/forum.php?mod=post&action=newthread&fid=125',assign:url=>navigations.push(url)},
|
location:{href:'https://www.dww11.com/forum.php?mod=post&action=newthread&fid=125',assign:url=>navigations.push(url)},
|
||||||
setInterval:fn=>{tick=fn;},setTimeout:(fn,ms)=>{const t=setTimeout(fn,ms);timers.add(t);return t;},clearTimeout:t=>{clearTimeout(t);timers.delete(t);}
|
setInterval:fn=>{tick=fn;},setTimeout:(fn,ms)=>{const t=setTimeout(fn,ms);timers.add(t);return t;},clearTimeout:t=>{clearTimeout(t);timers.delete(t);}
|
||||||
};
|
};
|
||||||
context.window={DWW11Results:result?{inspect:()=>result,clean:v=>String(v||'').trim()}:null,DWW11Importer:{stagePackage:async()=>({}),importPrepared:async({signal})=>{
|
context.window={DWW11Results:result?{inspect:()=>result,clean:v=>String(v||'').trim()}:null,DWW11Importer:{stagePackage:async()=>({}),importPrepared:async({signal,verifyImages})=>{
|
||||||
|
assert.equal(verifyImages,false);
|
||||||
if(holdImport)await new Promise((resolve,reject)=>{finishImport=()=>{rolledBack=true;reject(signal.reason);};signal.addEventListener('abort',()=>{}, {once:true});});
|
if(holdImport)await new Promise((resolve,reject)=>{finishImport=()=>{rolledBack=true;reject(signal.reason);};signal.addEventListener('abort',()=>{}, {once:true});});
|
||||||
signal.throwIfAborted();subject.value='标题';return {attachmentOrder:[]};
|
signal.throwIfAborted();subject.value='标题';return {attachmentOrder:[]};
|
||||||
},verifyEditorImages:async()=>{}},DWW11Formatter:{format(){}}};
|
},verifyEditorImages:async()=>{throw new Error("自动流程不应检查图片");}},DWW11Formatter:{format(source,options){assert.equal(options.verifyImages,false);}}};
|
||||||
vm.runInNewContext(fs.readFileSync(path.join(__dirname,'../browser/runner.js'),'utf8'),context);
|
vm.runInNewContext(fs.readFileSync(path.join(__dirname,'../browser/runner.js'),'utf8'),context);
|
||||||
return {calls,navigations,job,run,storage,doc,Event,tick:()=>tick(),skip:()=>doc.dispatchEvent(new Event('dww11:skip-article')),
|
return {calls,navigations,job,run,storage,doc,Event,tick:()=>tick(),skip:()=>doc.dispatchEvent(new Event('dww11:skip-article')),
|
||||||
finishImport:()=>finishImport(),finishHeartbeat:()=>finishHeartbeat(),rolledBack:()=>rolledBack,
|
finishImport:()=>finishImport(),finishHeartbeat:()=>finishHeartbeat(),rolledBack:()=>rolledBack,
|
||||||
|
|||||||
@@ -70,6 +70,16 @@ class ControllerTests(unittest.TestCase):
|
|||||||
self.controller.scan(self.controller.manager.root)
|
self.controller.scan(self.controller.manager.root)
|
||||||
self.assertEqual(len(self.controller.store.state()['jobs']), 1)
|
self.assertEqual(len(self.controller.store.state()['jobs']), 1)
|
||||||
|
|
||||||
|
def test_bridge_start_and_pause(self):
|
||||||
|
store = self.controller.store
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
self.controller.bridge('start', {})
|
||||||
|
store.configure(dict(fid=95, minimum=1, maximum=1, interval=0, history_checked=True))
|
||||||
|
for action, expected in [('start', True), ('pause', False), ('start', True)]:
|
||||||
|
code, body = self.request('/bridge/' + action, {}, token=self.controller.bridge_token)
|
||||||
|
self.assertEqual(code, 200)
|
||||||
|
self.assertEqual(json.loads(body)['running'], expected)
|
||||||
|
|
||||||
def test_bridge_skip_continues_queue(self):
|
def test_bridge_skip_continues_queue(self):
|
||||||
store = self.controller.store
|
store = self.controller.store
|
||||||
store.enqueue(ROOT / 'test/fixtures/article')
|
store.enqueue(ROOT / 'test/fixtures/article')
|
||||||
|
|||||||
+29
-1
@@ -22,7 +22,7 @@ class QueueTests(unittest.TestCase):
|
|||||||
self.md.write_text('# 测试新闻\n\n正文内容。\n\n来源:测试来源,如有侵权,请联系删除。\n')
|
self.md.write_text('# 测试新闻\n\n正文内容。\n\n来源:测试来源,如有侵权,请联系删除。\n')
|
||||||
self.store = Store(self.root / 'queue.sqlite3')
|
self.store = Store(self.root / 'queue.sqlite3')
|
||||||
self.store.configure(dict(fid=125, minimum=1, maximum=1, interval=0, history_checked=True))
|
self.store.configure(dict(fid=125, minimum=1, maximum=1, interval=0, history_checked=True))
|
||||||
self.owner = dict(owner='tab-a', account=dict(uid='123', name='测试账号'), extension={'idle': True}, url='https://www.dww11.com/')
|
self.owner = dict(owner='tab-a', account=dict(uid='123', name='测试账号'), extension={'ready': True, 'idle': True}, url='https://www.dww11.com/')
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
self.tmp.cleanup()
|
self.tmp.cleanup()
|
||||||
@@ -321,6 +321,34 @@ class QueueTests(unittest.TestCase):
|
|||||||
self.assertFalse(self.store.run['running'])
|
self.assertFalse(self.store.run['running'])
|
||||||
self.assertEqual(self.store.run['switch']['state'], 'clicked')
|
self.assertEqual(self.store.run['switch']['state'], 'clicked')
|
||||||
|
|
||||||
|
def pending_switch(self):
|
||||||
|
import time
|
||||||
|
job = self.begin(); self.submit(job); self.accept(job)
|
||||||
|
other = self.root / 'other'; shutil.copytree(self.article, other)
|
||||||
|
(other / 'a.md').write_text(self.md.read_text().replace('正文内容', '另一篇内容'))
|
||||||
|
self.store.enqueue(other)
|
||||||
|
self.assertEqual(self.store.next(self.owner)['action'], 'switch')
|
||||||
|
self.store.run['switch']['created_at'] = time.time() - 3600
|
||||||
|
|
||||||
|
def test_old_pending_switch_does_not_timeout_before_click(self):
|
||||||
|
self.pending_switch()
|
||||||
|
self.store.control('pause'); self.store.control('start')
|
||||||
|
self.assertEqual(self.store.next(self.owner)['action'], 'switch')
|
||||||
|
self.assertTrue(self.store.run['running'])
|
||||||
|
self.assertNotIn('clicked_at', self.store.run['switch'])
|
||||||
|
|
||||||
|
def test_missing_bridge_explains_problem_and_preserves_pending_switch(self):
|
||||||
|
self.pending_switch()
|
||||||
|
changed = {**self.owner, 'account': {'uid': '456', 'name': '手动换号后的账号'}, 'extension': {}}
|
||||||
|
self.assertEqual(self.store.next(changed)['action'], 'idle')
|
||||||
|
self.assertIn('联动接口未就绪', self.store.run['reason'])
|
||||||
|
self.assertEqual(self.store.run['switch']['state'], 'pending')
|
||||||
|
self.assertEqual(self.store.run['batch']['count'], 1)
|
||||||
|
self.store.control('start')
|
||||||
|
self.assertEqual(self.store.next({**changed, 'extension': {'ready': True, 'idle': True}})['action'], 'prepare')
|
||||||
|
self.assertIsNone(self.store.run['switch'])
|
||||||
|
self.assertEqual(self.store.run['batch']['count'], 0)
|
||||||
|
|
||||||
def test_rejected_submission_requires_manual_resolution(self):
|
def test_rejected_submission_requires_manual_resolution(self):
|
||||||
job = self.begin(); self.submit(job)
|
job = self.begin(); self.submit(job)
|
||||||
self.transition(job, 'rejected', error='标题超限')
|
self.transition(job, 'rejected', error='标题超限')
|
||||||
|
|||||||
Reference in new issue
Block a user