备份现场结果补录、图片修复与跳过文章功能
This commit is contained in:
1 parent
1e6504b4a2
commit
d157c179c8
20 files changed
+597
-47
No files matched your search
@@ -3,6 +3,7 @@
|
||||
- `history/`:从 auto_news_upload 根目录移入的历史截图和演示任务 JSON,保留原内容。不是自动测试,不能用作本次测试结论。
|
||||
- `fixtures/article/`:自造 Markdown 和三张 1×1 PNG,其中两张内容相同,用于验证上传去重及插图顺序。
|
||||
- `test_queue.py`:持久队列、去重、重启、终止、未知提交和按篇数换号。
|
||||
- `runner.test.cjs`:跳过后打开新编辑器、导入取消回滚等待、旧轮询响应、已提交保护及失败暂停。
|
||||
- `test_controller.py`:本地接口权限、模拟下载到队列、静态页面路由。
|
||||
- `test_download_core.py`:复制的下载器原有离线测试,针对合并项目中的下载器副本运行。
|
||||
- `importer.test.cjs`:导入器回归与自动模式的停止/冲突保护。
|
||||
|
||||
@@ -11,3 +11,26 @@ test('终止后较早的 heartbeat 响应不能恢复流程',async()=>{
|
||||
assert.equal(responses.find(r=>r.id==='old').value.running,false);assert.equal(values.halted,true);
|
||||
requests[1].onload({status:200,responseText:JSON.stringify({running:false,epoch:2})});await stop;
|
||||
});
|
||||
|
||||
test('悬浮窗跳过按钮发送页面事件并防止忙碌时重复点击',()=>{
|
||||
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());
|
||||
const doc={readyState:'complete',body:{append(){}},getElementById:()=>null,
|
||||
createElement:()=>({...element(),attachShadow:()=>({set innerHTML(value){},getElementById:id=>elements.get(id)})}),
|
||||
addEventListener(type,fn){if(!handlers.has(type))handlers.set(type,[]);handlers.get(type).push(fn);},
|
||||
dispatchEvent(event){emitted.push(event);for(const fn of handlers.get(event.type)||[])fn(event);}
|
||||
};
|
||||
class Event{constructor(type,options={}){this.type=type;this.detail=options.detail;}}
|
||||
vm.runInNewContext(fs.readFileSync(path.join(__dirname,'../browser/local-bridge.user.js'),'utf8'),{
|
||||
document:doc,CustomEvent:Event,GM_getValue:(k,d)=>k==='pairToken'?'test-token':k==='halted'?false:d,
|
||||
GM_setValue:()=>{},GM_addValueChangeListener:()=>{},GM_xmlhttpRequest:r=>requests.push(r)
|
||||
});
|
||||
const skip=elements.get('skip');assert.equal(skip.textContent,'跳过本文章');skip.onclick();
|
||||
assert.equal(emitted.at(-1).type,'dww11:skip-article');
|
||||
doc.dispatchEvent(new Event('dww11:skip-busy',{detail:true}));assert.equal(skip.disabled,true);
|
||||
doc.dispatchEvent(new Event('dww11:skip-busy',{detail:false}));assert.equal(skip.disabled,false);
|
||||
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}'});
|
||||
});
|
||||
@@ -32,8 +32,8 @@ const output=path.join(__dirname,'artifacts');fs.mkdirSync(output,{recursive:tru
|
||||
document.querySelector('#postsubmit').addEventListener('click',()=>window.clicks++);
|
||||
window.DWW11Importer={stagePackage:async()=>({warnings:[]}),importPrepared:async()=>{
|
||||
document.querySelector('#subject').value='测试标题';
|
||||
document.querySelector('#e_iframe').contentDocument.body.innerHTML='<p>正文第一段</p><img id="attachimg_1" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"><p>正文第二段</p><p>来源:测试来源,如有侵权,请联系删除。</p>';
|
||||
return {images:{'a.gif':'1'}};
|
||||
document.querySelector('#e_iframe').contentDocument.body.innerHTML='<p>正文第一段</p><img aid="attachimg_1" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"><p>正文第二段</p><p>来源:测试来源,如有侵权,请联系删除。</p>';
|
||||
return {images:{'a.gif':'1'},attachmentOrder:['1']};
|
||||
}};
|
||||
document.addEventListener('dww11:local-request',event=>{
|
||||
const {id,route,body}=JSON.parse(event.detail);let value={};
|
||||
@@ -46,6 +46,8 @@ const output=path.join(__dirname,'artifacts');fs.mkdirSync(output,{recursive:tru
|
||||
document.dispatchEvent(new CustomEvent('dww11:local-response',{detail:JSON.stringify({id,value})}));
|
||||
});
|
||||
});
|
||||
const importerCore=require('../browser/importer.js');
|
||||
await mock.addScriptTag({content:'(()=>{'+importerCore.attachmentId.toString()+';'+importerCore.verifyEditorImages.toString()+';Object.assign(window.DWW11Importer,{attachmentId,verifyEditorImages});})();'});
|
||||
await mock.addScriptTag({path:path.join(ROOT,'browser/formatter.js')});
|
||||
await mock.addScriptTag({path:path.join(ROOT,'browser/runner.js')});
|
||||
await mock.waitForFunction(()=>window.job.state==='awaiting_user_review');
|
||||
|
||||
+41
-2
@@ -73,7 +73,7 @@ test('服务器返回错误、登录 HTML 或混入脚本时不猜测附件编
|
||||
});
|
||||
|
||||
// Small DOM harness exercises the public API and site adapter without installing browser software.
|
||||
function harness({ failUpload = false, loseImages = false, existing = false } = {}) {
|
||||
function harness({ failUpload = false, loseImages = false, brokenImages = false, existing = false } = {}) {
|
||||
const ids = new Map(), callbacks = new Map(), requests = [];
|
||||
class Element {
|
||||
constructor(id='') { this.id=id; this.style={}; this.value=''; this.disabled=false; this.checked=false; this.hidden=false; this.listeners=new Map(); this.children=[]; }
|
||||
@@ -87,7 +87,14 @@ function harness({ failUpload = false, loseImages = false, existing = false } =
|
||||
removeEventListener(name) { this.listeners.delete(name); }
|
||||
dispatchEvent() {}
|
||||
click() { this.onclick?.(); }
|
||||
querySelectorAll() { return []; }
|
||||
querySelectorAll(selector) {
|
||||
if (selector !== 'img') return [];
|
||||
return [...this.innerHTML.matchAll(/<img aid="([^"]+)"[^>]*>/g)].map(m => ({
|
||||
id:'', complete:true, naturalWidth:brokenImages ? 0 : 100,
|
||||
getAttribute:name=>name==='aid'?m[1]:null,
|
||||
addEventListener(){},removeEventListener(){}
|
||||
}));
|
||||
}
|
||||
}
|
||||
for (const id of ['subject', 'e_textarea', 'e_switchercheck', 'postform']) ids.set(id, new Element(id));
|
||||
ids.get('subject').value=existing ? '原标题' : '';
|
||||
@@ -170,3 +177,35 @@ test('自动导入:上传中终止不会填写正文或继续下一张', async
|
||||
await assert.rejects(h.api.importPrepared({automatic:true,signal:c.signal}),/stopped/);
|
||||
assert.equal(h.requests.length,1);assert.equal(h.ids.get('subject').value,'');
|
||||
});
|
||||
|
||||
test('真实站点 aid 属性:上传成功但图片响应失败不得报告完成', async () => {
|
||||
const h=harness({brokenImages:true});await h.api.stageFiles(articleFiles());
|
||||
await assert.rejects(h.api.importPrepared({automatic:true}),/网站附件图片无法显示/);
|
||||
assert.equal(h.ids.get('subject').value,'');assert.equal(h.api.getState().result,null);
|
||||
});
|
||||
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'])]]));
|
||||
assert.deepEqual([...article.images.keys()],['image004.jpg','image001.jpg']);
|
||||
const rendered=article.render(new Map([['image004.jpg','104'],['image001.jpg','101']]));
|
||||
assert.match(rendered,/\[url=https:\/\/example.com\/promo\]\[attachimg\]104\[\/attachimg\]/);
|
||||
assert.deepEqual([...rendered.matchAll(/\[attachimg\](\d+)\[\/attachimg\]/g)].map(x=>x[1]),['104','101']);
|
||||
assert.equal(rendered.includes('!['),false);
|
||||
});
|
||||
test('图片重复引用必须保留每个位置,不能只校验去重后的编号', async () => {
|
||||
const img={id:'',getAttribute:n=>n==='aid'?'attachimg_12':null,complete:true,naturalWidth:100,addEventListener(){},removeEventListener(){}};
|
||||
await core.verifyEditorImages({querySelectorAll:()=>[img,img]},['12','12']);
|
||||
await assert.rejects(core.verifyEditorImages({querySelectorAll:()=>[img]},['12','12']),/数量或顺序/);
|
||||
});
|
||||
|
||||
test('正文图片尚在加载时等待 load;终止后释放等待', async () => {
|
||||
const make=()=>{const events=new Map();return {events,id:'',complete:false,naturalWidth:0,getAttribute:n=>n==='aid'?'attachimg_8':null,addEventListener:(n,f)=>events.set(n,f),removeEventListener:n=>events.delete(n)};};
|
||||
const image=make();let finished=false;
|
||||
const loaded=core.verifyEditorImages({querySelectorAll:()=>[image]},['8']).then(()=>finished=true);
|
||||
await Promise.resolve();assert.equal(finished,false);
|
||||
image.complete=true;image.naturalWidth=100;image.events.get('load')();await loaded;
|
||||
assert.equal(image.events.size,0);
|
||||
const pending=make(),controller=new AbortController();
|
||||
const cancelled=core.verifyEditorImages({querySelectorAll:()=>[pending]},['8'],controller.signal);
|
||||
controller.abort();await assert.rejects(cancelled,/终止/);assert.equal(pending.events.size,0);
|
||||
});
|
||||
@@ -0,0 +1,106 @@
|
||||
'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({holdImport=false,holdHeartbeat=false,rejectSkip=false}={}) {
|
||||
const handlers=new Map(),calls=[],navigations=[],storage=new Map(),timers=new Set();
|
||||
const job={id:'first',state:'preparing',title:'标题',source:'来源',fid:125,account:{uid:'123',name:'账号'}};
|
||||
const run={running:true,epoch:1,active:job.id,owner:'tab-a'};
|
||||
const body={innerHTML:'正文',innerText:'正文',querySelectorAll:()=>[]};
|
||||
const subject={value:''};let tick,finishImport,finishHeartbeat,rolledBack=false;
|
||||
class Event {constructor(type,options={}){this.type=type;Object.assign(this,options);}}
|
||||
const doc={
|
||||
body:{append(){}},createElement:()=>({style:{}}),
|
||||
querySelector(selector){
|
||||
if(selector==='#mumucms_username')return {childNodes:[{nodeType:3,textContent:'账号'}],href:'https://www.dww11.com/home.php?uid=123',querySelectorAll:()=>[]};
|
||||
if(selector==='#e_iframe')return {contentDocument:{body}};
|
||||
if(selector==='#subject')return subject;
|
||||
return null;
|
||||
},
|
||||
addEventListener(type,fn){if(!handlers.has(type))handlers.set(type,[]);handlers.get(type).push(fn);},
|
||||
dispatchEvent(event){for(const fn of handlers.get(event.type)||[])fn(event);}
|
||||
};
|
||||
const reply=(id,value,error)=>doc.dispatchEvent(new Event('dww11:local-response',{detail:JSON.stringify({id,value,error})}));
|
||||
doc.addEventListener('dww11:local-request',event=>{
|
||||
const {id,route,body}=JSON.parse(event.detail);calls.push({route,body});
|
||||
if(route==='heartbeat'&&holdHeartbeat&&calls.filter(x=>x.route==='heartbeat').length>1) {
|
||||
finishHeartbeat=()=>reply(id,run);return;
|
||||
}
|
||||
let value={};
|
||||
if(route==='heartbeat')value=run;
|
||||
if(route==='next')value={action:job.state==='preparing'?'prepare':'resume',job,run};
|
||||
if(route==='event')job.state=body.state;
|
||||
if(route==='skip') {
|
||||
if(rejectSkip){reply(id,null,'状态已改变');return;}
|
||||
job.state='skipped';run.active=null;run.epoch++;value={ok:true,fid:125,run};
|
||||
}
|
||||
if(route==='stop')run.running=false;
|
||||
reply(id,value);
|
||||
});
|
||||
let sequence=0;
|
||||
const context={document:doc,CustomEvent:Event,Node:{TEXT_NODE:3},URL,AbortController,
|
||||
crypto:{randomUUID:()=>sequence++===0?'tab-a':`request-${sequence}`},sessionStorage:{getItem:k=>storage.get(k),setItem:(k,v)=>storage.set(k,v),removeItem:k=>storage.delete(k)},
|
||||
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={DWW11Importer:{stagePackage:async()=>({}),importPrepared:async({signal})=>{
|
||||
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(){}}};
|
||||
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,
|
||||
close:()=>{for(const t of timers)clearTimeout(t);}};
|
||||
}
|
||||
|
||||
test('跳过待审文章后打开全新编辑器,不再领取或提交旧稿',async()=>{
|
||||
const f=fixture();try{
|
||||
await flush();assert.equal(f.job.state,'awaiting_user_review');
|
||||
f.skip();f.skip();await flush();
|
||||
assert.equal(f.calls.filter(x=>x.route==='skip').length,1);
|
||||
assert.equal(f.job.state,'skipped');assert.equal(f.storage.has('auto-news-current'),false);
|
||||
assert.deepEqual(f.navigations,['https://www.dww11.com/forum.php?mod=post&action=newthread&fid=125']);
|
||||
const count=f.calls.length;await f.tick();assert.equal(f.calls.length,count);
|
||||
assert.equal(f.calls.some(x=>x.route==='accept'),false);
|
||||
}finally{f.close();}
|
||||
});
|
||||
|
||||
test('导入中跳过等待取消回滚,旧回调不会把任务改成异常',async()=>{
|
||||
const f=fixture({holdImport:true});try{
|
||||
await flush();assert.equal(f.job.state,'importing');f.skip();await flush();
|
||||
assert.equal(f.calls.some(x=>x.route==='skip'),false);assert.deepEqual(f.navigations,[]);
|
||||
f.finishImport();await flush();
|
||||
assert.equal(f.rolledBack(),true);assert.equal(f.job.state,'skipped');
|
||||
assert.equal(f.calls.some(x=>x.body.state==='needs_attention'),false);
|
||||
assert.equal(f.navigations.length,1);
|
||||
}finally{f.close();}
|
||||
});
|
||||
|
||||
test('跳过时忽略较早的轮询响应',async()=>{
|
||||
const f=fixture({holdHeartbeat:true});try{
|
||||
await flush();const pending=f.tick();await flush();f.skip();await flush();
|
||||
const before=f.calls.filter(x=>x.route==='next').length;
|
||||
f.finishHeartbeat();await pending;
|
||||
assert.equal(f.calls.filter(x=>x.route==='next').length,before);
|
||||
}finally{f.close();}
|
||||
});
|
||||
|
||||
test('人工发表后的任务不能直接跳过',async()=>{
|
||||
const f=fixture();try{
|
||||
await flush();f.doc.dispatchEvent(new f.Event('click',{isTrusted:true,target:{closest:()=>true}}));
|
||||
await flush();assert.equal(f.job.state,'user_submitting');f.skip();await flush();
|
||||
assert.equal(f.calls.some(x=>x.route==='skip'),false);assert.deepEqual(f.navigations,[]);
|
||||
}finally{f.close();}
|
||||
});
|
||||
|
||||
test('跳过请求失败时暂停且保留当前稿件',async()=>{
|
||||
const f=fixture({rejectSkip:true});try{
|
||||
await flush();f.skip();await flush();
|
||||
assert.equal(f.run.running,false);assert.equal(f.job.state,'awaiting_user_review');assert.deepEqual(f.navigations,[]);
|
||||
assert.equal(f.storage.has('auto-news-current'),true);
|
||||
}finally{f.close();}
|
||||
});
|
||||
@@ -70,6 +70,19 @@ class ControllerTests(unittest.TestCase):
|
||||
self.controller.scan(self.controller.manager.root)
|
||||
self.assertEqual(len(self.controller.store.state()['jobs']), 1)
|
||||
|
||||
def test_bridge_skip_continues_queue(self):
|
||||
store = self.controller.store
|
||||
store.enqueue(ROOT / 'test/fixtures/article')
|
||||
store.configure(dict(fid=125, minimum=1, maximum=1, interval=0, history_checked=True))
|
||||
store.control('start')
|
||||
job = store.next(dict(owner='tab-a', account=dict(uid='123', name='测试账号')))['job']
|
||||
code, body = self.request('/bridge/skip', dict(owner='tab-a', id=job['id'], epoch=store.run['epoch']),
|
||||
token=self.controller.bridge_token)
|
||||
self.assertEqual(code, 200)
|
||||
self.assertTrue(json.loads(body)['ok'])
|
||||
self.assertEqual(store.state()['jobs'][0]['state'], 'skipped')
|
||||
self.assertIsNone(store.run['active'])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -144,6 +144,63 @@ class QueueTests(unittest.TestCase):
|
||||
self.assertEqual(self.store.next(self.owner)['action'], 'idle')
|
||||
self.assertIsNone(self.store.run['switch'])
|
||||
|
||||
def test_skip_preserves_history_and_continues_next_without_counting(self):
|
||||
job = self.begin()
|
||||
other = self.root / 'other'
|
||||
shutil.copytree(self.article, other)
|
||||
(other / 'a.md').write_text(self.md.read_text().replace('正文内容', '下一篇内容'))
|
||||
self.store.enqueue(other)
|
||||
self.transition(job, 'importing')
|
||||
self.transition(job, 'formatting')
|
||||
self.transition(job, 'awaiting_user_review')
|
||||
result = self.store.skip(dict(owner='tab-a', id=job['id'], epoch=self.store.run['epoch']))
|
||||
self.assertEqual(result['fid'], 125)
|
||||
self.assertEqual(self.store.run['batch']['count'], 0)
|
||||
self.assertEqual(self.store.state()['history_count'], 0)
|
||||
self.assertEqual(next(j for j in self.store.state()['jobs'] if j['id'] == job['id'])['state'], 'skipped')
|
||||
self.assertIn('已在队列', self.store.enqueue(self.article))
|
||||
self.assertEqual(self.store.next(self.owner)['action'], 'prepare')
|
||||
self.assertNotEqual(self.store.run['active'], job['id'])
|
||||
restarted = Store(self.root / 'queue.sqlite3')
|
||||
self.assertEqual(next(j for j in restarted.state()['jobs'] if j['id'] == job['id'])['state'], 'skipped')
|
||||
|
||||
def test_skip_last_article_finishes_queue(self):
|
||||
job = self.begin()
|
||||
self.store.skip(dict(owner='tab-a', id=job['id'], epoch=self.store.run['epoch']))
|
||||
self.assertEqual(self.store.next(self.owner)['action'], 'idle')
|
||||
self.assertFalse(self.store.run['running'])
|
||||
self.assertIsNone(self.store.run['switch'])
|
||||
|
||||
def test_skip_rejects_other_tab_stale_request_and_duplicate(self):
|
||||
job = self.begin()
|
||||
data = dict(owner='tab-a', id=job['id'], epoch=self.store.run['epoch'])
|
||||
with self.assertRaisesRegex(ValueError, '另一个'):
|
||||
self.store.skip({**data, 'owner': 'tab-b'})
|
||||
self.store.control('stop')
|
||||
with self.assertRaisesRegex(ValueError, '状态已改变'):
|
||||
self.store.skip(data)
|
||||
data['epoch'] = self.store.run['epoch']
|
||||
self.store.skip(data)
|
||||
with self.assertRaises(ValueError):
|
||||
self.store.skip(data)
|
||||
|
||||
def test_skip_resumes_unsubmitted_error_but_rejects_submitted_errors(self):
|
||||
job = self.begin()
|
||||
self.transition(job, 'needs_attention', error='上传失败')
|
||||
self.store.skip(dict(owner='tab-a', id=job['id'], epoch=self.store.run['epoch']))
|
||||
self.assertTrue(self.store.run['running'])
|
||||
self.assertEqual(self.store.next(self.owner)['action'], 'idle')
|
||||
|
||||
def test_submitted_article_cannot_be_skipped(self):
|
||||
job = self.begin()
|
||||
self.submit(job)
|
||||
for state in ('user_submitting', 'submit_unknown', 'needs_attention'):
|
||||
if state != 'user_submitting':
|
||||
self.transition(job, state)
|
||||
with self.assertRaisesRegex(ValueError, '已提交'):
|
||||
self.store.skip(dict(owner='tab-a', id=job['id'], epoch=self.store.run['epoch']))
|
||||
self.assertEqual(self.store.run['active'], job['id'])
|
||||
|
||||
def test_bad_result_does_not_increment(self):
|
||||
job = self.begin();self.submit(job)
|
||||
with self.assertRaises(ValueError):
|
||||
|
||||
+41
-2
@@ -1,6 +1,6 @@
|
||||
# 本轮验证记录
|
||||
|
||||
日期:2026-10-31。测试均未使用真实账号,未向新闻 API 请求内容,未向论坛上传或发布。
|
||||
日期:2026-10-31。下表所列离线测试均未使用真实账号,未向新闻 API 请求内容,未向论坛上传或发布。后续真实现场操作另按小节记录。
|
||||
|
||||
| 范围 | 结果 |
|
||||
| --- | --- |
|
||||
@@ -19,4 +19,43 @@
|
||||
|
||||
测试中曾发现模拟网页未声明 UTF-8,导致中文登录入口无法匹配;已修正测试响应编码并重新通过。真实网页编码未由此假定。
|
||||
|
||||
尚未验证:用户浏览器中油猴本地通信权限、真实网站当前的导入与附件转换、真实换号账号链路、结果页受理/审核/公开状态。按用户决定,结果页 HTML 待提供,自动核对暂不实现;因此当前不宣称全流程批量处理已完成在线验收。
|
||||
上述离线验证结束时尚未验证:用户浏览器中油猴本地通信权限、真实网站当前的导入与附件转换、真实换号账号链路、结果页受理/审核/公开状态。当时结果页 HTML 待提供,自动核对暂不实现;后续现场观察见下文,仍不代表全流程批量处理已完成在线验收。
|
||||
|
||||
## 2026-10-31:实际图片故障排查与 0.1.1 修复
|
||||
|
||||
只读检查实际发帖页面,没有点击发表、重新上传、刷新或改写用户正文。
|
||||
|
||||
- 正文已有 3 个图片元素,实际标识为 `aid="attachimg_…"`;原 runner 错查 `id`,已修正为共享附件验证函数。
|
||||
- 这 3 张图及网站附件缩略图的 `naturalWidth` 均为 0;Chrome 开发者工具记录图片接口 HTTP 500。未获取具体错误响应或服务端日志,不能认定其底层原因。
|
||||
- 原导入器只验证 HTML 中包含附件编号,不能证明图片可显示。现在校验附件序列(含重复引用),并等待图片加载;加载失败会报错,且不提示完成或继续排版。
|
||||
- 原 Markdown 解析器不能处理包含嵌套图片的多行链接。本次实际文章 4 张本地 JPG 原先只识别 3 张;修复后离线编译识别 4 张,输出 4 个正文附件引用,不残留 `。
|
||||
Reference in new issue
Block a user