diff --git a/README.md b/README.md
index 6be0d60..61079fe 100644
--- a/README.md
+++ b/README.md
@@ -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。
+## 0.2.2 悬浮窗控制与跳过图片检查
+
+自动流程不再等待编辑器图片加载,也不检查排版前后图片标识和顺序。原检查函数保留,独立手动导入仍沿用默认检查。图片上传与本地文件格式限制继续用于构建可导入的文章。人工发表后只认网站指定审核提示,持久记账后回首页,按已配置间隔继续下一篇或换号。
+
+论坛悬浮窗新增“开始 / 继续”和“暂停”,与工作台控制相同。开始前须在工作台保存处理规则;暂停会立即取消本标签可取消的操作,并通知本地服务与其他标签。更新两个油猴脚本:导入排版 0.2.2、本地连接 0.1.2;重启本地服务并重新配对。
+
## 标题自动截断
自动队列、手动目录和文章包导入均应用上述规则;已截断的标题不会重复添加省略号。已有队列在领取未开始的任务时更新标题,已导入、待审核或已提交任务保持现场标题。
diff --git a/app.py b/app.py
index fabfbf1..05fdeb7 100644
--- a/app.py
+++ b/app.py
@@ -105,9 +105,10 @@ class Controller:
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,
'takeover': self.store.takeover, 'current': self.store.current, 'skip': self.store.skip}
- if route == 'stop':
- self.manager.stop.set()
- return self.store.control('stop')
+ if route in ('start', 'pause', 'stop'):
+ if route == 'stop':
+ self.manager.stop.set()
+ return self.store.control(route)
if route not in methods:
raise ValueError('未知浏览器操作')
return methods[route](body)
diff --git a/browser/auto-news.user.js b/browser/auto-news.user.js
index 0246558..ed8254f 100644
--- a/browser/auto-news.user.js
+++ b/browser/auto-news.user.js
@@ -1,7 +1,7 @@
// ==UserScript==
// @name 新闻工作台 · 导入排版与流程
// @namespace local.dww11.auto-news
-// @version 0.2.1
+// @version 0.2.2
// @match https://www.dww11.com/*
// @match https://dww11.com/*
// @match https://house.dww11.com/*
@@ -439,7 +439,7 @@
if (switcher) switcher.checked = Number(win.wysiwyg) === 0;
}
async function importPrepared(options = {}) {
- const { signal, automatic = false } = options;
+ const { signal, automatic = false, verifyImages = true } = options;
signal?.throwIfAborted();
if (busy) throw new Error('正在导入,请勿重复点击。');
if (!article) throw new Error('请先选择文章目录或文章包。');
@@ -492,10 +492,10 @@
signal?.throwIfAborted();
setEditor(selected.title, bbcode);
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]);
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('标题填写校验失败。');
committed = true;
} catch (error) {
@@ -727,7 +727,7 @@ const cleanText = value => String(value || '').replace(/\s+/g, ' ').trim();
}
window.DWW11Formatter = Object.freeze({
- format(expectedSource) {
+ format(expectedSource, {verifyImages = true} = {}) {
if (Number(window.wysiwyg) !== 1) throw new Error('请切换到可视化编辑器后排版');
const body = document.querySelector('#e_iframe')?.contentDocument?.body;
if (!body) throw new Error('编辑器未就绪');
@@ -740,7 +740,7 @@ window.DWW11Formatter = Object.freeze({
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 (verifyImages && 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};
@@ -892,14 +892,15 @@ window.DWW11Formatter = Object.freeze({
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);
+ const result=await window.DWW11Importer.importPrepared({automatic:true,verifyImages:false,signal:aborter.signal});assertLive(epoch);
if(result?.cancelled)throw new Error('导入已取消');
await event('formatting');assertLive(epoch);
- window.DWW11Formatter.format(job.source);assertLive(epoch);
+ window.DWW11Formatter.format(job.source,{verifyImages:false});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);
+ // 按用户要求暂停图片检查;保留原调用以便恢复。
+ // 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});
diff --git a/browser/formatter.js b/browser/formatter.js
index 1a5e468..2690be8 100644
--- a/browser/formatter.js
+++ b/browser/formatter.js
@@ -172,7 +172,7 @@ const cleanText = value => String(value || '').replace(/\s+/g, ' ').trim();
}
window.DWW11Formatter = Object.freeze({
- format(expectedSource) {
+ format(expectedSource, {verifyImages = true} = {}) {
if (Number(window.wysiwyg) !== 1) throw new Error('请切换到可视化编辑器后排版');
const body = document.querySelector('#e_iframe')?.contentDocument?.body;
if (!body) throw new Error('编辑器未就绪');
@@ -185,7 +185,7 @@ window.DWW11Formatter = Object.freeze({
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 (verifyImages && 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};
diff --git a/browser/importer.js b/browser/importer.js
index 7582b9e..be34146 100644
--- a/browser/importer.js
+++ b/browser/importer.js
@@ -439,7 +439,7 @@
if (switcher) switcher.checked = Number(win.wysiwyg) === 0;
}
async function importPrepared(options = {}) {
- const { signal, automatic = false } = options;
+ const { signal, automatic = false, verifyImages = true } = options;
signal?.throwIfAborted();
if (busy) throw new Error('正在导入,请勿重复点击。');
if (!article) throw new Error('请先选择文章目录或文章包。');
@@ -492,10 +492,10 @@
signal?.throwIfAborted();
setEditor(selected.title, bbcode);
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]);
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('标题填写校验失败。');
committed = true;
} catch (error) {
diff --git a/browser/local-bridge.user.js b/browser/local-bridge.user.js
index 3c8113f..ed392d3 100644
--- a/browser/local-bridge.user.js
+++ b/browser/local-bridge.user.js
@@ -1,7 +1,7 @@
// ==UserScript==
// @name 新闻工作台 · 本地连接
// @namespace local.dww11.auto-news
-// @version 0.1.1
+// @version 0.1.2
// @match https://www.dww11.com/*
// @match https://dww11.com/*
// @match https://house.dww11.com/*
@@ -15,7 +15,7 @@
// ==/UserScript==
(() => {
'use strict';
- const allowed = new Set(['heartbeat','next','package','event','accept','switch-begin','takeover','stop','current','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 stopRevision = 0;
const signalStop = () => { stopRevision++; document.dispatchEvent(new CustomEvent('dww11:auto-stop')); };
@@ -35,13 +35,14 @@
try {message=JSON.parse(event.detail);} catch {return;}
if (!allowed.has(message.route) || typeof message.id !== 'string') return;
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;
let value=await request(message.route,message.body || {});
if (message.route==='heartbeat') {
if (revision !== stopRevision) value = {...value, running:false, reason:'终止后忽略旧连接响应'};
GM_setValue('halted',!value.running);
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})}));
} catch(error) {
@@ -56,6 +57,32 @@
const shadow=host.attachShadow({mode:'closed'});
shadow.innerHTML='';
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='跳过本文章';
$('halt').before(skip);
skip.onclick=()=>document.dispatchEvent(new CustomEvent('dww11:skip-article'));
diff --git a/browser/runner.js b/browser/runner.js
index f4238e8..b4d2b37 100644
--- a/browser/runner.js
+++ b/browser/runner.js
@@ -80,14 +80,15 @@
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);
+ const result=await window.DWW11Importer.importPrepared({automatic:true,verifyImages:false,signal:aborter.signal});assertLive(epoch);
if(result?.cancelled)throw new Error('导入已取消');
await event('formatting');assertLive(epoch);
- window.DWW11Formatter.format(job.source);assertLive(epoch);
+ window.DWW11Formatter.format(job.source,{verifyImages:false});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);
+ // 按用户要求暂停图片检查;保留原调用以便恢复。
+ // 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});
diff --git a/build_browser.py b/build_browser.py
index 18e3251..dfdc299 100644
--- a/build_browser.py
+++ b/build_browser.py
@@ -4,7 +4,7 @@ ROOT = Path(__file__).resolve().parent
header = '''// ==UserScript==
// @name 新闻工作台 · 导入排版与流程
// @namespace local.dww11.auto-news
-// @version 0.2.1
+// @version 0.2.2
// @match https://www.dww11.com/*
// @match https://dww11.com/*
// @match https://house.dww11.com/*
diff --git a/store.py b/store.py
index b1d2261..c3a86d0 100644
--- a/store.py
+++ b/store.py
@@ -165,13 +165,19 @@ class Store:
self.run['owner'] = owner
if 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._persist(db)
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'])
- 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._persist(db)
return dict(action='idle', run=self.run)
diff --git a/test/bridge.test.cjs b/test/bridge.test.cjs
index 37e62a6..73f80c4 100644
--- a/test/bridge.test.cjs
+++ b/test/bridge.test.cjs
@@ -12,7 +12,7 @@ test('终止后较早的 heartbeat 响应不能恢复流程',async()=>{
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 element=()=>({style:{},before(child){elements.set(child.id,child);}});
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'}})}));
assert.equal(requests[0].url,'http://127.0.0.1:8770/bridge/skip');
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,/已暂停/);
+
});
diff --git a/test/extension-bridge.test.cjs b/test/extension-bridge.test.cjs
new file mode 100644
index 0000000..325e35b
--- /dev/null
+++ b/test/extension-bridge.test.cjs
@@ -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);
+});
diff --git a/test/importer.test.cjs b/test/importer.test.cjs
index 2ddcdcf..d668b75 100644
--- a/test/importer.test.cjs
+++ b/test/importer.test.cjs
@@ -182,6 +182,11 @@ test('真实站点 aid 属性:上传成功但图片响应失败不得报告完
await assert.rejects(h.api.importPrepared({automatic:true}),/网站附件图片无法显示/);
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('链接内图片、多行图文链接均参与上传并按原位置渲染', () => {
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'])]]));
diff --git a/test/runner.test.cjs b/test/runner.test.cjs
index ff03b10..4edb8a9 100644
--- a/test/runner.test.cjs
+++ b/test/runner.test.cjs
@@ -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)},
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});});
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);
return {calls,navigations,job,run,storage,doc,Event,tick:()=>tick(),skip:()=>doc.dispatchEvent(new Event('dww11:skip-article')),
finishImport:()=>finishImport(),finishHeartbeat:()=>finishHeartbeat(),rolledBack:()=>rolledBack,
diff --git a/test/test_controller.py b/test/test_controller.py
index 851a550..26cd334 100644
--- a/test/test_controller.py
+++ b/test/test_controller.py
@@ -70,6 +70,16 @@ class ControllerTests(unittest.TestCase):
self.controller.scan(self.controller.manager.root)
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):
store = self.controller.store
store.enqueue(ROOT / 'test/fixtures/article')
diff --git a/test/test_queue.py b/test/test_queue.py
index 5cfad54..db3716d 100644
--- a/test/test_queue.py
+++ b/test/test_queue.py
@@ -22,7 +22,7 @@ class QueueTests(unittest.TestCase):
self.md.write_text('# 测试新闻\n\n正文内容。\n\n来源:测试来源,如有侵权,请联系删除。\n')
self.store = Store(self.root / 'queue.sqlite3')
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):
self.tmp.cleanup()
@@ -321,6 +321,34 @@ class QueueTests(unittest.TestCase):
self.assertFalse(self.store.run['running'])
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):
job = self.begin(); self.submit(job)
self.transition(job, 'rejected', error='标题超限')