Add resumable US policy news collectors and provenance
Development CI / check-and-build (push) Successful in 2m54s
GitHub Git mirror / mirror (push) Successful in 9s
Development CI / deploy-development (push) Failing after 4m24s

Collect official crypto and US policy documents with deterministic screening, full bodies and attachments, bounded retries, and durable history cursors. Preserve publication precision and source identity while keeping new collectors disabled.

Add real Congress, GovInfo, DOJ and Federal Register fixtures, offline capture replay, isolated database validation, and rollout instructions. Validate 982 backend tests, frontend checks, and 57 deployment control tests.
This commit is contained in:
eezd committed 2026-11-04 21:41:04 +08:00
1 parent dc8a283571
commit 89c729092c
31 files changed
+2199 -23

No files matched your search

+13 -2
View File
@@ -65,6 +65,8 @@ class RunState:
history_position: datetime | None = None
history_done: bool = False
history_deadline: float = 0
official_cursor: dict | None = None
stats: dict = field(default_factory=dict)
@property
def full_scan(self) -> bool:
@@ -104,6 +106,7 @@ class NewsSpider(RedisSpider):
self._tasks: set[asyncio.Task] = set()
# 上一轮成功后的回补进度 (目标, 断点, 是否结束)。写库是异步的,管理进程推来的下一条触发可能还带着旧断点
self.history_checkpoint: tuple[datetime, datetime, bool] | None = None
self.finish_task: asyncio.Task | None = None
@classmethod
def from_crawler(cls, crawler: Crawler, *args: Any, **kwargs: Any) -> "NewsSpider":
@@ -177,7 +180,7 @@ class NewsSpider(RedisSpider):
run, self.run = self.run, None
if run.history_target is not None and not run.errors:
self.history_checkpoint = (run.history_target, run.history_position, run.history_done)
self._spawn(self._finish_run(run))
self.finish_task = self._spawn(self._finish_run(run))
def _on_error(self, failure: Failure, response: Response) -> None:
if self.run:
@@ -191,10 +194,11 @@ class NewsSpider(RedisSpider):
self.logger.info("采集进程退出")
await self._flush_logs()
def _spawn(self, coro: Coroutine) -> None:
def _spawn(self, coro: Coroutine) -> asyncio.Task:
task = asyncio.get_running_loop().create_task(coro)
self._tasks.add(task)
task.add_done_callback(self._tasks.discard)
return task
async def _insert_run(self, run: RunState) -> None:
async with SessionLocal() as session:
@@ -211,6 +215,7 @@ class NewsSpider(RedisSpider):
values = {
"finished_at": now,
"status": "failed" if failed else "success",
"stats": run.stats,
"requests": run.requests,
"items_seen": run.items_seen,
"items_new": run.items_new,
@@ -227,6 +232,12 @@ class NewsSpider(RedisSpider):
source = await session.get(Source, self.name, with_for_update=True)
# 本轮失败不推进断点,下一轮从原位置重做(写入幂等);目标期间被修改或取消时丢弃本轮进度
history_saved = not failed and run.history_target is not None and source.history_target == run.history_target
if not failed and run.official_cursor is not None:
cursor = dict(source.history_cursor or {})
cursor.update({k: v for k, v in run.official_cursor.items() if k != "history"})
if history_saved:
cursor["history"] = run.official_cursor.get("history", {})
source.history_cursor = cursor
if history_saved:
source.history_position = run.history_position
source.history_done = run.history_done