diff --git a/README.md b/README.md
index 3a0fe98..a01dfeb 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
将新闻下载、文章打包、浏览器导入、排版与换号联动整合到本地控制台。
-**当前版本:0.1。可完成下载 → 入队 → 导入 → 排版 → 等待人工发表。结果页自动核对按用户决定暂留待接入;人工提交后会记录候选结果并暂停,不会自动计数、换号或处理下一篇。** 换号调度和计数接口已完成离线验证,本次已采集真实审核中结果页的关键结构;结果观察适配与完整在线链路验证仍待完成。
+**当前版本:0.2.0。已接通下载 → 入队 → 导入 → 排版 → 人工发表 → 审核中详情核对 → 受理记账 → 回首页 → 下一篇或换号。** 结果页需同时核对真实主题 ID、首帖作者、版块、最终提交标题和标题区域的审核中标志。图文检查有差异则保留已受理去重、暂停后续处理,不自动编辑或重发。尚无公开成功页样本,不能凭审核中标志消失认定已公开;完整真实发布与换号链路仍需小批验收。
## 启动
@@ -20,7 +20,7 @@ uv run --no-project --python 3.12 --with-requirements requirements.txt python ap
1. 在油猴安装控制台“浏览器连接”区域的两个脚本:
- `browser/local-bridge.user.js`:本地连接、配对与常驻终止按钮。
- - `browser/auto-news.user.js`:导入、排版和任务执行。它由 `importer.js`、`formatter.js`、`runner.js` 构建。
+ - `browser/auto-news.user.js`:导入、排版和任务执行。它由 `importer.js`、`formatter.js`、`results.js`、`runner.js` 构建。
2. 停用旧的独立导入与排版油猴脚本,避免同一编辑器重复挂载。
3. 换号插件仍由用户安装,位于项目根目录的 `dww11-account-switch-browser-extension/`,没有复制到本目录。本次在其 `content.js` 中增加了不含凭据的状态属性及终止事件监听。请在浏览器扩展管理页重新加载**这个原安装目录**,再刷新网站。旧 ZIP 未重新打包,不包含本次修改。
4. 在控制台点击“显示本次连接码”。在论坛左下角“新闻工作台 · 本地连接”展开设置,填写端口和连接码并保存。
@@ -35,9 +35,10 @@ uv run --no-project --python 3.12 --with-requirements requirements.txt python ap
3. 填写目标版块 `fid`、每账号最少/最多篇数和处理间隔。先核对历史发帖记录,再勾选确认并保存。版块和篇数不会替用户选择。
4. 登录论坛,建议只保留一个执行标签。点击控制台“开始 / 继续”。执行器识别账号后领取一篇任务,进入指定新帖页、导入、排版,停止在人工审核阶段。
5. 在论坛检查账号、版块、标题、图片位置、正文和来源,并亲手点击“发表帖子”。程序不调用发帖提交函数,不发送发帖请求,也不自动保存草稿。
-6. 当前版本会记录人工点击和提交快照,在跳转后的候选页面或等待超时后标记“发布结果待核对”并暂停。**已采集审核中页面资料,仍需完成结果观察适配,才能自动核对受理结果并接续下一篇。**
-
-待补齐结果页自动核对后:审核中算网站已受理一次,但不算已确认公开;达到账号随机目标篇数且仍有待处理文章时,回首页调用插件换号,核对新 UID 与插件空闲状态后创建下一账号批次。没有待处理文章时直接结束,不为达到数量重发。
+6. 人工点击时保存最终提交快照。快速提示页仅作为候选证据,等待详情页核对;审核中计入受理一次,公开数量另记。真实主题 ID 来自页面链接中的 `tid`,不将 `/thread/…html` 的整串数字当作 TID;首帖 PID 单独保存。
+7. 图文无差异且流程仍在运行时,先持久记账,再回首页;在设定间隔后处理下一篇。达到账号目标且仍有待处理文章时调用换号插件,确认新 UID 和插件空闲后创建下一批次。队列为空直接结束。
+8. 等待结果超过 45 秒仍无足够证据时暂停;结果不明可在原执行标签打开正确详情页进行被动核对。图片加载最多观察 15 秒,附件区图片、占位图、正文文字或图片顺序差异都会暂停后续处理。
+9. 已有现场结果可在暂停后使用队列中的“补核已受理结果”导入观察 JSON,并人工确认文章关联。支持本项目 `local-records/publish-result-*.json` 结构,字段含 title、uid、fid、threadId、postId、url、status;补核须与已有人工提交快照匹配,不增加当前批次计数。历史补核不会自动检查发布后的图文完整性。
## 终止、恢复与冲突
@@ -79,6 +80,8 @@ downloads/ 本工具下载结果(本地)
```sh
uv run --no-project --python 3.12 --with-requirements requirements.txt python -m unittest discover -s test -p 'test_*.py' -v
node --test test/importer.test.cjs test/bridge.test.cjs test/runner.test.cjs
+# 另需 Playwright 与 Chrome,网络请求全部阻断
+node --test test/results-browser.test.cjs
python3 build_browser.py
```
@@ -99,7 +102,7 @@ node test/browser.test.cjs
注意:该地址中的数字串为 `23699911`,页面链接实际使用 `tid=236999`,首帖 PID 为 `238791`。适配时必须区分访问地址与主题标识,不能直接把路径中的全部数字当作 `tid`。
-下一步是据此完成 `runner.js` 的结果观察适配,核对受理接口的标识处理,再联通计数和换号。公开帖子、失败提示及审核后的访客可见性仍需补充验证。本次仅保存观察资料,未据此修改程序或补写队列计数;不需要 Cookie、密码或表单令牌。
+本次已完成审核中结果适配,并只读核对现有帖子的作者、版块和延迟加载图片结构。仍需公开成功页、更多明确失败提示及其他版块样本,未知结果保守暂停;当前明确拒绝分支只支持标题超限提示。不要提供 Cookie、密码或表单令牌。
## 0.1.1 图片修复
@@ -108,3 +111,9 @@ node test/browser.test.cjs
## 0.1.2 跳过文章
保持现有控制器、SQLite 队列、连接桥和执行器的结构,新增 `skip` 浏览器接口及 `skipped` 队列状态。更新油猴中的 `browser/local-bridge.user.js`(0.1.1)和 `browser/auto-news.user.js`(0.1.2),重启本地工作台、重新配对并刷新论坛后使用。按钮位于网站左下角悬浮窗、“终止流程”上方。
+
+## 0.2.0 结果核对与接续
+
+更新油猴中的 `browser/auto-news.user.js` 至 0.2.0,连接脚本沿用现有 0.1.1。更新本地代码后重启服务、重新配对并刷新论坛;不会自动继续任务。新增真实 TID 与 PID 分离、链接别名防重、被动结果恢复、图文差异暂停、历史结果补核,以及记账后回首页的持久步骤。旧受理记录如只有路径数字会保留原值并标记主题身份待核对,不猜测删除 URL 后缀。
+
+换号过程中插件暂停、超过 120 秒未确认、或执行后超过 10 秒插件空闲但 UID 未变化,流程会暂停;请在插件内核对并恢复,不自动重复抽号。图片 HTTP 500 的服务端根因仍未解决。
diff --git a/app.py b/app.py
index 88e39f0..82b073e 100644
--- a/app.py
+++ b/app.py
@@ -87,6 +87,8 @@ class Controller:
if route == 'stop':
self.manager.stop.set()
return self.store.control(route)
+ if route == 'reconcile':
+ return self.store.reconcile(body)
if route == 'resolve':
return self.store.resolve(body)
if route == 'history':
diff --git a/browser/auto-news.user.js b/browser/auto-news.user.js
index 5bb18e7..d22b3c5 100644
--- a/browser/auto-news.user.js
+++ b/browser/auto-news.user.js
@@ -1,7 +1,7 @@
// ==UserScript==
// @name 新闻工作台 · 导入排版与流程
// @namespace local.dww11.auto-news
-// @version 0.1.2
+// @version 0.2.0
// @match https://www.dww11.com/*
// @match https://dww11.com/*
// @match https://house.dww11.com/*
@@ -730,6 +730,56 @@ window.DWW11Formatter = Object.freeze({
});
})();
+/* Read-only result adapter. Login state and URLs alone are never acceptance evidence. */
+(function(root,factory){
+ const api=factory();
+ if(typeof module==='object'&&module.exports)module.exports=api;
+ else root.DWW11Results=api;
+})(typeof window==='undefined'?null:window,()=>{
+ 'use strict';
+ const clean=value=>String(value||'').replace(/\s+/g,' ').trim();
+ function parameter(href,key,base){try{return new URL(href,base).searchParams.get(key);}catch{return null;}}
+ function aid(image){return String(image.getAttribute('aid')||image.id||'').match(/(?:attachimg_|aimg_)?(\d+)$/)?.[1]||null;}
+ function inspect(doc,url,snapshot={}) {
+ const titleNode=doc.querySelector('#thread_subject');
+ if(!titleNode) {
+ const prompt=doc.querySelector('#messagetext, .messagetext');
+ const text=clean(prompt?.textContent);
+ if(/^(?:抱歉[,,]\s*)?(?:您填写的)?(?:帖子)?标题.{0,40}(?:超过|超出).{0,40}(?:字符|长度|限制)/.test(text))return {kind:'rejected',error:text};
+ return {kind:'waiting',prompt:text,candidate:prompt?.querySelector('a[href]')?.href||''};
+ }
+ const region=titleNode.closest('td')||titleNode.closest('h1')?.parentElement;
+ if(!region)return {kind:'incomplete',error:'缺少标题状态区域'};
+ const ids=new Set([...region.querySelectorAll('a[href]')].filter(a=>parameter(a.href,'mod',url)==='viewthread').map(a=>parameter(a.href,'tid',url)).filter(x=>/^\d+$/.test(x)));
+ if(ids.size!==1)return {kind:'incomplete',error:'详情页真实主题编号缺失或冲突'};
+ const thread_id=[...ids][0],body=doc.querySelector('[id^="postmessage_"]');
+ const pid=body?.id.match(/^postmessage_(\d+)$/)?.[1];
+ const post=pid&&doc.querySelector('#post_'+pid);
+ if(!post||clean(post.querySelector('#postnum'+pid)?.textContent)!=='楼主')return {kind:'incomplete',error:'无法确认当前正文属于首帖'};
+ const authors=post?[...post.querySelectorAll('.plc .pti .authi a[href], .pls .authi a[href]')]:[];
+ const uids=new Set(authors.map(a=>parameter(a.href,'uid',url)).filter(x=>/^\d+$/.test(x)));
+ const fids=new Set([...doc.querySelectorAll('#pt a[href], .mumucms_pt a[href]')].filter(a=>parameter(a.href,'mod',url)==='forumdisplay').map(a=>parameter(a.href,'fid',url)).filter(x=>/^\d+$/.test(x)));
+ if(!body||!pid||uids.size!==1||fids.size!==1)return {kind:'incomplete',error:'首帖作者、正文或版块证据不足'};
+ const moderation=[...region.querySelectorAll('span.xg1')].some(n=>/\(\s*审核中\s*\)|(\s*审核中\s*)/.test(n.textContent));
+ // A public-page sample is not available yet. Absence of moderation is not proof of publication.
+ if(!moderation)return {kind:'incomplete',error:'尚无可验证的公开或审核中状态'};
+ const allImages=[...body.querySelectorAll('img')];
+ const images=allImages.filter(img=>aid(img)).map(img=>({aid:aid(img),src:img.getAttribute('src'),loaded:img.complete&&img.naturalWidth>0&&!/\/none\.gif(?:[?#]|$)/.test(img.src||img.getAttribute('src')||''),file:img.getAttribute('file')||img.getAttribute('zoomfile')}));
+ const expected=(snapshot.images||[]).map(img=>String(img.aid||img.id||'').match(/(?:attachimg_|aimg_)?(\d+)$/)?.[1]).filter(Boolean);
+ const issues=[];
+ if(allImages.some(img=>!aid(img)))issues.push('正文存在无法关联附件的图片');
+ if(expected.length!==(snapshot.images||[]).length)issues.push('提交快照存在无法识别附件编号的图片');
+ if(JSON.stringify(expected)!==JSON.stringify(images.map(img=>img.aid)))issues.push('首帖正文图片引用或顺序与提交快照不一致');
+ if(images.some(img=>!img.loaded))issues.push('结果页正文图片尚未加载或加载失败');
+ if(snapshot.text&&clean(body.innerText||body.textContent)!==clean(snapshot.text))issues.push('结果正文与最终提交快照存在文字差异');
+ const attachments=post?[...post.querySelectorAll('.pattl img, .attm img')].map(img=>({aid:aid(img),src:img.getAttribute('src')})):[];
+ if(attachments.length)issues.push('结果页存在附件区图片,请核对正文位置');
+ return {kind:'accepted',thread_id,first_post_pid:pid,post_url:url,title:clean(titleNode.textContent),author_uid:[...uids][0],fid:Number([...fids][0]),state:'accepted_pending_moderation',
+ evidence:{adapter:1,thread_id,first_post_pid:pid,title:clean(titleNode.textContent),author_uid:[...uids][0],fid:Number([...fids][0]),moderation:true,images,attachments,integrity_issues:issues}};
+ }
+ return {inspect,clean};
+});
+
/* Page-side orchestrator. This file never clicks or submits the publication form. */
(() => {
'use strict';
@@ -739,7 +789,7 @@ window.DWW11Formatter = Object.freeze({
const owner=sessionStorage.getItem('auto-news-owner') || crypto.randomUUID();
sessionStorage.setItem('auto-news-owner',owner);
let run=null, working=false, halted=true, aborter=null, active=null, tickBusy=false;
- let skipBusy=false, preparation=null;
+ let skipBusy=false, preparation=null, stopRevision=0, resultSeenAt=0;
const pending=new Map();
function rpc(route,body={}) {
return new Promise((resolve,reject)=>{
@@ -756,7 +806,7 @@ window.DWW11Formatter = Object.freeze({
data.error?waiter.reject(new Error(data.error)):waiter.resolve(data.value);
});
const status=message=>document.dispatchEvent(new CustomEvent('dww11:connection-status',{detail:message}));
- document.addEventListener('dww11:auto-stop',()=>{halted=true;aborter?.abort(new Error('流程已终止'));});
+ document.addEventListener('dww11:auto-stop',()=>{stopRevision++;halted=true;aborter?.abort(new Error('流程已终止'));});
function account() {
const element=document.querySelector('#mumucms_username');
if(!element)return null;
@@ -874,16 +924,41 @@ window.DWW11Formatter = Object.freeze({
const local=loadLocal();
if(local?.id===job.id&&local.snapshot&&job.state==='awaiting_user_review') {
await rpc('event',{id:job.id,state:'user_submitting',epoch:run.epoch,snapshot:local.snapshot});
- job.state='user_submitting';
+ job.state='user_submitting';job.snapshot=local.snapshot;
}
- if(job.state==='user_submitting') {
- // Result selectors intentionally await user-provided HTML; URL alone is not proof.
- if(!editorPage(job) || (local?.clickedAt && Date.now()-local.clickedAt>45000)) {
- await rpc('event',{id:job.id,state:'submit_unknown',error:'等待结果页 HTML 适配;候选页面:'+location.href});
- await rpc('stop');
- status('已记录候选结果页;尚不能自动核对。流程已暂停,请补充结果页 HTML。');
+ if(!['user_submitting','submit_unknown'].includes(job.state))return;
+ const snapshot=job.snapshot||local?.snapshot||{};
+ const result=window.DWW11Results?.inspect(document,location.href,snapshot);
+ if(result?.kind==='rejected'&&job.state==='user_submitting') {
+ await rpc('event',{id:job.id,state:'rejected',error:result.error});
+ status('网站明确拒绝本次提交,已暂停;核对并处理稿件后仍需亲手发表');return;
+ }
+ if(result?.kind==='accepted') {
+ if(result.author_uid!==job.account.uid||result.fid!==job.fid||result.title!==window.DWW11Results.clean(snapshot.title)) {
+ if(job.state==='user_submitting')await rpc('event',{id:job.id,state:'submit_unknown',error:'结果页标题、首帖作者或版块与提交任务不符'});
+ await rpc('stop');status('结果不匹配,已暂停,请人工核对');return;
}
- }else if(job.state==='submit_unknown')status('发布结果待核对;不会重发、计数或换号。');
+ resultSeenAt ||= Date.now();
+ if(result.evidence.integrity_issues.some(x=>x.includes('尚未加载'))&&Date.now()-resultSeenAt<15000) {
+ status('已看到审核中详情,等待正文图片加载后核对');return;
+ }
+ const revision=stopRevision,epoch=run.epoch;
+ await rpc('accept',{id:job.id,...result});
+ sessionStorage.removeItem(SLOT);active=null;
+ // Re-read persistent control after accounting; Stop must suppress navigation and subsequent jobs.
+ run=await rpc('heartbeat',{account:account(),url:location.href,extension:extension()});
+ halted=!run.running;
+ if(!halted&&revision===stopRevision&&run.epoch===epoch)location.assign('https://www.dww11.com/');
+ else status('受理结果已保存;流程已暂停,请核对后明确继续');
+ return;
+ }
+ const clickedAt=local?.clickedAt||Number(job.submitted_at||0)*1000;
+ if(clickedAt&&Date.now()-clickedAt<45000) {
+ status(result?.prompt||result?.error||'等待网站提交结果,不会重复发表');return;
+ }
+ if(job.state==='user_submitting')await rpc('event',{id:job.id,state:'submit_unknown',error:result?.error||'提交结果证据不足,请打开对应帖子详情后核对'});
+ if(run.running)await rpc('stop');
+ status('发布结果待核对;打开对应详情页可继续核对,不会重发');
}
async function switchAccount() {
if(working||halted)return;
@@ -891,9 +966,10 @@ window.DWW11Formatter = Object.freeze({
location.assign('https://www.dww11.com/');return;
}
const ext=extension(),button=document.querySelector('#dww11-extension-next');
- if(!ext.ready||!ext.idle||!button||button.disabled) {status('请安装并重新加载带联动接口的换号插件');return;}
- await rpc('switch-begin');
- if(halted||!run?.running)return;
+ if(!ext.ready||!ext.idle||!button||button.disabled) {await rpc('stop');status('换号插件未就绪,流程已暂停,请检查插件后继续');return;}
+ const revision=stopRevision,epoch=run.epoch;
+ await rpc('switch-begin',{epoch});
+ if(halted||!run?.running||revision!==stopRevision)return;
button.click();status('正在换号;等待插件完成并核对新账号');
}
async function tick() {
@@ -913,7 +989,13 @@ window.DWW11Formatter = Object.freeze({
if(run.active && run.owner===owner) {const saved=await rpc('current');if(saved.job) {active=saved.job;await observeResult(saved.job);}}
status(run.reason || '流程已暂停');return;
}
- const next=await rpc('next',{account:account(),extension:extension()});run=next.run;
+ if(run.active&&run.owner===owner) {
+ const saved=await rpc('current');
+ if(saved.job&&['user_submitting','submit_unknown'].includes(saved.job.state)) {active=saved.job;await observeResult(saved.job);return;}
+ }
+ const revision=stopRevision,epoch=run.epoch;
+ const next=await rpc('next',{account:account(),extension:extension(),url:location.href});run=next.run;
+ if(revision!==stopRevision||run.epoch!==epoch)return;
if(skipBusy)return;
if(next.job)active=next.job;
if(next.action==='prepare'||(next.action==='resume'&&next.job.state==='preparing'))preparation=prepare(next.job);
@@ -925,6 +1007,8 @@ window.DWW11Formatter = Object.freeze({
}
else if(['importing','formatting'].includes(next.job.state))await fail(new Error('导入阶段发生中断,请检查编辑器和已上传图片后处理任务'));
else if(next.job.state==='needs_attention')status(next.job.error);
+ }else if(next.action==='home') {
+ if(!halted&&run.running)location.assign('https://www.dww11.com/');
}else if(next.action==='switch')await switchAccount();
else if(next.action==='switch_wait')status('等待换号结果;若插件已暂停,请人工处理后继续');
else if(next.action==='login')status('未能读取当前账号 UID,请登录并确认账号元素');
diff --git a/browser/results.js b/browser/results.js
new file mode 100644
index 0000000..ce7c7bd
--- /dev/null
+++ b/browser/results.js
@@ -0,0 +1,49 @@
+/* Read-only result adapter. Login state and URLs alone are never acceptance evidence. */
+(function(root,factory){
+ const api=factory();
+ if(typeof module==='object'&&module.exports)module.exports=api;
+ else root.DWW11Results=api;
+})(typeof window==='undefined'?null:window,()=>{
+ 'use strict';
+ const clean=value=>String(value||'').replace(/\s+/g,' ').trim();
+ function parameter(href,key,base){try{return new URL(href,base).searchParams.get(key);}catch{return null;}}
+ function aid(image){return String(image.getAttribute('aid')||image.id||'').match(/(?:attachimg_|aimg_)?(\d+)$/)?.[1]||null;}
+ function inspect(doc,url,snapshot={}) {
+ const titleNode=doc.querySelector('#thread_subject');
+ if(!titleNode) {
+ const prompt=doc.querySelector('#messagetext, .messagetext');
+ const text=clean(prompt?.textContent);
+ if(/^(?:抱歉[,,]\s*)?(?:您填写的)?(?:帖子)?标题.{0,40}(?:超过|超出).{0,40}(?:字符|长度|限制)/.test(text))return {kind:'rejected',error:text};
+ return {kind:'waiting',prompt:text,candidate:prompt?.querySelector('a[href]')?.href||''};
+ }
+ const region=titleNode.closest('td')||titleNode.closest('h1')?.parentElement;
+ if(!region)return {kind:'incomplete',error:'缺少标题状态区域'};
+ const ids=new Set([...region.querySelectorAll('a[href]')].filter(a=>parameter(a.href,'mod',url)==='viewthread').map(a=>parameter(a.href,'tid',url)).filter(x=>/^\d+$/.test(x)));
+ if(ids.size!==1)return {kind:'incomplete',error:'详情页真实主题编号缺失或冲突'};
+ const thread_id=[...ids][0],body=doc.querySelector('[id^="postmessage_"]');
+ const pid=body?.id.match(/^postmessage_(\d+)$/)?.[1];
+ const post=pid&&doc.querySelector('#post_'+pid);
+ if(!post||clean(post.querySelector('#postnum'+pid)?.textContent)!=='楼主')return {kind:'incomplete',error:'无法确认当前正文属于首帖'};
+ const authors=post?[...post.querySelectorAll('.plc .pti .authi a[href], .pls .authi a[href]')]:[];
+ const uids=new Set(authors.map(a=>parameter(a.href,'uid',url)).filter(x=>/^\d+$/.test(x)));
+ const fids=new Set([...doc.querySelectorAll('#pt a[href], .mumucms_pt a[href]')].filter(a=>parameter(a.href,'mod',url)==='forumdisplay').map(a=>parameter(a.href,'fid',url)).filter(x=>/^\d+$/.test(x)));
+ if(!body||!pid||uids.size!==1||fids.size!==1)return {kind:'incomplete',error:'首帖作者、正文或版块证据不足'};
+ const moderation=[...region.querySelectorAll('span.xg1')].some(n=>/\(\s*审核中\s*\)|(\s*审核中\s*)/.test(n.textContent));
+ // A public-page sample is not available yet. Absence of moderation is not proof of publication.
+ if(!moderation)return {kind:'incomplete',error:'尚无可验证的公开或审核中状态'};
+ const allImages=[...body.querySelectorAll('img')];
+ const images=allImages.filter(img=>aid(img)).map(img=>({aid:aid(img),src:img.getAttribute('src'),loaded:img.complete&&img.naturalWidth>0&&!/\/none\.gif(?:[?#]|$)/.test(img.src||img.getAttribute('src')||''),file:img.getAttribute('file')||img.getAttribute('zoomfile')}));
+ const expected=(snapshot.images||[]).map(img=>String(img.aid||img.id||'').match(/(?:attachimg_|aimg_)?(\d+)$/)?.[1]).filter(Boolean);
+ const issues=[];
+ if(allImages.some(img=>!aid(img)))issues.push('正文存在无法关联附件的图片');
+ if(expected.length!==(snapshot.images||[]).length)issues.push('提交快照存在无法识别附件编号的图片');
+ if(JSON.stringify(expected)!==JSON.stringify(images.map(img=>img.aid)))issues.push('首帖正文图片引用或顺序与提交快照不一致');
+ if(images.some(img=>!img.loaded))issues.push('结果页正文图片尚未加载或加载失败');
+ if(snapshot.text&&clean(body.innerText||body.textContent)!==clean(snapshot.text))issues.push('结果正文与最终提交快照存在文字差异');
+ const attachments=post?[...post.querySelectorAll('.pattl img, .attm img')].map(img=>({aid:aid(img),src:img.getAttribute('src')})):[];
+ if(attachments.length)issues.push('结果页存在附件区图片,请核对正文位置');
+ return {kind:'accepted',thread_id,first_post_pid:pid,post_url:url,title:clean(titleNode.textContent),author_uid:[...uids][0],fid:Number([...fids][0]),state:'accepted_pending_moderation',
+ evidence:{adapter:1,thread_id,first_post_pid:pid,title:clean(titleNode.textContent),author_uid:[...uids][0],fid:Number([...fids][0]),moderation:true,images,attachments,integrity_issues:issues}};
+ }
+ return {inspect,clean};
+});
diff --git a/browser/runner.js b/browser/runner.js
index d7dc73b..6a7a98b 100644
--- a/browser/runner.js
+++ b/browser/runner.js
@@ -7,7 +7,7 @@
const owner=sessionStorage.getItem('auto-news-owner') || crypto.randomUUID();
sessionStorage.setItem('auto-news-owner',owner);
let run=null, working=false, halted=true, aborter=null, active=null, tickBusy=false;
- let skipBusy=false, preparation=null;
+ let skipBusy=false, preparation=null, stopRevision=0, resultSeenAt=0;
const pending=new Map();
function rpc(route,body={}) {
return new Promise((resolve,reject)=>{
@@ -24,7 +24,7 @@
data.error?waiter.reject(new Error(data.error)):waiter.resolve(data.value);
});
const status=message=>document.dispatchEvent(new CustomEvent('dww11:connection-status',{detail:message}));
- document.addEventListener('dww11:auto-stop',()=>{halted=true;aborter?.abort(new Error('流程已终止'));});
+ document.addEventListener('dww11:auto-stop',()=>{stopRevision++;halted=true;aborter?.abort(new Error('流程已终止'));});
function account() {
const element=document.querySelector('#mumucms_username');
if(!element)return null;
@@ -142,16 +142,41 @@
const local=loadLocal();
if(local?.id===job.id&&local.snapshot&&job.state==='awaiting_user_review') {
await rpc('event',{id:job.id,state:'user_submitting',epoch:run.epoch,snapshot:local.snapshot});
- job.state='user_submitting';
+ job.state='user_submitting';job.snapshot=local.snapshot;
}
- if(job.state==='user_submitting') {
- // Result selectors intentionally await user-provided HTML; URL alone is not proof.
- if(!editorPage(job) || (local?.clickedAt && Date.now()-local.clickedAt>45000)) {
- await rpc('event',{id:job.id,state:'submit_unknown',error:'等待结果页 HTML 适配;候选页面:'+location.href});
- await rpc('stop');
- status('已记录候选结果页;尚不能自动核对。流程已暂停,请补充结果页 HTML。');
+ if(!['user_submitting','submit_unknown'].includes(job.state))return;
+ const snapshot=job.snapshot||local?.snapshot||{};
+ const result=window.DWW11Results?.inspect(document,location.href,snapshot);
+ if(result?.kind==='rejected'&&job.state==='user_submitting') {
+ await rpc('event',{id:job.id,state:'rejected',error:result.error});
+ status('网站明确拒绝本次提交,已暂停;核对并处理稿件后仍需亲手发表');return;
+ }
+ if(result?.kind==='accepted') {
+ if(result.author_uid!==job.account.uid||result.fid!==job.fid||result.title!==window.DWW11Results.clean(snapshot.title)) {
+ if(job.state==='user_submitting')await rpc('event',{id:job.id,state:'submit_unknown',error:'结果页标题、首帖作者或版块与提交任务不符'});
+ await rpc('stop');status('结果不匹配,已暂停,请人工核对');return;
}
- }else if(job.state==='submit_unknown')status('发布结果待核对;不会重发、计数或换号。');
+ resultSeenAt ||= Date.now();
+ if(result.evidence.integrity_issues.some(x=>x.includes('尚未加载'))&&Date.now()-resultSeenAt<15000) {
+ status('已看到审核中详情,等待正文图片加载后核对');return;
+ }
+ const revision=stopRevision,epoch=run.epoch;
+ await rpc('accept',{id:job.id,...result});
+ sessionStorage.removeItem(SLOT);active=null;
+ // Re-read persistent control after accounting; Stop must suppress navigation and subsequent jobs.
+ run=await rpc('heartbeat',{account:account(),url:location.href,extension:extension()});
+ halted=!run.running;
+ if(!halted&&revision===stopRevision&&run.epoch===epoch)location.assign('https://www.dww11.com/');
+ else status('受理结果已保存;流程已暂停,请核对后明确继续');
+ return;
+ }
+ const clickedAt=local?.clickedAt||Number(job.submitted_at||0)*1000;
+ if(clickedAt&&Date.now()-clickedAt<45000) {
+ status(result?.prompt||result?.error||'等待网站提交结果,不会重复发表');return;
+ }
+ if(job.state==='user_submitting')await rpc('event',{id:job.id,state:'submit_unknown',error:result?.error||'提交结果证据不足,请打开对应帖子详情后核对'});
+ if(run.running)await rpc('stop');
+ status('发布结果待核对;打开对应详情页可继续核对,不会重发');
}
async function switchAccount() {
if(working||halted)return;
@@ -159,9 +184,10 @@
location.assign('https://www.dww11.com/');return;
}
const ext=extension(),button=document.querySelector('#dww11-extension-next');
- if(!ext.ready||!ext.idle||!button||button.disabled) {status('请安装并重新加载带联动接口的换号插件');return;}
- await rpc('switch-begin');
- if(halted||!run?.running)return;
+ if(!ext.ready||!ext.idle||!button||button.disabled) {await rpc('stop');status('换号插件未就绪,流程已暂停,请检查插件后继续');return;}
+ const revision=stopRevision,epoch=run.epoch;
+ await rpc('switch-begin',{epoch});
+ if(halted||!run?.running||revision!==stopRevision)return;
button.click();status('正在换号;等待插件完成并核对新账号');
}
async function tick() {
@@ -181,7 +207,13 @@
if(run.active && run.owner===owner) {const saved=await rpc('current');if(saved.job) {active=saved.job;await observeResult(saved.job);}}
status(run.reason || '流程已暂停');return;
}
- const next=await rpc('next',{account:account(),extension:extension()});run=next.run;
+ if(run.active&&run.owner===owner) {
+ const saved=await rpc('current');
+ if(saved.job&&['user_submitting','submit_unknown'].includes(saved.job.state)) {active=saved.job;await observeResult(saved.job);return;}
+ }
+ const revision=stopRevision,epoch=run.epoch;
+ const next=await rpc('next',{account:account(),extension:extension(),url:location.href});run=next.run;
+ if(revision!==stopRevision||run.epoch!==epoch)return;
if(skipBusy)return;
if(next.job)active=next.job;
if(next.action==='prepare'||(next.action==='resume'&&next.job.state==='preparing'))preparation=prepare(next.job);
@@ -193,6 +225,8 @@
}
else if(['importing','formatting'].includes(next.job.state))await fail(new Error('导入阶段发生中断,请检查编辑器和已上传图片后处理任务'));
else if(next.job.state==='needs_attention')status(next.job.error);
+ }else if(next.action==='home') {
+ if(!halted&&run.running)location.assign('https://www.dww11.com/');
}else if(next.action==='switch')await switchAccount();
else if(next.action==='switch_wait')status('等待换号结果;若插件已暂停,请人工处理后继续');
else if(next.action==='login')status('未能读取当前账号 UID,请登录并确认账号元素');
diff --git a/build_browser.py b/build_browser.py
index 570a44d..d4ef2fe 100644
--- a/build_browser.py
+++ b/build_browser.py
@@ -4,7 +4,7 @@ ROOT = Path(__file__).resolve().parent
header = '''// ==UserScript==
// @name 新闻工作台 · 导入排版与流程
// @namespace local.dww11.auto-news
-// @version 0.1.2
+// @version 0.2.0
// @match https://www.dww11.com/*
// @match https://dww11.com/*
// @match https://house.dww11.com/*
@@ -14,7 +14,7 @@ header = '''// ==UserScript==
// ==/UserScript==
'''
parts = []
-for name in ('importer.js', 'formatter.js', 'runner.js'):
+for name in ('importer.js', 'formatter.js', 'results.js', 'runner.js'):
source = (ROOT / 'browser' / name).read_text()
if '// ==/UserScript==' in source:
source = source.split('// ==/UserScript==', 1)[1]
diff --git a/static/app.js b/static/app.js
index fa970b3..74923f8 100644
--- a/static/app.js
+++ b/static/app.js
@@ -1,7 +1,7 @@
'use strict';
const $=id=>document.getElementById(id);
let token='',state=null,loadedKey='',refreshing=false;
-const labels={queued:'待处理',preparing:'准备中',importing:'上传与导入',formatting:'排版中',awaiting_user_review:'等待人工审核',user_submitting:'已观察到人工提交',submit_unknown:'发布结果待核对',accepted_pending_moderation:'已受理 · 审核中',published:'已确认公开',needs_attention:'需要处理',cancelled:'已移出',skipped:'已跳过'};
+const labels={queued:'待处理',preparing:'准备中',importing:'上传与导入',formatting:'排版中',awaiting_user_review:'等待人工审核',user_submitting:'已观察到人工提交',submit_unknown:'发布结果待核对',accepted_pending_moderation:'已受理 · 审核中',published:'已确认公开',needs_attention:'需要处理',cancelled:'已移出',skipped:'已跳过',rejected:'网站明确拒绝'};
function notice(message){$('notice').hidden=!message;$('notice').textContent=message;}
async function api(route,body={}) {
const response=await fetch('/api/'+route,{method:'POST',headers:{'Content-Type':'application/json','X-Session-Token':token},body:JSON.stringify(body)});
@@ -19,8 +19,24 @@ function render(data){
$('jobs').replaceChildren(...jobs.map(job=>{
const tr=document.createElement('tr'),title=document.createElement('td');title.append(text('div',job.title),text('small',`${job.image_count} 张配图 · ${job.source}`));if(job.original_title!==job.title)title.append(text('small','已缩短标题,原题:'+job.original_title));
const status=document.createElement('td');status.append(text('span',labels[job.state]||job.state,'badge'));if(job.error)status.append(text('small',job.error));
+ for(const issue of job.integrity_issues||[])status.append(text('small',issue));
const account=text('td',job.account?`${job.account.name} / ${job.fid}`:'待领取');const result=document.createElement('td');
if(job.post_url){const link=text('a','查看帖子');link.href=job.post_url;link.target='_blank';link.rel='noopener';result.append(link);}
+ if(['user_submitting','submit_unknown'].includes(job.state)) {
+ const input=document.createElement('input');input.type='file';input.accept='.json,application/json';input.hidden=true;
+ const reconcile=text('button','补核已受理结果');reconcile.className='secondary';reconcile.onclick=()=>input.click();
+ input.onchange=async()=>{
+ try {
+ if(state.run.running)throw new Error('请先暂停流程');
+ const file=input.files[0];if(!file)return;if(file.size>2*1024*1024)throw new Error('记录文件过大');
+ const record=JSON.parse(await file.text());
+ if(!confirm(`确认该观察记录属于“${job.title}”?补核会阻止重发,不增加当前批次计数。`))return;
+ await api('reconcile',{id:job.id,article_key:job.article_key,confirmed:true,thread_id:String(record.threadId||record.thread_id||''),first_post_pid:String(record.postId||record.first_post_pid||''),post_url:record.url||record.post_url,title:record.title,author_uid:String(record.uid||record.author_uid||''),fid:Number(record.fid),state:record.status||record.state,evidence:{thread_id:String(record.threadId||record.thread_id||''),prompt:record.observedPrompt||'',historical:true,integrity_issues:['历史补核尚未完成发布后图文完整性检查']}});
+ notice('历史受理已登记,未增加当前批次计数');await refresh();
+ }catch(error){notice(error.message);}finally{input.value='';}
+ };
+ result.append(reconcile,input);
+ }
if(!['published','accepted_pending_moderation'].includes(job.state)){
const retry=text('button','人工核对 / 处理');retry.className='secondary';retry.onclick=async()=>{
if(state.run.running){notice('请先暂停流程,再处理任务');return;}
diff --git a/static/index.html b/static/index.html
index ce25dae..9c6d6bb 100644
--- a/static/index.html
+++ b/static/index.html
@@ -7,7 +7,7 @@
下载、导入、排版与账号切换,在一个工作台管理。把内容准备好,把审核留给你。