feat(sources): 站点快照连续失败 3 次开站点不可达事项,按站点去重,恢复后自动关闭(M9)
连续失败次数记在 site.snapshot_cursor.failStreak;凭据失效仍走原有登录失效事项。 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
1 parent
074dab04ca
commit
1959e62f3f
1 file changed
+34
-1
@@ -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<SnapshotResult> {
|
||||
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(),
|
||||
}),
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user