Compare commits

..
1 Commits
Author SHA1 Message Date
LongtailandCopilot fd9474d0e1 修复新闻工作台换号联动流程
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-01 15:11:41 +08:00
15 changed files with 203 additions and 34 deletions

No files matched your search

+15 -1
View File
@@ -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;重启本地服务并重新配对。
## 标题自动截断
自动队列、手动目录和文章包导入均应用上述规则;已截断的标题不会重复添加省略号。已有队列在领取未开始的任务时更新标题,已导入、待审核或已提交任务保持现场标题。
+4 -3
View File
@@ -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)
+11 -10
View File
@@ -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});
+2 -2
View File
@@ -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};
+3 -3
View File
@@ -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) {
+30 -3
View File
@@ -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='<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 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'));
+5 -4
View File
@@ -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});
+1 -1
View File
@@ -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/*
+9 -3
View File
@@ -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)
+20 -1
View File
@@ -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,/已暂停/);
});
+56
View File
@@ -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);
});
+5
View File
@@ -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[![宣传](image004.jpg)\n\n### 宣传标题\n\n说明](https://example.com/promo)\n\n![](image001.jpg)';
const article=core.compile(md,'a.md',new Map([['image004.jpg',new Blob(['x'])],['image001.jpg',new Blob(['y'])]]));
+3 -2
View File
@@ -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,
+10
View File
@@ -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')
+29 -1
View File
@@ -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='标题超限')