/** * 板块限制(subreddit restriction)SDK 类型 + 受限拦截的识别 helper(板块限制阶段 A4)。 * * SDK 不依赖 contracts 包,类型在这里内联,与 `packages/contracts/src/subreddit-restrictions.ts` * 同形(那边是权威定义;改契约时两边一起改)。 */ export type SubredditRestrictionKind = "banned" | "private" | "quarantined" | "unavailable"; /** provisional / verification_overdue / confirmed **都拦**;只有 revoked 不拦。 */ export type SubredditRestrictionState = "provisional" | "confirmed" | "verification_overdue" | "revoked"; export type SubredditRestrictionEvidenceLevel = "strong" | "weak"; export type SubredditRestrictionEvidenceItem = { type: string; url?: string; observedAt?: string; excerpt?: string; httpStatus?: number; detail?: Record; [key: string]: unknown; }; export type SubredditRestriction = { id: string; canonicalSubreddit: string; displaySubreddit: string; kind: SubredditRestrictionKind; state: SubredditRestrictionState; source: "admin" | "report" | "backfill"; version: number; evidence: SubredditRestrictionEvidenceItem[]; evidenceLevel: SubredditRestrictionEvidenceLevel; parserVersion: string | null; reason: string; lastVerifiedAt: string | null; /** 复核期限:provisional 过了它 → verification_overdue(仍拦)。 */ expiresAt: string | null; createdBy: string | null; createdAt: string; confirmedBy: string | null; confirmedAt: string | null; revokedBy: string | null; revokedAt: string | null; revokeReason: string | null; updatedAt: string; }; export type SubredditRestrictionEvidenceSummary = { count: number; types: string[]; latestObservedAt: string | null; firstUrl: string | null; }; /** 列表项:不带全量 evidence,只带摘要(全量走 get)。 */ export type SubredditRestrictionListItem = Omit & { evidenceSummary: SubredditRestrictionEvidenceSummary; }; export type ListSubredditRestrictionsResponse = { items: SubredditRestrictionListItem[]; total: number; limit: number; offset: number; hasMore: boolean; }; export type ListSubredditRestrictionsParams = { /** 多值 OR;缺省 = 全部状态(含 revoked)。 */ state?: SubredditRestrictionState[]; /** ISO 时间:只要 `updatedAt >= updatedSince`(agent 增量拉取)。 */ updatedSince?: string; subreddit?: string; limit?: number; offset?: number; }; export type CreateSubredditRestrictionInput = { subreddit: string; kind: SubredditRestrictionKind; /** 缺省 confirmed;provisional 必须带 `expiresAt`。 */ state?: "provisional" | "confirmed"; evidence?: SubredditRestrictionEvidenceItem[]; evidenceLevel: SubredditRestrictionEvidenceLevel; reason: string; expiresAt?: string; parserVersion?: string; }; export type ConfirmSubredditRestrictionInput = { expectedVersion?: number; evidence?: SubredditRestrictionEvidenceItem[]; evidenceLevel?: SubredditRestrictionEvidenceLevel; }; export type RevokeSubredditRestrictionInput = { reason: string; expectedVersion?: number; }; export type SubredditRestrictionFanoutOutcome = "cancelled" | "blocked" | "skipped" | "race_lost"; export type SubredditRestrictionFanoutParams = { version?: number; outcome?: SubredditRestrictionFanoutOutcome; teamId?: string; limit?: number; offset?: number; }; export type SubredditRestrictionFanoutResponse = { restrictionId: string; currentVersion: number; /** 有 outbox 行且全部 done 才为 true;阶段 B(扇出 worker)之前恒 false。 */ complete: boolean; changes: Array<{ version: number; change: "created" | "confirmed" | "overdue" | "revoked"; status: "pending" | "processing" | "done" | "failed"; attempts: number; summary: Record | null; lastError: string | null; createdAt: string; processedAt: string | null; }>; /** 不受 outcome 过滤与分页影响。 */ counts: Record; receipts: { items: Array<{ id: string; restrictionVersion: number; teamId: string; targetType: "calendar_entry" | "scheduled_job"; targetId: string; outcome: SubredditRestrictionFanoutOutcome; detail: Record | null; createdAt: string; }>; total: number; limit: number; offset: number; hasMore: boolean; }; }; export type ReportSubredditRestrictionInput = { subreddit: string; socialAccountId: string; kind: SubredditRestrictionKind; /** 至少一条。 */ evidence: SubredditRestrictionEvidenceItem[]; evidenceLevel: SubredditRestrictionEvidenceLevel; reason: string; /** 复核期限,必填(ISO)。 */ expiresAt: string; /** * 缺省 account。第一版判定:只有 `system` **且** `evidenceLevel=strong` 才写系统级 provisional * (且要求 manager/admin);`system` + weak 会降级为账号级 sanction(`downgraded:true`)。 */ scope?: "system" | "account"; parserVersion?: string; }; export type ReportSubredditRestrictionResponse = { target: "system"; created: boolean; restriction: SubredditRestrictionListItem; } | { target: "account"; created: boolean; sanctionId: string; sanctionStatus: string; downgraded: boolean; downgradeReason: "weak_evidence" | null; }; export type SubredditRestrictionCheckResult = { allowed: true; subreddit: string; invalidSubreddit: boolean; } | { allowed: false; subreddit: string; scope: "system" | "account"; source: "restriction" | "blocklist" | "sanction"; id: string; kind: string; state: SubredditRestrictionState | null; reason: string | null; expiresAt: string | null; evidenceLevel: SubredditRestrictionEvidenceLevel | null; }; /** `error.details.code` 的两个受限拦截码(与 contracts `SUBREDDIT_RESTRICTION_BLOCK_CODES` 同值)。 */ export declare const SUBREDDIT_RESTRICTION_BLOCK_CODES: { readonly TARGET_BLOCKED: "TARGET_BLOCKED"; readonly JOB_BLOCKED: "JOB_BLOCKED"; }; export type SubredditRestrictionBlockCode = (typeof SUBREDDIT_RESTRICTION_BLOCK_CODES)[keyof typeof SUBREDDIT_RESTRICTION_BLOCK_CODES]; /** `details.reason` 的固定值(外部抢权被拒时运营 executor 按它分流)。 */ export declare const TARGET_BLOCKED_REASON: "target_blocked"; /** * 受限拦截 409 的 `error.details`(与 contracts `subredditRestrictionBlockedDetailsSchema` 同形): * `{ code: "TARGET_BLOCKED" | "JOB_BLOCKED", reason: "target_blocked", source, id, kind?, subreddit }`。 * 外部条目抢权(scheduled→running)被拒时 `code` 可能是 `TARGET_BLOCKED`,识别以 `reason` 为准。 */ export type SubredditRestrictionBlockedDetails = { code: SubredditRestrictionBlockCode | string; reason: typeof TARGET_BLOCKED_REASON; source: "restriction" | "blocklist" | "sanction"; id: string; kind?: string; subreddit: string; }; /** * ops fail / skip 对「因板块限制被拦下」的 job 的幂等回执(no-op,无副作用)。 * 与 contracts `opsBlockedJobAckResponseSchema` 同形。 */ export type OpsBlockedJobAck = { ok: true; jobId: string; status: "blocked"; code: "JOB_BLOCKED"; }; /** * ops complete 对「认领后才被板块限制拦下」的 job 的执行回执:结果与 permalink 已留档, * job 仍是 blocked、不会被标成 succeeded、也不复发。与 contracts `opsBlockedReceiptResponseSchema` 同形。 * 注意它**没有** `trace` / `updated` —— 调用方按 `status === "blocked"` 分流。 */ export type OpsBlockedReceipt = { ok: true; jobId: string; status: "blocked"; code: "JOB_BLOCKED"; resultRecorded: true; warnings?: string[]; }; /** * 判断一个错误是不是「板块受限拦截」,是则返回 `details`,否则 null。 * * 🔴 **只看结构化字段,不看 message**:HTTP 409 且 `details.code ∈ {TARGET_BLOCKED, JOB_BLOCKED}` * 或 `details.reason === "target_blocked"`。message 是给人看的,措辞随时会改。 * * 命中后的 agent 停止规则:**本轮跳过该目标,不重试、不 fail/skip 重报**(重试只会得到同一个 409)。 * 解除路径按 `details.source` 区分:`restriction` 只有 admin `restrictions revoke`; * `sanction`(团队/账号级)由有权限的人把该 sanction 更新为 lifted;`blocklist` 由 admin 维护系统 blocklist。 * * 用鸭子类型而不是 `instanceof SocialHubHttpError`:跨包副本/打包后 instanceof 会假阴性。 */ export declare function getSubredditRestrictionBlock(err: unknown): SubredditRestrictionBlockedDetails | null; //# sourceMappingURL=subreddit-restrictions.d.ts.map