From 1959e62f3f8d1359a0dc6b9179fb4cde2d69bd2d Mon Sep 17 00:00:00 2001 From: root Date: Sat, 3 Oct 2026 23:31:20 +0800 Subject: [PATCH] =?UTF-8?q?feat(sources):=20=E7=AB=99=E7=82=B9=E5=BF=AB?= =?UTF-8?q?=E7=85=A7=E8=BF=9E=E7=BB=AD=E5=A4=B1=E8=B4=A5=203=20=E6=AC=A1?= =?UTF-8?q?=E5=BC=80=E7=AB=99=E7=82=B9=E4=B8=8D=E5=8F=AF=E8=BE=BE=E4=BA=8B?= =?UTF-8?q?=E9=A1=B9,=E6=8C=89=E7=AB=99=E7=82=B9=E5=8E=BB=E9=87=8D,?= =?UTF-8?q?=E6=81=A2=E5=A4=8D=E5=90=8E=E8=87=AA=E5=8A=A8=E5=85=B3=E9=97=AD?= =?UTF-8?q?(M9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 连续失败次数记在 site.snapshot_cursor.failStreak;凭据失效仍走原有登录失效事项。 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei --- .../sources-identity/service/snapshot.ts | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/apps/server/src/modules/sources-identity/service/snapshot.ts b/apps/server/src/modules/sources-identity/service/snapshot.ts index 63a65e29..d0dbee3d 100644 --- a/apps/server/src/modules/sources-identity/service/snapshot.ts +++ b/apps/server/src/modules/sources-identity/service/snapshot.ts @@ -8,7 +8,7 @@ import { jsonb } from "../rows.ts"; import type { SourcesRuntime } from "../runtime.ts"; import { siteCan } from "../site-kinds.ts"; import { enqueueRelogin } from "./account.ts"; -import { type Exec, openDecision } from "./common.ts"; +import { closeDecisions, type Exec, openDecision } from "./common.ts"; import { hasDetailPage, saveDetailPage } from "./detail-page.ts"; // 站点快照(周期列表)、补漏(Feed 疑似缺口,有页数预算)、实时补查(工作台搜索)。 @@ -129,6 +129,35 @@ async function onAuthError(rt: SourcesRuntime, trx: Exec, site: SiteRow, e: Site await enqueueRelogin(rt, site.id); } +/** 站点快照连续失败(改版 / Cloudflare 挑战 / 限流 / 网络,凭据失效另有事项)多少次后开待处理 */ +export const SNAPSHOT_FAIL_DECISION_AT = 3; + +/** 快照连续失败达到阈值 → 开站点不可达事项(按站点去重);恢复(本次成功或部分成功)→ 自动关闭 */ +async function syncSnapshotFailureDecision( + rt: SourcesRuntime, + trx: Exec, + site: SiteRow, + failStreak: number, + err: { kind: string; message: string } | null, +) { + const dedupeKey = `site-snapshot:${site.id}`; + if (failStreak === 0) { + await closeDecisions(rt.ctx, trx, dedupeKey, "resolved", { by: "snapshot_recovered" }, "system"); + return; + } + if (failStreak < SNAPSHOT_FAIL_DECISION_AT || !err) return; + await openDecision(rt.ctx, trx, { + kind: "source_unreachable", + severity: "warning", + title: `站点「${site.name}」列表抓取连续失败`, + summary: `连续 ${failStreak} 次没抓到列表(${err.kind}:${err.message})。可能是站点改版、Cloudflare 验证或限流;检查站点设置或稍后重试,恢复后自动关闭。`, + subjectType: "site", + subjectId: site.id, + dedupeKey, + evidence: { siteId: site.id, kind: err.kind, message: err.message.slice(0, 300), failStreak }, + }); +} + export async function snapshotSite(rt: SourcesRuntime, input: SnapshotInput): Promise { const { db } = rt.ctx; const none = { snapshotId: null, pages: [], itemCount: 0, newCount: 0, releaseIds: [] }; @@ -215,6 +244,9 @@ export async function snapshotSite(rt: SourcesRuntime, input: SnapshotInput): Pr if (error instanceof SiteError && error.isAuthError) { await onAuthError(rt, trx, site, error); } else if (input.kind !== "live_search") { + const prevStreak = Number((site.snapshot_cursor as { failStreak?: unknown } | null)?.failStreak) || 0; + const failStreak = status === "failed" ? prevStreak + 1 : 0; + await syncSnapshotFailureDecision(rt, trx, site, failStreak, errJson); await trx .updateTable("site") .set({ @@ -226,6 +258,7 @@ export async function snapshotSite(rt: SourcesRuntime, input: SnapshotInput): Pr lastItemCount: releases.length, lastStatus: status, lastError: errJson, + failStreak, at: rt.now().toISOString(), }), })