修复新闻工作台换号联动流程
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
15 files changed
+201
-32
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。
|
||||
|
||||
## 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,
|
||||
'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 in ('start', 'pause', 'stop'):
|
||||
if route == 'stop':
|
||||
self.manager.stop.set()
|
||||
return self.store.control('stop')
|
||||
return self.store.control(route)
|
||||
if route not in methods:
|
||||
raise ValueError('未知浏览器操作')
|
||||
return methods[route](body)
|
||||
|
||||
+11
-10
@@ -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});
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
@@ -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/*
|
||||
|
||||
@@ -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
@@ -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,/已暂停/);
|
||||
|
||||
});
|
||||
@@ -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}),/网站附件图片无法显示/);
|
||||
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'])]]));
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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='标题超限')
|
||||
|
||||
Reference in new issue
Block a user