接通审核中结果核对、受理记账与后续调度
This commit is contained in:
1 parent
d157c179c8
commit
f0be4f5596
18 files changed
+510
-57
No files matched your search
@@ -1,12 +1,14 @@
|
||||
"""Persistent serial queue. Browser submissions are always initiated by the user."""
|
||||
import json
|
||||
import copy
|
||||
import re
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
import secrets
|
||||
import sqlite3
|
||||
import threading
|
||||
import time
|
||||
from urllib.parse import urlsplit
|
||||
from urllib.parse import urlsplit, parse_qs
|
||||
|
||||
from articles import inspect_article
|
||||
|
||||
@@ -25,10 +27,21 @@ class Store:
|
||||
CREATE TABLE IF NOT EXISTS ledger(key TEXT PRIMARY KEY, value TEXT NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS settings(id INTEGER PRIMARY KEY CHECK(id=1), value TEXT NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS posts(id TEXT PRIMARY KEY, job TEXT NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS post_aliases(url TEXT PRIMARY KEY, thread_id TEXT NOT NULL, job TEXT NOT NULL);
|
||||
''')
|
||||
row = db.execute('SELECT value FROM settings WHERE id=1').fetchone()
|
||||
self.run = json.loads(row[0]) if row else dict(running=False, epoch=0, config={}, batch=None, active=None, owner=None, switch=None, reason='请配置任务后开始')
|
||||
self.run.update(running=False, epoch=self.run['epoch'] + 1, reason='服务已启动,请明确点击开始/继续;未决任务不会重发')
|
||||
for row in db.execute('SELECT value FROM jobs').fetchall():
|
||||
job = json.loads(row[0])
|
||||
if job['state'] in ACCEPTED and job.get('post_url'):
|
||||
canonical = job.get('thread_id')
|
||||
if not canonical:
|
||||
job['legacy_url_id'] = job.get('post_id')
|
||||
job['identity_needs_reconciliation'] = True
|
||||
self._save(db, job)
|
||||
db.execute('INSERT OR IGNORE INTO post_aliases VALUES(?,?,?)',
|
||||
(job['post_url'], canonical or 'unknown', job['id']))
|
||||
self._persist(db)
|
||||
self.clients = {}
|
||||
if ledger and Path(ledger).is_file():
|
||||
@@ -37,9 +50,14 @@ class Store:
|
||||
@contextmanager
|
||||
def db(self):
|
||||
connection = sqlite3.connect(self.path, timeout=20)
|
||||
previous = copy.deepcopy(getattr(self, 'run', None))
|
||||
try:
|
||||
with connection:
|
||||
yield connection
|
||||
except Exception:
|
||||
if previous is not None:
|
||||
self.run = previous
|
||||
raise
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
@@ -151,6 +169,12 @@ class Store:
|
||||
self.run.update(batch=self._batch(account), switch=None)
|
||||
self._persist(db)
|
||||
else:
|
||||
ext = data.get('extension', {})
|
||||
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:
|
||||
self.run.update(running=False, reason='换号已暂停、超时或账号未改变,请检查插件并人工恢复,禁止重复抽号')
|
||||
self._persist(db)
|
||||
return dict(action='idle', run=self.run)
|
||||
return dict(action='switch' if switch['state'] == 'pending' else 'switch_wait', run=self.run)
|
||||
if self.run['batch'] is None:
|
||||
self.run['batch'] = self._batch(account)
|
||||
@@ -158,6 +182,13 @@ class Store:
|
||||
self.run.update(running=False, reason='检测到账号变化,请先核对当前任务')
|
||||
self._persist(db)
|
||||
return dict(action='idle', run=self.run)
|
||||
if self.run.get('return_home'):
|
||||
url = urlsplit(str(data.get('url', '')))
|
||||
if url.scheme == 'https' and url.netloc in ('www.dww11.com', 'dww11.com') and url.path == '/' and not url.query:
|
||||
self.run['return_home'] = False
|
||||
self._persist(db)
|
||||
else:
|
||||
return dict(action='home', run=self.run)
|
||||
if self.run['active']:
|
||||
job = self._job(db, self.run['active'])
|
||||
return dict(action='resume', job=job, run=self.run)
|
||||
@@ -207,7 +238,9 @@ class Store:
|
||||
self._check_owner(data['owner'])
|
||||
if not self.run['running'] or not self.run['switch'] or self.run['switch']['state'] != 'pending':
|
||||
raise ValueError('换号状态已改变,禁止重复点击')
|
||||
self.run['switch']['state'] = 'clicked'
|
||||
if data.get('epoch', self.run['epoch']) != self.run['epoch']:
|
||||
raise ValueError('换号请求已过期')
|
||||
self.run['switch'].update(state='clicked', clicked_at=time.time())
|
||||
self._persist(db)
|
||||
return {'ok': True}
|
||||
|
||||
@@ -234,7 +267,7 @@ class Store:
|
||||
transitions = {'preparing': {'importing', 'needs_attention'}, 'importing': {'formatting', 'needs_attention'},
|
||||
'formatting': {'awaiting_user_review', 'needs_attention'},
|
||||
'awaiting_user_review': {'user_submitting', 'needs_attention'},
|
||||
'user_submitting': {'submit_unknown', 'needs_attention'}, 'submit_unknown': {'needs_attention'}}
|
||||
'user_submitting': {'submit_unknown', 'needs_attention', 'rejected'}, 'submit_unknown': {'needs_attention'}}
|
||||
if target not in transitions.get(job['state'], set()):
|
||||
raise ValueError('任务状态不允许此操作:' + job['state'] + ' → ' + target)
|
||||
if target in ('importing', 'formatting', 'awaiting_user_review') and (not self.run['running'] or data.get('epoch') != self.run['epoch']):
|
||||
@@ -244,13 +277,27 @@ class Store:
|
||||
job.update(submitted_at=time.time(), snapshot=data.get('snapshot', {}), attempt=secrets.token_hex(8))
|
||||
if target == 'awaiting_user_review':
|
||||
job['prepared'] = data.get('snapshot', {})
|
||||
if target == 'needs_attention':
|
||||
if target in ('needs_attention', 'rejected'):
|
||||
self.run.update(running=False, reason=job['error'] or '当前任务需要处理')
|
||||
self._persist(db)
|
||||
self._save(db, job)
|
||||
return {'ok': True}
|
||||
|
||||
def reconcile(self, data):
|
||||
if data.get('confirmed') is not True:
|
||||
raise ValueError('请先确认这是当前文章的已受理结果')
|
||||
with self.lock, self.db() as db:
|
||||
if self.run['running']:
|
||||
raise ValueError('请先暂停流程再补核历史受理记录')
|
||||
job = self._job(db, data['id'])
|
||||
if data.get('article_key') != job['article_key']:
|
||||
raise ValueError('文章身份不匹配')
|
||||
return self._accept({**data, 'owner': self.run['owner']}, count_batch=False)
|
||||
|
||||
def accept(self, data):
|
||||
return self._accept(data)
|
||||
|
||||
def _accept(self, data, count_batch=True):
|
||||
with self.lock, self.db() as db:
|
||||
self._check_owner(data['owner'])
|
||||
job = self._job(db, data['id'])
|
||||
@@ -259,32 +306,46 @@ class Store:
|
||||
if self.run['active'] != job['id'] or job['state'] not in SUBMITTED:
|
||||
raise ValueError('只有已观察到人工提交的任务才能登记结果')
|
||||
url = urlsplit(str(data.get('post_url', '')))
|
||||
import re
|
||||
match = re.fullmatch(r'/thread/(\d+)\.html', url.path)
|
||||
if url.scheme != 'https' or url.netloc not in ('www.dww11.com', 'dww11.com') or not match:
|
||||
query = parse_qs(url.query)
|
||||
thread_id = str(data.get('thread_id', ''))
|
||||
if not re.fullmatch(r'[1-9]\d*', thread_id):
|
||||
raise ValueError('缺少详情页真实主题 ID,不能以地址数字代替')
|
||||
if url.scheme != 'https' or url.netloc not in ('www.dww11.com', 'dww11.com') or not (match or (url.path == '/forum.php' and query.get('mod') == ['viewthread'] and query.get('tid') == [thread_id])):
|
||||
raise ValueError('未识别到帖子详情页')
|
||||
if data.get('author_uid') != job['account']['uid'] or data.get('fid') != job['fid']:
|
||||
raise ValueError('结果页作者或版块与任务不符,请人工核对')
|
||||
if data.get('title') != job.get('snapshot', {}).get('title'):
|
||||
if ' '.join(str(data.get('title', '')).split()) != ' '.join(str(job.get('snapshot', {}).get('title', '')).split()):
|
||||
raise ValueError('结果页标题与人工提交快照不一致')
|
||||
if data.get('state') not in ACCEPTED:
|
||||
raise ValueError('需要明确的公开或审核中状态')
|
||||
if not data.get('evidence'):
|
||||
raise ValueError('缺少页面结果证据')
|
||||
post_id = match[1]
|
||||
evidence = data['evidence']
|
||||
if not isinstance(evidence, dict) or str(evidence.get('thread_id')) != thread_id:
|
||||
raise ValueError('主题 ID 与页面证据不一致')
|
||||
alias = db.execute('SELECT thread_id, job FROM post_aliases WHERE url=?', (url.geturl(),)).fetchone()
|
||||
if alias and (alias[0] != thread_id or alias[1] != job['id']):
|
||||
raise ValueError('帖子链接已绑定其他记录')
|
||||
post_id = thread_id
|
||||
existing = db.execute('SELECT job FROM posts WHERE id=?', (post_id,)).fetchone()
|
||||
if existing and existing[0] != job['id']:
|
||||
raise ValueError('帖子已绑定其他任务,不能重复计数')
|
||||
db.execute('INSERT OR IGNORE INTO posts VALUES(?,?)', (post_id, job['id']))
|
||||
job.update(state=data['state'], post_id=post_id, post_url=url.geturl(), result=data['evidence'])
|
||||
db.execute('INSERT OR IGNORE INTO post_aliases VALUES(?,?,?)', (url.geturl(), thread_id, job['id']))
|
||||
job.update(state=data['state'], thread_id=thread_id, post_id=post_id, first_post_pid=data.get('first_post_pid'), post_url=url.geturl(), result=evidence, integrity_issues=evidence.get('integrity_issues', []))
|
||||
keys = ['article:' + job['article_key'], 'md:' + job['markdown_sha256']] + ['source:' + str(k) for k in job['source_keys']]
|
||||
for key in keys:
|
||||
db.execute('INSERT OR REPLACE INTO ledger VALUES(?,?)', (key, json.dumps(job, ensure_ascii=False)))
|
||||
self.run['batch']['count'] += 1
|
||||
self.run.update(active=None, not_before=time.time() + self.run['config']['interval'])
|
||||
if count_batch:
|
||||
self.run['batch']['count'] += 1
|
||||
job['historical_reconciliation'] = not count_batch
|
||||
self.run.update(active=None, return_home=True, not_before=time.time() + self.run['config']['interval'])
|
||||
if job['integrity_issues']:
|
||||
self.run.update(running=False, reason='网站已受理,内容核对发现差异,请检查帖子后明确继续')
|
||||
self._save(db, job)
|
||||
self._persist(db)
|
||||
return {'ok': True}
|
||||
return {'ok': True, 'run': dict(self.run)}
|
||||
|
||||
def resolve(self, data):
|
||||
"""Explicit local UI resolution, never an automatic retry of an unknown submission."""
|
||||
@@ -297,6 +358,8 @@ class Store:
|
||||
if data.get('confirmed_not_accepted') is not True:
|
||||
raise ValueError('必须先人工核对网站未受理,并清理编辑器/草稿')
|
||||
job.update(state='queued' if data.get('retry') else 'cancelled', error='', snapshot={})
|
||||
for key in ('submitted_at', 'attempt', 'prepared'):
|
||||
job.pop(key, None)
|
||||
if self.run['active'] == job['id']:
|
||||
self.run.update(active=None, owner=None)
|
||||
self._save(db, job)
|
||||
|
||||
Reference in new issue
Block a user