fix(news): harden exclusive batch completion and timeline evidence
Development CI / check-and-build (push) Successful in 2m10s
GitHub Git mirror / mirror (push) Successful in 7s
Development CI / deploy-development (push) Successful in 2m31s

This commit is contained in:
eezd committed 2026-11-02 20:53:53 +08:00
1 parent 8240ec44d1
commit 8f1d3b249b
5 files changed
+157 -3

No files matched your search

@@ -76,6 +76,7 @@ async def start(session, identifier, run_mode="shared", validated=False):
if not validated: raise ValueError('仅历史批次必须先验证三阶段配置')
settings = await session.get(RiskSettings, 1)
settings.enabled = True
if settings.start_at is None: settings.start_at = datetime.now(UTC)
job = ReprocessJob(state='pending', items=[], position=0, failures=[], settings=value['config'])
session.add(job); await session.flush()
job.settings = {**job.settings, 'job_id':job.id, 'request':{**value['request'], 'run_mode':run_mode}}
@@ -166,6 +167,23 @@ async def reconcile(session, job):
await session.delete(row); waiting=True
continue
if obj.state=='retired': continue
if row.kind == 'events' and job.settings.get('request', {}).get('run_mode') == 'history_only':
# Retry busy members next pass instead of blocking their publication.
dependencies = (await session.scalars(select(NewsSummary).join(EventMember).where(
EventMember.event_id == obj.id, NewsSummary.replacement_ids == [], NewsSummary.state != 'withdrawn',
or_(NewsSummary.state != 'done', NewsSummary.result.is_(None), func.jsonb_typeof(NewsSummary.result) == 'null',
NewsSummary.published_revision < NewsSummary.revision)).order_by(NewsSummary.id)
.with_for_update(of=NewsSummary, skip_locked=True))).all()
for package in dependencies:
# Only unfinished members of this event join the frozen batch.
package.review_config = job.settings
if package.state == 'done':
from alphapulse.news_risk.worker import schedule
schedule(package, datetime.now(UTC), True)
if package.state == 'pending': package.due_at = datetime.now(UTC)
await target(session, job.id, 'analysis', package)
if package.state == 'failed': failures.append({'stage':'analysis','id':package.id,'error':package.error})
else: waiting = True
if obj.state=='failed': failures.append({'stage':row.kind,'id':obj.id,'error':obj.error})
elif row.kind == 'events' and await session.scalar(select(NewsSummary.id).join(EventMember).where(
EventMember.event_id == obj.id, NewsSummary.state == 'failed', NewsSummary.replacement_ids == []).limit(1)):
@@ -44,6 +44,8 @@ def validate_timeline(result, packages):
raise ValueError('事件发生时间必须来自组成包已核验时间点')
if point.planned_at is not None and not any(
PackageTimePoint.model_validate(p).planned_at == point.planned_at and p.get('planned_precision') == point.planned_precision
and p.get('time_text') == point.time_text and point.time_text.strip()
and any(ref in p['evidence_refs'] and point.time_text in ref.get('quote', '') for ref in point.evidence_refs)
for p in package['result'].get('timeline_points', [])):
raise ValueError('事件计划时间必须来自组成包已核验计划')
if any(ref not in allowed for ref in point.evidence_refs):
+3 -3
View File
@@ -265,14 +265,14 @@ async def outlook(prepare_only=False):
if job and job.settings.get('request', {}).get('run_mode') != 'history_only' and job.settings.get('request'): job = None
if job and job.settings.get('request', {}).get('run_mode') == 'history_only':
from alphapulse.models.risk import ReprocessItem
if await session.scalar(select(ReprocessItem.id).where(ReprocessItem.job_id == job.id, ReprocessItem.state.in_(['pending','processing'])).limit(1)): return
if await session.scalar(select(ReprocessItem.id).where(ReprocessItem.job_id == job.id, ReprocessItem.state.in_(['pending','processing','failed'])).limit(1)): return
if job and job.position < len(job.items):
if requested and requested.quotes.get('job_id') == job.id: return
job = None
if job:
from alphapulse.models.risk import NewsEvent
if await session.scalar(select(NewsSummary.id).where(NewsSummary.state.in_(['pending', 'processing']), NewsSummary.review_config['job_id'].astext.cast(BigInteger) == job.id).limit(1)): return
if await session.scalar(select(NewsEvent.id).where(NewsEvent.state.in_(['pending', 'processing']), NewsEvent.review_config['job_id'].astext.cast(BigInteger) == job.id).limit(1)): return
if await session.scalar(select(NewsSummary.id).where(NewsSummary.state.in_(['pending', 'processing', 'failed']), NewsSummary.review_config['job_id'].astext.cast(BigInteger) == job.id).limit(1)): return
if await session.scalar(select(NewsEvent.id).where(NewsEvent.state.in_(['pending', 'processing', 'failed']), NewsEvent.review_config['job_id'].astext.cast(BigInteger) == job.id).limit(1)): return
from alphapulse.news_risk.reprocess import frozen_configuration
provider, system, rules = await frozen_configuration(session, job.settings, 'outlook')
else: