备份现场结果补录、图片修复与跳过文章功能

This commit is contained in:
Longtail committed 2026-10-31 17:08:49 +08:00
1 parent 1e6504b4a2
commit d157c179c8
20 files changed
+597 -47

No files matched your search

+1
View File
@@ -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`:导入器回归与自动模式的停止/冲突保护。
+23
View File
@@ -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}'});
});
+4 -2
View File
@@ -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
View File
@@ -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[![宣传](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'])]]));
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);
});
+106
View File
@@ -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();}
});
+13
View File
@@ -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()
+57
View File
@@ -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
View File
@@ -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 个正文附件引用,不残留 `![`。
- 排版前后对比现同时包含图片的 `id`、`aid` 和 `src`。
- 本轮导入与连接测试 16/16 通过,覆盖 aid 属性、图片错误响应、异步加载/终止、多行嵌套图文、重复图片位置;构建脚本已重新生成 0.1.1 合并油猴文件,语法检查通过。
需要在油猴更新 `browser/auto-news.user.js` 后才会应用代码修复。此更新不会解决尚未查明的图片接口 HTTP 500;未宣称现场图片显示已恢复。
## 2026-10-31:跳过当前文章
- 悬浮窗新增“跳过本文章”,沿用连接桥和持久队列,记录 `skipped` 状态、不计发表数、不自动重新入队。
- 执行器取消并等待当前导入退出后登记跳过,打开新的发帖页继续;提交中、结果不明以及有提交历史的异常任务拒绝直接跳过。
- Python 离线测试 33/33 通过,包括浏览器接口、跳过后继续、队列耗尽、重复/跨标签/过期请求、提交状态保护与重启持久化。
- Node.js 20 脚本测试 22/22 通过,包括悬浮窗事件、导入取消回滚等待、旧轮询竞态及请求失败暂停。两个油猴脚本语法检查通过,已生成 0.1.2 合并脚本。
- 本轮未操作真实论坛或账号;未执行 Playwright 浏览器回归,页面脚本使用模拟文档和连接响应验证。
## 2026-10-31:真实提交提示与审核中详情页观察
本节为真实登录态下的现场观察,与前述离线测试分开。用户明确授权助手点击一次“发表帖子”以捕捉快速跳转;未改写待发表内容、重新上传或重复提交。本次不属于自动执行器全流程验收。
| 范围 | 观察结果 |
| --- | --- |
| 提交前 | 标题“湖人只有一个卢卡·开拓者的四大金刚”,体育休闲 `fid=90` |
| 提示页 | 明确显示“新主题需要审核,您的帖子通过审核后才能显示” |
| 跳转 | 提示页指向并自动进入 `https://www.dww11.com/thread/23699911.html`,本次未自动回首页 |
| 详情页 | 标题、作者及版块信息可核对,标题旁显示“(审核中)” |
| 主题与首帖 | 页面链接实际 `tid=236999`;首帖 PID `238791`,容器 `#postmessage_238791` |
| 受理结论 | 本次已明确受理 1 篇,已确认公开 0 篇;不能重发 |
| 保存资料 | `../local-records/publish-result-23699911.json` 与 `../local-records/publish-result-23699911.png` |
已保存详情页关键结构与审核状态截图;提示页文字和跳转链接已捕捉,跳转前的容器 HTML 未保存成功。文件名按访问地址命名,不能将 `23699911` 直接当作真实主题 ID。账号名称与 UID 仅保留在本地排除目录,正文档继续匿名化。
本次未验证公开后的访客可见性、失败/超限提示、结果自动识别、受理接口自动记账、重复计数保护或真实换号链路;未补写运行队列与去重库。已有现场资料不等于结果适配已经实现。详见[操作规范第 12 节](../发布流程操作记录与自动化规范.md#12-2026-10-31实际提交与审核中页面观察)。