import type { ConfirmSubredditRestrictionInput, CreateSubredditRestrictionInput, ListSubredditRestrictionsParams, ListSubredditRestrictionsResponse, OpsBlockedJobAck, OpsBlockedReceipt, ReportSubredditRestrictionInput, ReportSubredditRestrictionResponse, RevokeSubredditRestrictionInput, SubredditRestriction, SubredditRestrictionCheckResult, SubredditRestrictionFanoutParams, SubredditRestrictionFanoutResponse } from "./subreddit-restrictions.js"; export * from "./subreddit-restrictions.js"; /** * 「返回了不完整结果、但看起来像完整的」这一族缺陷的统一探针。 * * 触发条件:JSON 响应体是个分页列表,而这一页并不是全部。判据有两条,任一成立即触发: * - `hasMore === true`(**最可靠**的信号,总数被封顶/缺失时唯一可用的那条); * - `items.length < total`,且 `total` 确实是**精确条数**(见 `totalRelation`)。 * * ⚠️ 判定必须在**运行时按响应体形状**做,不能按 SDK 方法的返回类型做: * 实测 apps/api 里返回该形状的端点有 43 个,而 SDK 在类型上声明了 `items`+`total` * 的方法只有 20 个(`listSystemSubredditBlocklists` 就曾把返回类型误写成 * `{ items: unknown[] }`,漏掉了 `total`)。按类型判会漏掉一半以上。 * * SDK 默认**不设**这个回调,所以对既有 SDK 消费者零行为变化;由 CLI 装上一个 * 写 stderr 的实现(stdout 必须保持干净可 `jq` 解析)。 */ export type PartialListInfo = { /** * **只有 pathname,不含 query string**。查询串里可能带 `search=<关键词>` 这类 * 调用方输入,而这条信息是要打到 stderr 上的 —— 不该顺手把用户的搜索词写进 * 日志。(Codex 评审提出) */ path: string; itemsLength: number; /** 服务端给出的总数原值;`totalRelation` 决定它能不能当精确条数用。 */ total?: number; /** * 服务端对 `total` 精确度的自述(迁移 0140 起部分端点会封顶计数): * - `eq` / 缺省 —— `total` 是精确条数; * - `gte` —— `total` 只是**下界**(计数被封顶),不能据此算「还缺几条」; * - `unavailable` —— 没有总数,分页信号只有 `hasMore`。 */ totalRelation?: string; /** 服务端明说还有下一页。比任何基于 `total` 的推算都可靠。 */ hasMore?: boolean; limit?: number; offset?: number; }; export type SocialHubClientOptions = { baseUrl: string; apiKey: string; /** 见 {@link PartialListInfo};不传则完全不做通知(默认行为不变)。 */ onPartialList?: (info: PartialListInfo) => void; }; /** system subreddit blocklist 的一行(与 contracts subredditBlocklistSchema 同形)。 */ export type SystemSubredditBlocklistEntry = { id: string; kind: string; subreddit: string; reason: string | null; source: string | null; expiresAt: string | null; createdAt: string; }; /** * 按形状识别「分页列表返回体」并判断是否被截断。返回 undefined 表示不是该形状, * 或者这一页已经是全部。 * * 刻意**只**认 `items` + `total` 两个字段同时存在:仓库里 `total` 作为「条数」 * 的用法与 `items` 恒定成对出现(43 个端点),而把 `total` 用作金额/总分之类 * 其它语义的返回体不带 `items` 数组,不会误伤。 */ export declare function detectPartialList(path: string, body: unknown): PartialListInfo | undefined; /** * 「拿到的是一页、不是全量」的统一措辞。与仓库既有的 CSV 导出约定同源 —— * 那边命中行数硬顶时必须在末行显式写 `mode=truncated`(见 contracts/src/common.ts), * 一样是「绝不静默截断」。这里是同一条纪律在列表端点上的落点。 * * ⚠️ 措辞刻意保持**通用**,两处不能写死(Codex 评审提出): * - 不能写「用 --limit/--offset 翻页」—— `compliance blocklists-list` 这类安全闸 * 刻意**不提供**这两个参数(它自己自动翻页),照着提示做只会撞上「未知选项」; * - 不能写「漏掉的是最早的那批」—— 那只对按时间倒序的 blocklist 成立, * 别的端点排序不同,写死就是在编造事实。 */ export declare function formatPartialListWarning(info: PartialListInfo): string; export type HealthCheckJson = { ok?: boolean; db?: string; redis?: string; }; /** Persona default style guide shape (see contracts personaDefaultStyleGuideSchema). */ export type PersonaDefaultStyleGuide = { schemaVersion?: string; locale?: string | null; voiceTraits?: string[]; toneTags?: string[]; doPatterns?: string[]; avoidPatterns?: string[]; formatRules?: string[]; samples?: { excerpt: string; note?: string; }[]; /** 声音宪法行为差异层(六维行为规则短句;需求 PX7q §3.1)。 */ constitution?: { attentionPriority?: string; stanceOnUncertainty?: string; disagreementStyle?: string; closingBehavior?: string; detailDepth?: string; topicReflex?: string; }; }; /** GET /health without API key. */ export declare function getSocialHubHealth(baseUrl: string): Promise; /** GET /health/live without API key. */ export declare function getSocialHubHealthLive(baseUrl: string): Promise<{ ok: boolean; }>; /** POST /v1/auth/device/code — no API key required. */ export declare function createDeviceCode(baseUrl: string, body: { clientName?: string; deviceName?: string; teamId?: string; durationKind?: "short" | "long" | "custom" | "never"; expiresAt?: string; }): Promise; /** POST /v1/auth/device/token — poll for CLI access token. */ export declare function pollDeviceToken(baseUrl: string, deviceCode: string): Promise; /** Query params for GET /calendar-entries */ export type BehavioralHealthMetric = { numerator: number; denominator: number; rate: number | null; sampleStatus: "ok" | "insufficient_sample"; }; export type ListCalendarEntriesParams = { brandId?: string; campaignId?: string; publishingPlanId?: string; socialAccountId?: string; status?: string; from?: string; to?: string; limit?: number; /** 服务端上限 200;超过就得靠它翻页,别用时间窗二分绕(会漏)。 */ offset?: number; }; /** Body for PATCH /calendar-entries/:id */ export type UpdateCalendarEntryBody = { plannedAt?: string; status?: string; /** * 「谁来发这条内容」——防重复发帖的唯一闸门(方案 Block 0 · D1)。 * 改它会让服务端在同一事务里撤销/创建内部 job 并 append directive 指令, * 因此这是**高风险写**,CLI 侧带 `--apply` 门。见 {@link CalendarEntryDeliveryMode}。 */ deliveryMode?: CalendarEntryDeliveryMode; permalink?: string; failureReason?: string; /** * R7 · 抢占执行权时的乐观版本钉子(只对 `scheduled → running` 有效)。 * 🔴 终态回写**不要带**:抢占那一次自己会把 revision 推到 N+1,终态再带 N 必然失败 * (得到 `expected_revision_not_applicable`)。 */ expectedRevision?: number; /** * R7 · 服务端签发的执行 capability token。终态回写必带(只要条目有在途授权); * 初次抢占**不得**带。⚠️ bearer 凭据,别打日志。 */ executionToken?: string; /** * R7 · 抢占的幂等键(客户端生成,建议 UUID)。 * **强烈建议每次抢占都带** —— 它是「claim 已提交但响应丢失」的唯一补救。 */ claimRequestId?: string; /** * R7 · 强制终结:宿主机死在 `running` 中途时的逃生口。 * 仅 manager/admin、仅 `failed`/`cancelled`、必须同时给 `failureReason`。 * ⚠️ 不是把执行权让给下一台机 —— 条目就此终结。 */ forceTerminalize?: boolean; }; /** * R7 · 抢占成功时服务端回的执行授权。 * 🔴 `token` 是 bearer 凭据:存本机 state 文件,别打日志、别回显。 */ export type CalendarEntryExecutionGrant = { /** 单调递增的执行代数,仅供审计对账;授权判据是 `token`。 */ epoch: number; token: string; /** * 抢占**之后**的 directiveRevision(= 抢占前 + 1)。 * ⚠️ 抢占会 append 一条 `cancel` 指令并广播给**包括赢家在内**的全部机器 —— * 协议规定它只删**未来的** cron,**绝不中止已持 token 的在途执行**。 */ directiveRevision: number; }; export type ListRedditPostSnapshotsParams = { limit?: number; offset?: number; campaignId?: string; brandId?: string; calendarEntryId?: string; socialAccountId?: string; opsStatus?: string; draftStatus?: string; postType?: string; repostType?: string; containsBrandKeyword?: "true" | "false" | "unset"; primaryProduct?: string; /** * 序列号闭区间 `seqNoMin <= seqNo <= seqNoMax`(两端各自可选)。 * 任一端给定时,`seqNo` 为 null 的行都会落选;min > max 是 400。 */ seqNoMin?: number; seqNoMax?: number; /** 时间段(与 stats 同口径:COALESCE(postedAt, capturedAt));dateFrom/dateTo 优先。 */ range?: number | "all"; dateFrom?: string; dateTo?: string; sortBy?: "score" | "comments_count" | "posted_at" | "view_count" | "seq_no"; sortDir?: "asc" | "desc"; aggregateVisibleTeams?: boolean; scopeTeamId?: string; }; export type RedditPostSnapshotStatsParams = { campaignId?: string; brandId?: string; calendarEntryId?: string; opsStatus?: string; draftStatus?: string; postType?: string; repostType?: string; containsBrandKeyword?: "true" | "false" | "unset"; primaryProduct?: string; /** 序列号闭区间(与列表同口径)。 */ seqNoMin?: number; seqNoMax?: number; range?: number | "all"; /** Custom period bounds (YYYY-MM-DD, inclusive). Take precedence over range. */ dateFrom?: string; dateTo?: string; compare?: "wow" | "mom" | "both"; aggregateVisibleTeams?: boolean; scopeTeamId?: string; }; /** Mirrors the API's browser environment telemetry schema (all 14 keys). */ export type BrowserEnvironmentTelemetryInput = { baselineIp?: string; baselineCity?: string; baselineCountry?: string; lastIp?: string; lastCity?: string; lastCountry?: string; expectedRegion?: string; lastCheckedAt?: string; lastAsn?: string; lastProvider?: string; driftDetected?: boolean; driftReason?: string; baselineFingerprint?: string; lastCheckStatus?: string; }; /** Canonical opsStatus values stored in Hub DB. */ export type RedditOpsStatus = "normal" | "risk" | "deleted" | "abnormal" | "unknown"; export type OpsClaimNextSuccessResponse = { job: Record; }; export type OpsClaimNextIdleResponse = { job: null; code: "no_claimable_job"; message?: string; }; export type OpsClaimNextResponse = OpsClaimNextSuccessResponse | OpsClaimNextIdleResponse; export type AccountPoolsResponse = { items: unknown[]; personaPools?: unknown; systemPools?: unknown; fallback?: unknown; /** * 分页方案 B8b:本次生效的 limit / 返回条数 / 是否被截断(配额、limit 或单 key 行数上限)。 * 该端点没有 offset。配额 / limit 截断可调大 limit 或收窄 industry/tier;单 key 行数上限是 * 上游固定帽,调 limit 补不全,只能收窄 industry 或人工处理。 * 旧服务端不返回这三个字段。 */ selectionLimit?: number; returned?: number; truncated?: boolean; }; export type DeviceCodeResponse = { device_code: string; user_code: string; verification_uri: string; verification_uri_complete: string; expires_in: number; interval: number; }; export type PollDeviceTokenResponse = { error: "authorization_pending"; } | { error: "slow_down"; } | { error: "expired_token"; } | { error: "access_denied"; } | { access_token: string; token_type: "Bearer"; expires_at: string; team_id: string; team_ids?: string[]; duration_kind: "short" | "long" | "custom" | "never"; downgraded?: boolean; }; export type AuthContextResponse = { kind: "api_key" | "session" | "cli_token"; teamId: string; role: string; visibleTeamIds: string[]; visibleBrandIds: string[]; visibleCampaignIds: string[] | null; visibleAccountIds: string[]; userId?: string; email?: string; name?: string; }; export type UpdateCampaignBody = { name?: string; startsAt?: string | null; endsAt?: string | null; }; export type UpdateContentDraftBody = { title?: string; body?: string; status?: string; /** * 目标板块(迁移 0136)。省略 = 不改;`null` = 清空;字符串 = 设置(服务端规范化)。 * 属内容身份 → **会** bump 草稿版本(旧内部审核 / 客户批准因此失效)。 */ subreddit?: string | null; /** * 写作参考 / 归因备注 URL(迁移 0136,原前端字段名 `targetUrl`)。 * ⚠️ **不参与发布执行** —— 系统没有 Reddit link post 概念,执行只冻结 title+body。 * 不属内容身份 → 单独改它**不** bump 版本、不使既有审核/批准失效。 */ referenceUrl?: string | null; /** Optimistic concurrency: last-loaded version; a mismatch is rejected 409. */ expectedVersion?: number; }; /** * 本次审核实际采用的**作者关系**(说话人是谁),与 `commentType`(结构位置)正交。 * * ⚠️ 一律**服务端派生**。调用方给不了这个值,只能给 {@link ContentReviewAuthorRelationHint}; * Hub 按「实际发布账号 handle × 原帖 permalink × Hub 侧帖子快照作者」比对得出。 */ export type ContentReviewAuthorRelation = "post_author" | "other" | "unknown"; /** * 调用方可给的**非权威提示**。它不是身份声明,也不会被回显为审核结论: * 证据永远覆盖 hint(声称 `other` 但证据显示是作者 → 仍派生 `post_author`); * 声称 `post_author` 而证据证伪 → 422 `AUTHOR_RELATION_MISMATCH`(见 * {@link isAuthorRelationMismatchError})。 */ export type ContentReviewAuthorRelationHint = "post_author" | "other"; /** * 派生依据。`verified_snapshot`=比对了 Hub 侧帖子快照;`verified_live`=预留(未实现); * `mismatch`=显式声称 post_author 但证据证伪(同步 API 走 422,成功响应通常见不到); * `unavailable`=无账号/无 permalink/无快照/作者已删/快照与本次 thread 绑定校验不过 → * `authorRelation=unknown`,按普通基线审核(既不 block 也不给任何 post_author 校准)。 */ export type ContentReviewAuthorRelationEvidence = "verified_snapshot" | "verified_live" | "mismatch" | "unavailable"; export type ContentReviewThreadInput = { subreddit: string; postTitle: string; postBody?: string; /** `commentType: "reply"` 时必填。 */ parentComment?: string; topComments?: string[]; subredditToneHint?: string; /** * 所审原帖 permalink —— 作者关系轴的**唯一校验入口**(不给 → `authorRelation=unknown`)。 * 服务端 canonicalize 后查帖子快照取作者,并校验快照 subreddit/title 与本次 thread 一致。 * SDK 不做 URL 校验,原样透传。 */ threadPermalink?: string; }; export type ContentReviewRequestInput = { /** 账号 UUID 或 handle;省略=persona-less(ContentOnly)审核。 */ accountId?: string; /** 仅一致性校验;无 accountId 时禁止携带。 */ personaId?: string; brandContext?: Record; draft: string; /** 结构位置(调用方声明);与作者关系是两根正交的轴。 */ commentType: "top_level" | "reply"; /** 非权威提示;服务端证据永远覆盖它。见 {@link ContentReviewAuthorRelationHint}。 */ authorRelationHint?: ContentReviewAuthorRelationHint; /** 账号阶段(影响长度/克制度与阈值收紧);不确定就省略,不要传任意字符串。 */ stage?: "静默期" | "互动期" | "活跃期"; thread: ContentReviewThreadInput; rewrite?: boolean; /** 第几次送审;服务端强制上限 2(防重写循环)。 */ attempt?: 1 | 2; traceId?: string; } & ReviewScopeDeclaration; /** * ── Aporro Native Copy:四象限声明 / 逐稿 Card / acceptance 回执(手抄镜像)── * * 🔴 SDK **不能** re-export contracts(contracts 是 `private: true`,直接引用会生成 * 消费者装不上的 `.d.ts`),所以这组类型是手抄的,真源在 * `packages/contracts/src/review-scope.ts` 与 `knowledge-boundary.ts`。 * 手抄就会漂移 —— `index.test.ts` 里有对应的 `expectTypeOf` 编译期锁。 * * 线上请求体一直是**原样透传**的(服务端 zod 校验);补这组类型之前,TS 调用方 * 传这几个字段会在编译期就被 SDK 自己的类型拦下 —— 能力在服务端,SDK 这层把门关上了。 */ export type ReviewAccountBinding = "account_bound" | "persona_less"; export type ReviewBrandTreatment = "managed_brand" | "organic"; export type ReviewScopeSource = "declared" | "legacy_derived"; export type ReviewBrandResolutionStatus = "resolved" | "none" | "unresolved"; export type ReviewAcceptanceProfile = "default" | "native_copy"; /** 调用方**声明**的象限(意图,不是事实)。 */ export type ReviewRequestedScope = { accountBinding: ReviewAccountBinding; brandTreatment: ReviewBrandTreatment; }; /** 服务端解析后的权威象限 + 可用性事实。 */ export type ReviewResolvedScope = { accountBinding: ReviewAccountBinding; brandTreatment: ReviewBrandTreatment; /** ⚠️ `legacy_derived` 不满足 Native Copy profile,也不满足发布 gate。 */ scopeSource: ReviewScopeSource; /** `unresolved` 是覆盖态:声明 organic 却识别到已登记品牌 → 转人工,不豁免。 */ brandResolutionStatus: ReviewBrandResolutionStatus; brandResolutionDetail: { detectedBrandId: string | null; detectedBrandKey: string | null; reason: string; resolverVersion: string; } | null; personaVoiceEvidenceAvailable: boolean; knowledgeContextAvailable: boolean; rulesEvidenceAvailable: boolean; styleProfileAvailable: boolean; }; /** * 逐稿知识边界(Persona Card)。服务端内容寻址后固化,worker 发布前复审时完整恢复。 * ⚠️ 当前 **不影响 verdict**:`knowledge_boundary_consistency` 维度尚未激活 * (方案块 2a 口径下的 v5 原子提交),本字段现阶段只留痕、只决定 Gate 0 是否齐备。 */ export type KnowledgeBoundaryContextInput = { schemaVersion: 1; knowledgeAccess: "research" | "owned" | "used" | "observed" | "supplied_by_brand"; /** 最多 4 项。 */ permittedExperienceClaims: Array<"ownership" | "delivery" | "wear" | "service">; postingWhy: string; knows: string[]; doesNotKnow: string[]; attentionFocus?: string; questionIntent?: string; permittedFactVersionIds?: string[]; archetypeLabels?: string[]; }; export type KnowledgeContextRef = { id: string; hash: string; schemaVersion: number; }; /** 本次生效的 acceptance profile(可能被降级,原因在 downgradedReason)。 */ export type ResolvedAcceptanceProfile = { profile: ReviewAcceptanceProfile; policyVersion: string; /** 非 null = 请求了 native_copy 但被降级(`legacy_derived_scope` / `incomplete_gate0`)。 */ downgradedReason: string | null; }; export type ReviewProfileVerdictDetail = { profile: ReviewAcceptanceProfile; policyVersion: string; verdict: "pass" | "revise" | "block" | "required_missing" | "not_applicable"; findings: Array<{ rule: string; dimension: string | null; threshold: number | null; actual: number | null; detail: string | null; }>; missingGateItems: string[]; }; /** * 审核响应里的 Aporro 回执(帖/评共用)。全部可选:旧 pod / 旧 engine 下服务端会 * fail-closed **省略**它们,调用方应把「字段不在」读成「没有这项能力」,⛔ 不是「通过」。 */ export type ReviewScopeReceipt = { resolvedScope?: ReviewResolvedScope; capabilities?: string[]; knowledgeContextRef?: KnowledgeContextRef | null; resolvedAcceptanceProfile?: ResolvedAcceptanceProfile; profileVerdictDetail?: ReviewProfileVerdictDetail | null; }; /** 审核请求里的 Aporro 声明字段(帖/评共用)。 */ export type ReviewScopeDeclaration = { requestedScope?: ReviewRequestedScope; /** * ⚠️ 两类入口约束**不同**(Codex 块 5 P1 纠正了我原来写的「需同时声明」): * - 同步 `postReview` / `contentReview`:三个字段各自可选,不要求成对;只传 Card * 不传 requestedScope 时服务端照收,按 `legacy_derived` 象限审(不满足 native_copy)。 * - 异步 `createReviewTarget`:Card 与 requestedAcceptanceProfile **必须**伴随 * requestedScope,否则 400 —— 因为只有声明分支会被固化,不拦就是「接受后丢数据」。 */ knowledgeContext?: KnowledgeBoundaryContextInput; requestedAcceptanceProfile?: ReviewAcceptanceProfile; }; export type ContentReviewResponse = { reviewId: string; executionStatus: "completed" | "degraded"; verdict: "pass" | "revise" | "block"; /** * 服务端派生的权威作者关系。**绝不是**调用方 `authorRelationHint` 的回显 —— * 回显会让「自称 OP」看起来像被系统承认。 */ authorRelation: ContentReviewAuthorRelation; authorRelationEvidence: ContentReviewAuthorRelationEvidence; score: number; modelScore: number | null; dimensions: Record | null; optimized: string | null; rationale: string | null; reviewModel: string; rubricVersion: string; thresholdSnapshot: Record; latencyMs: number; degradedReason: string | null; } & ReviewScopeReceipt; /** 审核目标状态(mirrors contracts REVIEW_TARGET_STATUSES;SDK 不 hard-dep contracts)。 */ export type ReviewTargetStatusLike = "pending" | "running" | "passed" | "needs_revision" | "blocked" | "degraded" | "superseded"; /** * 审核**记录**列表参数(账本,append-only)—— 与 {@link ListReviewTargetsParams} * (等人审的队列)是两回事,别混。 */ export type ListReviewRecordsParams = { limit?: number; offset?: number; /** * 判定过滤,逗号拼后传服务端;空数组=不过滤。 * ⚠️ 刻意是 `string[]` 而不是联合字面量:`verdict` 是无约束的自由文本列, * 收窄成已知几档会让「筛选一个存量里真实存在但没登记的取值」在客户端就被挡掉。 */ verdict?: string[]; /** 帖/评共表的类型判别(`post` 或评论类取值)。 */ commentType?: string; socialAccountId?: string; rubricVersion?: string; /** 只看降级产出的判定;`false` 与缺省一样表示**不加条件**,不是「只看未降级」。 */ degradedOnly?: boolean; /** 起始时间;Date 或 ISO 字符串。 */ since?: Date | string; }; export type ListReviewTargetsParams = { limit?: number; offset?: number; /** 逗号拼后传服务端;空数组=不过滤。 */ status?: ReviewTargetStatusLike[]; contentType?: "post" | "comment"; }; /** 建审核目标 body(mirrors contracts createReviewTargetBodySchema;宽松结构由服务端 zod 校验)。 */ export type CreateReviewTargetBody = { sourceKind: "content_draft" | "comment_draft" | "calendar_entry" | "scheduled_job" | "post_performance" | "external_delegated"; contentType: "post" | "comment"; deliveryMode: "internal_publish" | "external_delegated"; reviewLevel: "draft" | "target"; contentDraftId?: string; commentDraftId?: string; calendarEntryId?: string; scheduledJobId?: string; postSnapshotId?: string; externalRef?: string; socialAccountId?: string; subreddit?: string; brandId?: string; contentVersion: number; contentHash: string; dedupeKey: string; subject: { title?: string; body: string; /** ⚠️ Gate 0 的版规证据读的是这里的 `subredditRules`(worker 重建请求也读这个位置)。 */ threadContext?: Record; subreddit?: string; }; } & ReviewScopeDeclaration; export type CommentDraftStatusLike = "draft" | "scheduled" | "approved" | "archived" | "published"; /** * 评论审核语境快照(mirrors contracts commentDraftThreadContextSchema;严格对象—— * 服务端 `.strict()` 拒未知键)。subredditRules 由服务端注入,勿从客户端提交。 */ export type CommentDraftThreadContext = { postTitle?: string; postBody?: string; /** 父评论正文快照(回复场景)。 */ parentComment?: string; /** 线程头部评论样本(有序)。 */ topComments?: string[]; subredditToneHint?: string; commentType?: "reply" | "top_level"; }; export type ListCommentDraftsParams = { limit?: number; offset?: number; /** 逗号拼后传服务端;空数组=不过滤。 */ brandId?: string[]; campaignId?: string; status?: CommentDraftStatusLike[]; subreddit?: string; search?: string; /** 聚合可见团队(跨 team 列表);默认单团队。 */ aggregateVisibleTeams?: boolean; /** 把聚合收窄到指定可见团队(逗号拼);空数组=不收窄。 */ scopeTeamIds?: string[]; }; /** * R12(迁移 0153)——「**显式**声明这条内容不做品牌提及」。 * * 只有 `"none"` 一个值,对应契约的 `brandMentionDeclarationSchema` * (`packages/contracts/src/brand-mention-declaration.ts`)。 * * 🔴 **缺失 ≠ 无意图**:省略 `brandId` 而不带这个声明,服务端按「忘了传」判 400, * 不会静默降级成无品牌。判定规则全在服务端(`refineBrandMentionDeclaration` + * 路由的四值比较 + db 层兜底),**SDK 只表达类型、不复刻校验** —— 复刻必然两边漂移。 */ export type BrandMentionDeclaration = "none"; /** * `POST /content-drafts` 的 body。 * * ⚠️ `brandId` 与 `brandMention` 的互斥/必填关系**故意不用类型联合表达**: * 服务端的 `superRefine` 是唯一判定处,在 SDK 里再写一遍会漂移(见 * {@link BrandMentionDeclaration})。类型只负责让「无品牌草稿」可以被表达出来。 */ export type CreateContentDraftBody = { /** R12:可选。省略时**必须**同时显式给 `brandMention: "none"`(服务端校验)。 */ brandId?: string; /** R12:显式声明本草稿不做品牌提及。见 {@link BrandMentionDeclaration}。 */ brandMention?: BrandMentionDeclaration; /** @deprecated legacy 归因;新草稿只用 brandId。 */ campaignId?: string; title: string; body: string; /** 目标板块(内容语义真源);服务端规范化后落库。省略/null → NULL。 */ subreddit?: string | null; /** * 写作参考 / 归因备注 URL:「这篇内容围绕哪个落地页写的」。 * ⚠️ **不参与发布执行** —— 系统没有 Reddit link post 概念,执行只冻结 title+body。 */ referenceUrl?: string | null; /** * R2:**创建幂等键**(可选)。同一个 `(team, sourceRef)` 重复创建会复用首次那篇, * 返回 `{ reused: true }` 而不是再造一篇。 * * 用它包住会重试的调用(cron 重跑、超时重发、回执丢失后补发)。key 由调用方拥有, * 服务端只做 NFKC + trim + 长度 1..200 + 拒控制字符,**不限制字符集** —— * `agent:reports/2026-08.json#3` 这类带 `#` 的键是合法的。 * * 🔴 唯一例外:**`openclaw:` 前缀是导入器专用的保留命名空间,写入面直接 400** * (导入器有权用源文件覆盖同 key 的草稿;你占用它,你的草稿会在下次 ingest 被覆盖)。 * 换个自己的前缀即可。按 `openclaw:` 反查**不受限制**。 * * ⚠️ **同 key 换内容 = 409**(`CONTENT_DRAFT_SOURCE_REF_CONFLICT`),不是静默覆盖。 * 改稿走 `PATCH`,或换一个新 key。删除会释放 key。 */ sourceRef?: string; }; /** `POST /publishing-plans` 里的单条待排期条目。 */ export type CreatePublishingPlanEntryInput = { contentDraftId?: string; socialAccountId: string; subreddit: string; /** ISO-8601 datetime。 */ plannedAt: string; /** 缺省 = `hub_managed`。见 {@link CalendarEntryDeliveryMode}。 */ deliveryMode?: CalendarEntryDeliveryMode; }; /** * `POST /publishing-plans` 的 body(此前是 `Record`,运行时不挡、 * 类型也表达不出 R12 的无品牌计划)。 * * ⚠️ 与 {@link CreateContentDraftBody} 同理:brandId / brandMention 的四条 fail-closed * 规则**只在服务端**判,SDK 不复刻。 */ export type CreatePublishingPlanBody = { /** R12:可选。省略时**必须**同时显式给 `brandMention: "none"`(服务端校验)。 */ brandId?: string; /** R12:显式声明本计划不做品牌提及。见 {@link BrandMentionDeclaration}。 */ brandMention?: BrandMentionDeclaration; /** @deprecated legacy 归因。 */ campaignId?: string; name: string; entries: CreatePublishingPlanEntryInput[]; }; export type CreateCommentDraftBody = { brandId: string; campaignId?: string; body: string; subreddit: string; threadPermalink?: string; parentCommentId?: string; threadContext?: CommentDraftThreadContext; }; export type UpdateCommentDraftBody = { body?: string; subreddit?: string; /** null = 清除(回到未选帖)。 */ threadPermalink?: string | null; parentCommentId?: string | null; threadContext?: CommentDraftThreadContext | null; /** 仅人工态;scheduled/published 由系统驱动,不可客户端直设。 */ status?: "draft" | "approved" | "archived"; /** Optimistic concurrency: last-loaded content version; mismatch → 409. */ expectedVersion?: number; }; export type ScheduleCommentDraftBody = { socialAccountId: string; /** ISO datetime。 */ runAt: string; windowMinutes?: number; }; export type DeliveryBundleStatusLike = "open" | "closed" | "cancelled"; /** 客户面列 bundle:brandId 服务端**必填**(客户按 brand 收窄);SDK 类型强制。 */ /** * `cursor` 与 `offset` **互斥**,用可辨识联合在**编译期**就拒掉错误组合 —— 服务端也会 * 400,但把它留到运行时等于让调用方先发一次注定失败的请求。 */ export type ListClientDeliveryBundlesParams = { brandId: string; status?: DeliveryBundleStatusLike; limit?: number; } & ({ /** * keyset 游标 —— 上一次响应的 `nextCursor` 原样回传。对调用方是**不透明字符串**: * 不要解析或构造,也不要换了 brandId/status 之后复用(游标绑定过滤条件指纹,会 400)。 */ cursor: string; offset?: never; } | { cursor?: undefined; /** * @deprecated 用 `cursor`。offset 分页在并发写入下会重复/跳条(服务端已改 keyset)。 */ offset?: number; }); /** staff 建 bundle。 */ export type CreateClientDeliveryBundleBody = { brandId: string; bundleCode: string; name: string; clientDueAt?: string | null; }; /** staff upsert item(帖/评 + 矩阵规划字段)。 */ export type UpsertClientDeliveryItemBody = { kind: "post" | "comment"; itemCode: string; contentDraftId?: string | null; commentDraftId?: string | null; ordinal?: number; boundProductId?: string | null; primaryPostType?: string[] | null; writingVariant?: string | null; structureFamily?: string | null; contentFunction?: string | null; functionDetail?: string | null; brandEntryMode?: string | null; anchorSlot?: string | null; mappingRuleId?: string | null; vocabularyVersion?: string | null; }; /** staff 送审 revision(note 可选)。 */ export type SubmitClientDeliveryRevisionBody = { note?: string; }; export type ClientDeliveryInventoryStatus = "in_stock" | "low_stock" | "out_of_stock" | "preorder" | "discontinued" | "unknown"; /** 产品事实版本 body(客户面:正文/draft 字段物理不接受;仅事实)。 */ export type ClientDeliveryFactVersionBody = { positioning?: string | null; materials?: string | null; priceAmount?: string | null; priceCurrency?: string | null; inventoryStatus?: ClientDeliveryInventoryStatus | null; inventoryAsOf?: string | null; shippingDaysMin?: number | null; shippingDaysMax?: number | null; factsJson?: Record; effectiveAt?: string | null; expiresAt?: string | null; sourceRef?: string | null; }; /** staff 事实版本 body:额外可指定 provenance(默认 staff)。 */ export type StaffClientDeliveryFactVersionBody = ClientDeliveryFactVersionBody & { provenance?: "client" | "staff" | "import"; }; /** staff 建产品身份。 */ export type CreateClientDeliveryProductBody = { brandId: string; sku: string; brandRegistryId?: string | null; modelName?: string | null; market?: string | null; status?: string; }; /** staff 建品牌内容政策版本。 */ export type CreateClientDeliveryPolicyVersionBody = { brandId: string; market?: string | null; provenance?: "client" | "staff" | "import"; policyJson?: Record; effectiveAt?: string | null; }; /** 字段级评论(request-changes 内嵌 / 独立讨论)。 */ export type ClientDeliveryDecisionComment = { fieldKey: string; quotedText?: string; body: string; }; /** 客户批准(CAS:expectedRevisionHash)。 */ export type ApproveClientDeliveryRevisionBody = { expectedRevisionHash: string; note?: string; }; /** 客户请求修改(CAS + 字段级评论,至少一条)。 */ export type RequestChangesClientDeliveryRevisionBody = { expectedRevisionHash: string; comments: ClientDeliveryDecisionComment[]; note?: string; }; /** 客户字段级讨论评论。 */ export type AddClientDeliveryCommentBody = { fieldKey: string; quotedText?: string; body: string; }; export type UpdateNotificationChannelBody = { provider?: string; externalId?: string; minRole?: string; campaignId?: string | null; }; export type ListSubredditWatchlistsParams = { limit?: number; offset?: number; campaignId?: string; /** 按板块名精确查归属(r/ 前缀可省,大小写不敏感)。 */ subreddit?: string; /** 板块名**子串**搜索(大小写不敏感,r/ 可省);与 `subreddit` 是两个独立谓词。 */ subredditQuery?: string; /** ⚠️ 2026-09-09 之前本 SDK 方法**漏传**了服务端早就支持的 status/brandId/industryKeys。 */ status?: "active" | "paused" | "archived"; brandId?: string; /** 逗号分隔的行业 key 列表(服务端自己 split)。 */ industryKeys?: string; tag?: string; /** * 新帖监听状态。软停语义:`disabled` = 停过(配置行还在),`never` = 从没加过。 * ⚠️ 受限主体(client)传这个会 403 —— 监听是系统级运维配置。 */ monitorState?: "enabled" | "disabled" | "never"; /** ⚠️ `monitorLastObservedAt` 与 `monitorState` 同一道权限门。 */ sortBy?: "createdAt" | "subreddit" | "monitorLastObservedAt"; sortDir?: "asc" | "desc"; }; export type ListSubredditMonitorsParams = { limit?: number; offset?: number; /** 缺省 = 全部(含已停止的);服务端只认 "true"/"false" 字符串,方法内负责转换。 */ enabled?: boolean; watchlistId?: string; }; /** * 监听配置行(POST 启用的返回)。时间字段一律 `string`(HTTP JSON 里是 ISO 串, * SDK 不代为 `new Date()`,免得调用方拿到「有时 Date 有时 string」的混合类型)。 */ export type SubredditMonitorConfig = { id: string; watchlistId: string; subreddit: string; enabled: boolean; sort: string; fetchLimit: number; createdAt: string; updatedAt: string; /** 当前生效的关键词规则集(0168);null = **不设关键词门**,报告放行全部新帖。 */ activeRuleSetId?: string | null; activeRuleSetVersion?: number | null; /** 该监听的回看窗口(分钟)。无 active 规则集 → 720(12h)。 */ lookbackMinutes?: number; }; export type SubredditMonitorRuleScope = "title" | "body" | "both"; /** * `word` 带词边界(🔴 `art` 不会命中 `earth`);`substring` 明确放弃边界; * `phrase` 词序固定、中间允许任意标点换行。 * ⚠️ CJK 没有词边界概念,`word` 在 CJK 上退化为子串 —— 否则「咖啡」永远匹配不上「咖啡机」。 */ export type SubredditMonitorMatchMode = "word" | "substring" | "phrase"; /** * 规则极性(0171)。 * - `include`:命中它就算相关; * - `exclude`:命中它就**撤销**同等或更低来源优先级的 include 命中 * (优先级 manual > brand_keyword > expansion —— 模型扩出来的否定词不该把运营 * 亲手填的 include 词否掉)。 * * ⚠️ exclude 规则**自己不产生命中**。一条 include 都没有的规则集会被服务端拒绝激活 *(409)—— 那等于把整个板块静音,而卡片上看起来只是「今天没有相关新帖」。 * ⚠️ 同一个 (term, scope, matchMode) 上**可以**同时存在 include 与 exclude 两条: *「espresso 收进来、espresso martini 排出去」正是这个形状。 */ export type SubredditMonitorRulePolarity = "include" | "exclude"; export type SubredditMonitorKeywordRuleInput = { /** 原样字面值;服务端归一化(NFKC / 去重音 / 小写)后才进唯一键。 */ term: string; scope?: SubredditMonitorRuleScope; matchMode?: SubredditMonitorMatchMode; /** 省略 = `include`。见 {@link SubredditMonitorRulePolarity}。 */ polarity?: SubredditMonitorRulePolarity; /** * 影响卡片里命中词的展示顺序;对 exclude 规则则决定「因为 X 被排除」里 X 的排序。 * ⚠️ 它**不是**打分阈值:命中与否是布尔判定,不存在「权重不够就不算命中」。 */ weight?: number; /** 品牌词导入的**来源溯源**;⛔ 不建立同步语义。 */ sourceBrandKeywordId?: string | null; sourceBrandKeywordValue?: string | null; }; export type SubredditMonitorKeywordRule = Required> & { id: string; ruleSetId: string; normalizedTerm: string; scope: SubredditMonitorRuleScope; /** `include` / `exclude`(0171 起)。见 {@link SubredditMonitorRulePolarity}。 */ polarity: SubredditMonitorRulePolarity; matchMode: SubredditMonitorMatchMode; weight: number; sourceBrandKeywordId: string | null; sourceBrandKeywordValue: string | null; }; /** * 回放的逐帖判定。 * 🔴 `excluded` 与 `no_match` 是**两个不同的值**,⛔ 不得合并: * `excluded` = 命中了 include 但被排除词否掉(去看排除词是不是配宽了); * `no_match` = 一个 include 都没命中(去看 include 词是不是配窄了)。 */ export type SubredditMonitorReplayDecision = "matched" | "indeterminate" | "excluded" | "no_match"; export type SubredditMonitorReplayRun = { id: string; monitorId: string; ruleSetId: string; ruleSetVersion: number | null; ruleHash: string | null; windowStart: string; windowEnd: string; evaluatedCount: number; matchedCount: number; indeterminateCount: number; /** 命中了 include 但被排除词否掉。⛔ 不含在 screenedOutCount 里。 */ excludedCount: number; screenedOutCount: number; /** 撞上硬顶。⚠️ **是布尔不是计数** —— 查询给不出准确的剩余数。 */ truncated: boolean; note: string | null; requestedBy: string | null; createdAt: string; }; export type SubredditMonitorReplayHit = { id: string; canonicalPermalink: string; title: string | null; postedAt: string | null; decision: SubredditMonitorReplayDecision; matchedTerms: string[]; matchedScopes: string[]; totalWeight: number; bodyAvailability: string; /** 造成撤销的排除词。`decision='excluded'` 时非空。 */ excludedByTerms: string[]; /** 正文拿不到 + 有正文域 exclude 规则 ⇒「该不该排除」判不了。 */ exclusionIndeterminate: boolean; /** 当时**真实**账本里的判定;null = 当时没有账本行(见 originalAbsentReason)。 */ originalDecision: "matched" | "indeterminate" | null; originalReportedAt: string | null; originalRuleSetId: string | null; /** * 🔴 `originalDecision === null` 是多义的,这一位把它拆开: * `screened_out` = 当时确实见过、只是判了不命中(账本对不命中一条都不写); * `never_observed` = 当时根本没见过(还没开监听 / VOC 回灌进来的)。 * ⛔ 合并成「原来没报」会让「监听那时还没开」被误读成「规则漏了」。 */ originalAbsentReason: "screened_out" | "never_observed" | null; }; export type SubredditMonitorReplayRunDetail = SubredditMonitorReplayRun & { subreddit: string; ruleSetStatus: "draft" | "active" | "retired"; /** 与真实账本的差分 —— 回放的产品价值就在这里。 */ diff: { /** 当初报过、按新规则不再报的条数。 */ wouldStopReporting: number; /** * 当初被筛掉、按新规则会报的条数。 * ⚠️ 分母**只算** `screened_out`,⛔ 不含 `never_observed` *(那是「监听那时还没开」,不是规则的功劳)。 */ wouldNewlyReport: number; /** 当时根本没被这个监听观察到的条数。⛔ 不是「规则漏了」。 */ neverObserved: number; }; /** 🔴 显式的副作用边界声明:回放没通知任何人、没改账本、没推进水位。 */ sideEffects: { notificationsSent: 0; hitsLedgerWritten: false; reportedWatermarkAdvanced: false; }; }; export type SubredditMonitorRuleSet = { id: string; monitorId: string; version: number; /** 🔴 `active` 之后不可原地修改,只能新建版本。`retired` 是终态。 */ status: "draft" | "active" | "retired"; lookbackMinutes: number; /** 激活时物化的规则指纹;draft 恒为 null。 */ ruleHash: string | null; note: string | null; ruleCount: number; createdBy: string | null; activatedBy: string | null; retiredBy: string | null; createdAt: string; updatedAt: string; activatedAt: string | null; retiredAt: string | null; }; export type SubredditMonitorRuleSetDetail = SubredditMonitorRuleSet & { rules: SubredditMonitorKeywordRule[]; }; /** * 近义词扩展集(0169)。🔴 **固化的**词集 —— approved 之后内容不可原地改。 * * ⛔ 它**不参与运行时匹配**:approved 的词要被显式**物化**(复制)成真实的 * keyword rule 行才会生效。生产匹配路径上没有任何即时展开。 */ export type SubredditMonitorExpansionSet = { id: string; monitorId: string; baseTerm: string; normalizedBaseTerm: string; /** BCP-47 风格;`und` = 未指定。同一原词在不同语言下各批一套。 */ language: string; /** 🔴 固化的扩展词。approved 之后一个字都改不了。 */ expansions: Array<{ term: string; normalizedTerm: string; }>; expansionCount: number; /** 固化内容的指纹;激活规则集时会被折进 `ruleHash` 的内容摘要。 */ contentHash: string; model: string | null; promptVersion: string | null; generatedAt: string | null; /** * 🔴 `proposed` 进不了任何 active 规则集。 * `retired` 有两种来历:从 `proposed` 来是**否决**(`approvedAt` 恒为 null, * 永远进不了 active);从 `approved` 来是**下架**(`approvedAt` 仍在, * 已复制出去的规则行不受影响)。两者只有 `approvedAt` 分得开。 */ status: "proposed" | "approved" | "retired"; approvedBy: string | null; approvedAt: string | null; retiredBy: string | null; retiredAt: string | null; reviewNote: string | null; createdBy: string | null; createdAt: string; updatedAt: string; }; /** * 标签 spec / taxonomy(0169,系统级)。 * ⚠️ `taxonomyVersion` **全局唯一** —— 标签行的唯一键带的是它。 */ export type SubredditMonitorLabelSpec = { id: string; version: number; status: "draft" | "active" | "retired"; taxonomyVersion: string; /** 稳定 key 白名单。模型自创的 key 一律丢弃。 */ labelKeys: string[]; promptVersion: string; model: string | null; temperature: number | null; specHash: string | null; note: string | null; createdBy: string | null; activatedBy: string | null; retiredBy: string | null; createdAt: string; updatedAt: string; activatedAt: string | null; retiredAt: string | null; }; export type SubredditMonitorDryRunResult = { monitorId: string; ruleSetId: string; ruleSetVersion: number; ruleSetStatus: "draft" | "active" | "retired"; subreddit: string; lookbackMinutes: number; /** * 样本量。⚠️ 必须先看它:`sampled === 0` 时「0 命中」说明的是**没有样本** * (板块太冷 / 库里没这段时间的数据),不是「规则太窄」—— 两者在结果列表里长得一样。 */ sampled: number; matched: number; /** 正文拿不到、判不了的条数。**不算不命中**,上线后会照发并在卡片上标注。 */ indeterminate: number; screenedOut: number; /** 逐词命中数:一眼看出哪个词过宽(几乎命中所有帖子)。 */ termHitCounts: Array<{ term: string; count: number; }>; /** 一次都没命中的词:配错/拼错的第一手信号。 */ unusedTerms: string[]; results: Array<{ permalink: string; title: string | null; postedAt: string | null; score: number | null; commentsCount: number | null; decision: "matched" | "indeterminate" | "no_match"; matchedTerms: string[]; matchedFields: Array<"title" | "body">; evidence: Array<{ ruleId: string; term: string; field: "title" | "body"; matchMode: SubredditMonitorMatchMode; weight: number; start: number; end: number; snippet: string; }>; bodyAvailability: "present" | "absent_link_post" | "missing"; indeterminateReason: "body_missing" | null; }>; }; /** * 监听列表的一行 = 配置行 + 三个派生水位字段。 * ⚠️ 这三个字段**只在列表读口有**,POST 启用返回的是裸配置行(见 SubredditMonitorConfig)。 */ export type SubredditMonitorItem = SubredditMonitorConfig & { /** 该监听最近一次观察到帖子的时间;从未观察到 → null。 */ lastObservedAt: string | null; /** 最近一轮 run 中该监听**首次观察**到的帖子数。 */ newPostsInLatestRun: number; /** 已观察但尚未进入任何送达报告的积压数(报告水位落后的信号)。 */ unreportedCount: number; }; export type SubredditMonitorRunStatus = "pending" | "claimed" | "collecting" | "reporting" | "completed" | "degraded" | "failed"; /** run 列表/摘要读口的形状:**不含** fetchDiagnostics / reportPayload 两个大字段。 */ export type SubredditMonitorRunSummary = { id: string; scheduleKey: "cron" | "manual"; windowStart: string; windowEnd: string; status: SubredditMonitorRunStatus; attempts: number; monitorCount: number; succeededMonitorCount: number; failedMonitorCount: number; observedPostCount: number; newPostCount: number; errorCode: string | null; createdAt: string; completedAt: string | null; }; /** run 详情:摘要 + 只有单条读口才投影的 jsonb 大字段与各阶段时间戳。 */ export type SubredditMonitorRunDetail = SubredditMonitorRunSummary & { fetchDiagnostics: Record | null; reportPayload: Record | null; error: string | null; fetchStartedAt: string | null; fetchCompletedAt: string | null; reportCompletedAt: string | null; }; /** run 列表是 **keyset 分页**(无 offset):游标 = 上一页最后一行的 windowStart + id。 */ export type ListSubredditMonitorRunsParams = { limit?: number; status?: SubredditMonitorRunStatus; beforeWindowStart?: string; beforeId?: string; }; export type EnableSubredditMonitorBody = { watchlistId: string; fetchLimit?: number; }; /** * 监听配置更新 body。 * * 🔴 **没有 `enabled`,也不许加**:改配置绝不隐式改启停 —— 启停走 * `enableSubredditMonitor`(启用)与 `disableSubredditMonitor`(软停)两个显式入口。 * 🔴 **没有 `sort`**:DDL 的 CHECK 只允许 `'new'` 一个值,加了就是「改了就 400」的面。 */ export type UpdateSubredditMonitorConfigBody = { /** * 单页抓取条数上限,1–100(与 `EnableSubredditMonitorBody` 同一套边界)。 * * 🔴 **必填,⛔ 不要写成可选。** 服务端 schema 要求「至少给一个可更新字段」, * 而当前可更新字段只有它一个 —— 声明成可选会让 `updateSubredditMonitorConfig(id, {})` * 通过类型检查、运行时必然 400。将来真的加了第二个可更新字段,再把这里 * 改成「二选一」的联合类型。 */ fetchLimit: number; }; export type ListHotPostsParams = { limit?: number; offset?: number; subreddit?: string; sortBy?: "score" | "comments" | "capturedAt" | "upvoteRatio"; }; /** * hot_post 列表每行 enrich 的 content_classification「current proposed」四轴(see contracts * HotPostContentClassificationView)。`null` = 无当前 revision 的成功 run(missing/stale/无 active spec)。 */ export type HotPostClassification = { contentForms: string[]; intent: string; sentiment: string; format: string; }; /** * listHotPosts 返回的单行(除既有 hot_post 字段外,带 classification enrich;宽松索引签名保留兼容)。 * classification 为可选:新 SDK 连未升级的旧服务端时该字段可能缺失(滚动升级安全)。 */ export type HotPostListItem = Record & { id: string; classification?: HotPostClassification | null; }; export type ListKolIntentsParams = { limit?: number; offset?: number; campaignId?: string; status?: "interested" | "contacting" | "connected" | "not_interested"; sortBy?: "createdAt" | "status" | "rating" | "karma" | "activity" | "lastActive"; sortOrder?: "asc" | "desc"; }; export type ListEventsParams = { brandId?: string; limit?: number; type?: string; actor?: string; result?: string; socialAccountId?: string; createdFrom?: string; createdTo?: string; }; export type ListNotificationChannelsParams = { brandId?: string; campaignId?: string; }; export type ListApiKeysParams = { limit?: number; }; export type AccountGraphDrilldownParams = { socialAccountId: string; limit?: number; }; export type ListScheduledJobsParams = { limit?: number; status?: string; socialAccountId?: string; from?: string; to?: string; }; export type ListAccountRiskProfilesParams = { /** 服务端上限 200;缺省 100。 */ limit?: number; /** 翻页起始偏移量(offset,不是 cursor):每次 += limit。 */ offset?: number; search?: string; }; export type ListAccountsParams = { limit?: number; offset?: number; status?: string; platform?: string; brandId?: string; campaignId?: string; personaId?: string; category?: string; search?: string; sort?: "platform" | "team" | "handle" | "externalRef" | "karma" | "accountAgeDays" | "workflowStage" | "status"; dir?: "asc" | "desc"; }; export type ListAuditLogsParams = { limit?: number; type?: string; actor?: "user" | "agent" | "cron"; result?: "succeeded" | "failed" | "skipped"; socialAccountId?: string; createdFrom?: string; createdTo?: string; }; /** `GET /v1/teams/:teamId/permissions-matrix/revisions` 的查询参数(迁移 0128)。 */ export type PermissionsMatrixRevisionsParams = { /** * 只查一张轨迹表。省略 = 两张都查。 * 给了 `action` 隐含 `permission`、给了 `field` 隐含 `field`(服务端会拒绝矛盾组合)。 */ kind?: "permission" | "field"; role?: string; resource?: string; /** 只对 permission 轨迹有意义。不能与 `field` 同时给。 */ action?: string; /** 只对 field 轨迹有意义。不能与 `action` 同时给。 */ field?: string; /** * keyset 游标(排他上界):上一页响应的 `nextPermissionRevisionNo`,**原样回传**。 * * 🔴 **十进制字符串**,不是 number:`revision_no` 是 int8(63 bit),JS number 只有 * 53 bit —— `Number(cursor)` 会在跨过 2^53 后静默失真,把游标指到错误的位置,于是 * 「翻页」悄悄变成「跳过或重复一段审计轨迹」。不要解析、不要拼装,拿到什么传什么。 */ beforePermissionRevisionNo?: string; /** keyset 游标(排他上界):上一页响应的 `nextFieldRevisionNo`,原样回传(十进制字符串)。 */ beforeFieldRevisionNo?: string; /** 1–500,服务端默认 50。 */ limit?: number; }; /** * `GET /v1/teams/:teamId/permissions-matrix/revisions` 的响应。 * * 🔴 结构真源是 `@social-ops-hub/contracts` 的 * `permissionsMatrixRevisionsResponseSchema`。这里**重新声明**而不是 re-export: * SDK 是公开发布包,contracts 是 `private: true` —— 直接引用会生成一份消费者装不上的 * `.d.ts`。改契约时**两处一起改**。 * * ⚠️ 五个顶层字段全部必填:两个数组恒为数组(`kind=field` 时另一张是**空数组**而不是 * 缺字段)、两个游标恒为 `string | null`、`note` 恒在。对新版 API 可以直接用,不必再写 * `res.permissionRevisions ?? []` —— 那种防御会把契约违约读成「没有变更记录」,也就是一个 * 错误的审计结论。**但**需要兼容尚未升级的旧 Hub 时,运行时防御仍然要留(滚动部署期)。 * * ⚠️ 两个 `next*RevisionNo` 是 **continuation token 而不是 has-more 信号**:本页非空 * 就一定有值(哪怕已是最后一页),`null` 只代表本页为空。到底判定 = 再请求一次拿到空数组。 */ export type PermissionOverrideRevisionRowDto = { /** * 🔴 **十进制字符串**(DB 是 identity `bigint`)。需要比较大小/排序用 `BigInt(a) < BigInt(b)`, * **绝不 `Number()` / `parseInt`** —— 超过 2^53 会静默丢精度。 */ revisionNo: string; /** 开放字符串(历史轨迹:registry 改名/删除后旧行仍要可读),不是 enum。 */ role: string; resource: string; action: string; /** 三态:`true` allow / `false` deny / `null` 未设置。 */ oldOverrideAllowed: boolean | null; newOverrideAllowed: boolean | null; changeKind: "insert" | "update" | "delete"; /** api_key / cli_token 主体天然为 null —— 正常状态,不是「未知用户」。 */ actorUserId: string | null; actorKind: string | null; /** 「从哪个团队入口发起」,**不是**「只影响这个团队」:override 存储是全局的。 */ contextTeamId: string | null; requestId: string | null; /** null = 早期记录(写路径未设审计 GUC),无法精确重放当时的 effective 权限。 */ authzPolicyVersion: string | null; changedAt: string; }; export type FieldVisibilityRevisionRowDto = Omit & { field: string; oldOverrideReadable: boolean | null; newOverrideReadable: boolean | null; }; export type PermissionsMatrixRevisionsResponseDto = { permissionRevisions: PermissionOverrideRevisionRowDto[]; fieldRevisions: FieldVisibilityRevisionRowDto[]; /** 十进制字符串游标,原样回传给下一次的 `beforePermissionRevisionNo`。 */ nextPermissionRevisionNo: string | null; nextFieldRevisionNo: string | null; /** 口径提示:轨迹记的是 override 存储值,不是 effective 权限。 */ note: string; }; export type ListAccountGraphParams = { limit?: number; cursor?: string; socialAccountId?: string; edgeType?: string; minRisk?: "high" | "medium" | "low"; }; export type ListCampaignsParams = { brandId?: string; }; export type ListPublishingPlansParams = { brandId?: string; campaignId?: string; /** Derived plan status (aggregated from calendar entries). */ status?: string; }; /** Mirrors contracts subredditSanctionStatusSchema; only "lifted" releases the guardrail. */ export type SubredditSanctionStatus = "warned" | "banned" | "restricted" | "blocked" | "cooldown" | "shadowbanned_suspected" | "lifted"; export type ListSubredditSanctionsParams = { limit?: number; socialAccountId?: string; subreddit?: string; }; export type ListReportsParams = { limit?: number; reportType?: "reddit-post-snapshot" | "account-status-snapshot" | "campaign-digest" | "agent-run-summary" | "style-curation-report" | "quota-violation-report" | "content-weekly"; }; /** `user_feishu_identities.resolve_status`(迁移 0144)。 */ export type FeishuIdentityResolveStatus = "resolved" | "not_found" | "lookup_failed"; /** * 列表过滤值。比 {@link FeishuIdentityResolveStatus} 多一个 `unmapped` —— * 那是「映射表里根本没有这一行」,在库里表示为 NULL 而不是某个状态值。 */ export type FeishuIdentityListStatusFilter = FeishuIdentityResolveStatus | "unmapped"; export type FeishuIdentityListItem = { userId: string; email: string; name: string; userStatus: string; provider: string | null; appId: string | null; /** 未映射为 null;非 admin 读到的是脱敏值(见 `redacted`)。 */ openId: string | null; unionId: string | null; /** `null` = 从未同步过,与 `not_found` 不同。 */ resolveStatus: FeishuIdentityResolveStatus | null; lastAttemptedAt: string | null; lastResolvedAt: string | null; lastErrorCode: string | null; }; export type FeishuIdentityListResponse = { items: FeishuIdentityListItem[]; /** true 表示 `openId`/`unionId` 已脱敏,不可回写。 */ redacted: boolean; summary: { total: number; resolved: number; notFound: number; lookupFailed: number; unmapped: number; }; }; /** * 写入条目。三个状态的字段集**互斥**(与契约的 discriminated union 同源): * 服务端 `.strict()`,多传字段是 400 而不是被忽略。 */ export type FeishuIdentityUpsertEntry = { userId: string; resolveStatus: "resolved"; /** `^ou_[A-Za-z0-9]{1,128}$` —— 会被拼进飞书卡片的 ``。 */ openId: string; unionId?: string | null; } | { userId: string; resolveStatus: "not_found"; } | { userId: string; resolveStatus: "lookup_failed"; /** 必填 `^[a-z0-9_.-]{1,64}$`:没有错误码的失败无法排查。 */ lastErrorCode: string; }; export type FeishuIdentityUpsertResult = { userId: string; requestedStatus: FeishuIdentityResolveStatus; /** 实际落库状态:`lookup_failed` 撞上既有 `resolved` 时这里仍是 `resolved`。 */ storedStatus: FeishuIdentityResolveStatus; mentionable: boolean; }; export type UpsertFeishuIdentitiesResponse = { provider: string; appId: string; results: FeishuIdentityUpsertResult[]; counts: { resolved: number; notFound: number; lookupFailed: number; /** `lookup_failed` 但保住了上次成功解析结果的条数。 */ preservedResolved: number; }; }; /** * 四眼 registry proposal API 的结构化错误(D2):携带 HTTP status + 稳定 code + staleCode。 * apply 的 409 + code PROPOSAL_STALE → isStale=true(消费方据此重新提案,保留 staleCode)。 */ /** * `subreddit-candidates` 的候选耗尽诊断 —— 回答「`items` 为空到底是哪一类空」。 * * ⛔ **这不是准入依据。** 它是诊断快照,可能因并发变更、查询上限或数据不完整而滞后。 * 实际可执行性**只由**服务端硬闸、`items` 与 `allowedActions` 决定。 * (`account_subreddit_affinities` 就曾差点被当成「可去板块」的依据 —— 别再踩一次。) * * 🔴 **`complete: false` 时任何 `all_*` 都不成立**,`reason` 会是 `resolution_incomplete`: * 源集合没被完整枚举时,「没查到」和「查了但都被拦掉」长得一样,而处置方式完全相反。 * * 判「要不要惊动人」的最小用法: * · `all_comment_cooldown` —— 良性,近 24h 已在这些板评论过,**不要**告警; * · `all_action_not_permitted` —— 结构性(系统共享池不作评论目标),**不要**告警; * · `all_sanctioned` / `all_blocklisted` —— 被闸拦光,**要**人工换板; * · `pool_empty` / `persona_unavailable` —— 配置问题,**要**修数据; * · `resolution_incomplete` / `mixed_exhausted` —— 说不清,按要惊动人处理。 */ export type SubredditCandidatesExhaustion = { reason: "not_exhausted" | "resolution_incomplete" | "pool_empty" | "persona_unavailable" | "all_blocklisted" | "all_sanctioned" | "all_tier_filtered" | "all_quota_truncated" | "all_affinity_blocked" | "all_comment_cooldown" | "all_action_not_permitted" | "all_limit_truncated" | "mixed_exhausted"; complete: boolean; incompleteReasons: string[]; eligibleBeforeFilters: number; returnedBoards: number; returned: number; /** 互斥且守恒:`Σ terminal + returnedBoards === eligibleBeforeFilters`。 */ terminal: Record; /** 各闸 raw 命中数,**允许重叠**,仅供排障 —— 不要用它选 reason。 */ filtered: Record; }; export type SubredditPostAnnotationType = "style_fit" | "brand_friendliness"; export type SubredditPostAnnotationSource = "llm" | "human"; /** `subreddit-style-profiles` 的行视图;写操作要回传 `rowVersion` 作 If-Match。 */ export type SubredditStyleProfileView = { id: string; status: "proposed" | "approved" | "retired"; summary: string | null; proposedByUserId: string | null; /** If-Match 的值。状态每次合法迁移必变。 */ rowVersion: string; [key: string]: unknown; }; export type SubredditStyleProfileTransitionInput = { id: string; /** 来自 `getSubredditStyleProfile(id).profile.rowVersion`;**必传**,见方法说明。 */ rowVersion: string; }; /** * If-Match token 归一,**fail-closed**:只接受单个 strong token(可带成对引号)。 * 拒 `W/` 弱 ETag、`*` 通配、多值(含逗号)、引号不成对 —— 非法形状本地即拒, * 不留给服务端猜。与 CLI 的 `normalizeRowVersionToken` 同一口径。 */ export declare function normalizeStyleProfileRowVersion(raw: string | undefined): string | undefined; export declare class RegistryProposalSdkError extends Error { readonly status: number; readonly code?: string; readonly staleCode?: string; constructor(status: number, code: string | undefined, staleCode: string | undefined, message: string); get isStale(): boolean; } /** * Hub API 的结构化 HTTP 错误。 * * `message` 保持与历史一致(`HTTP : `),所以既有 `catch (e) { e.message }` * 的调用方不受影响;新增的 `status` / `code` / `details` 让调用方(尤其是要把服务端错误 * 映射成**退出码**的 CLI)不必 regex 解析 message。 * * `retryAfterSeconds` 取自 `Retry-After` 响应头(仅当其为整数秒时)。 */ export declare class SocialHubHttpError extends Error { readonly status: number; readonly code?: string; readonly details?: unknown; readonly retryAfterSeconds?: number; readonly body?: string; constructor(args: { status: number; message: string; code?: string; details?: unknown; retryAfterSeconds?: number; body?: string; }); } /** * 抓取结果状态。`reauth_required`/`human_required` 属立项 F(授权连接器), * 当前 Hub 永不产出,仅预留以免将来扩枚举变成破坏性变更。 */ export type ScrapeOutcome = "content" | "login_wall" | "captcha" | "blocked" | "timeout" | "rate_limited" | "upstream_error" | "empty_content" | "unknown" | "unsupported" | "cancelled" | "reauth_required" | "human_required"; export type ScrapeFailureStage = "queued" | "dns" | "connect" | "proxy" | "navigate" | "render" | "wait" | "extract" | "parse" | "classify" | "transport" | "unknown"; export type ScrapeClassificationEvidence = { ruleId: string; ruleVersion: number; strength: "strong" | "weak"; location: string; signal: string; excerpt: string | null; }; /** 两层事实:provider 原样事实 + Hub 执行观测。未知一律 null。 */ export type ScrapeDiagnostics = { provider: { provider: "firecrawl" | "reddit_direct"; captureMethod: "firecrawl" /** Hub 既有的 Reddit 直连(curl_cffi + TLS 指纹)通道。 */ | "direct_http" | "official_api" | "authorized_browser"; providerStatusCode: number | null; providerErrorCode: string | null; providerErrorType: string | null; providerRequestId: string | null; providerScrapeId: string | null; targetStatusCode: number | null; providerFinalUrl: string | null; /** `[]` = 确认无重定向;`null` = provider 未提供该事实。 */ redirectChain: string[] | null; }; execution: { attemptCount: number; attemptHistory: Array<{ attempt: number; outcome: ScrapeOutcome; providerStatusCode: number | null; failureStage: ScrapeFailureStage | null; durationMs: number; waitedMs: number; }>; failureStage: ScrapeFailureStage | null; retryAfterMs: number | null; timings: { totalMs: number; providerMs: number; backoffMs: number; classifyMs: number; }; errorSummary: string | null; }; }; type ScrapeResponseCommon = { url: string; finalUrl: string; /** 目标站点状态码(语义未变)。 */ statusCode: number | null; metadata?: Record; requestId: string | null; diagnostics: ScrapeDiagnostics; classification: { outcome: ScrapeOutcome; classifierVersion: string; classificationEvidence: ScrapeClassificationEvidence[]; }; provider: "firecrawl" | "reddit_direct"; providerRequestId: string | null; providerStatusCode: number | null; targetStatusCode: number | null; attempts: number; retryAfterMs: number | null; failureStage: ScrapeFailureStage | null; classifierVersion: string; classificationEvidence: ScrapeClassificationEvidence[]; warnings: string[]; }; /** * `ok` 是判别字段。被判定为「确定不是目标正文」的 outcome(login_wall / captcha / * blocked / rate_limited / upstream_error / empty_content …)运行时不会带正文字段, * 只有 `suppressedContent` 记录被抑制的格式与字符数;`outcome:"unknown"`(证据不足) * 仍可能带正文,但 `warnings` 里会标明它未经确认——**不要**当成已验证的正文使用。 */ export type ScrapeUrlResponse = (ScrapeResponseCommon & { ok: true; outcome: "content"; markdown?: string; html?: string; rawHtml?: string; links?: string[]; json?: unknown; }) | (ScrapeResponseCommon & { ok: false; outcome: Exclude; /** * 被抑制的正文格式与字符数(`outcome` 属于「确定不是正文」那一类时出现)。 * `outcome:"unknown"` 不抑制正文,正文字段仍可能存在——**但它未经确认**。 */ suppressedContent?: Array<{ format: string; chars: number; }>; markdown?: string; html?: string; rawHtml?: string; links?: string[]; json?: unknown; }); /** * POST /content-review 的 422 稳定错误码:调用方显式声称 `authorRelationHint:"post_author"`, * 但 Hub 侧帖子快照证明发布账号不是该帖作者。 * * 这是**硬失败**,不是降级:LLM 一次都不会调用,服务端另落一条安全审计。 * 换一个「能验证的」permalink 重试属于 anchor shopping,不要这么做。 */ export declare const CONTENT_REVIEW_AUTHOR_RELATION_MISMATCH: "AUTHOR_RELATION_MISMATCH"; /** 该错误是否为 {@link CONTENT_REVIEW_AUTHOR_RELATION_MISMATCH}(422 + 稳定 code)。 */ export declare function isAuthorRelationMismatchError(error: unknown): error is SocialHubHttpError; export declare class SocialHubClient { private readonly opts; constructor(opts: SocialHubClientOptions); private teamBase; /** GET /health — no API key required. */ getHealth(): Promise; /** * 绝对 URL 拼接的**唯一**入口:`path` 一律是站内相对路径(如 `/v1/teams/x/events`)。 * 直接把相对路径交给 `fetch()` 在 Node 下会抛 * `TypeError: Failed to parse URL from /v1/...`,所以新增请求方法必须走这里。 */ private buildUrl; /** 鉴权 + 默认 Content-Type;调用方 headers 覆盖默认值(与历史行为一致)。 */ private buildHeaders; /** 共享请求管线:统一 base-url 拼接、鉴权头与错误体解析,返回原始响应文本。 */ private requestText; private fetchJson; /** * 所有 JSON 响应的**唯一**汇合点上做一次截断探测(见 {@link PartialListInfo})。 * * 为什么钩在传输层而不是打印层:CLI 根本没有统一的打印函数 —— index.ts 里有 235 处 * 裸 `console.log(JSON.stringify(...))`,另有 79 处 `printResult`。逐个打补丁正是 * 这个缺陷已经复发四次的原因。而所有命令都必经 `fetchJson`。 */ private notifyIfPartialList; /** 与 `fetchJson` 同一条 base-url/鉴权/错误处理路径,但返回文本(CSV 等非 JSON 响应)。 */ private fetchText; appendEvent(teamId: string, body: { type: string; socialAccountId?: string; /** 品牌关联:账号绑多品牌时必须显式指定;单绑可省略(自动继承)。 */ brandId?: string; actor?: "user" | "agent" | "cron"; targetUrl?: string; subreddit?: string; result?: "succeeded" | "failed" | "skipped"; externalRef?: string; payload?: Record; }): Promise<{ id: string; }>; listEvents(teamId: string, limit?: number): Promise<{ items: { id: string; teamId: string; type: string; socialAccountId: string | null; payload: Record | null; createdAt: string; }[]; }>; getBehavioralHealth(teamId: string, params: { scope?: "environment" | "account"; from: string; to: string; }): Promise<{ scope: "environment" | "account"; window: { from: string; to: string; }; groups: { scopeId: string | null; outcomeCount: number; succeededCount: number; failedCount: number; stoppedCount: number; uncertainCount: number; challengeRate: BehavioralHealthMetric; rateLimitRate: BehavioralHealthMetric; writeUncertainRate: BehavioralHealthMetric; focusFailRate: BehavioralHealthMetric; domFallbackShare: BehavioralHealthMetric; }[]; }>; getDashboardSummary(teamId: string, params?: { interactionTrendDays?: 7 | 14 | 30; campaignId?: string; brandId?: string; from?: string; to?: string; }): Promise<{ metrics?: Record; }>; /** * `GET .../calendar-entries` 的 URL —— {@link SocialHubClient.listCalendarEntries} * 与 {@link SocialHubClient.listCalendarEntriesTyped} 共用。 * * 抽出来是为了让两个方法**不可能**漂移:将来加一个过滤参数只改一处, * 否则 typed 变体会静默少一个过滤条件,表现为「同样的参数却多返回了行」。 */ private calendarEntriesUrl; listCalendarEntries(teamId: string, params?: ListCalendarEntriesParams): Promise<{ items: unknown[]; }>; /** * PATCH 一条日历条目。 * * ## R7 · 执行权排他(只对 `external_agent` 条目) * 协议是四步,**顺序不能换**: * 1. `calendar preflight`(可以两台都拿到 ok,它是纯读); * 2. **抢 `status:"running"`** —— 这一步才是排他。赢家在响应里拿到 * `execution.token`;输的那台拿 **409 `EXECUTION_NOT_ACQUIRED`**, * `details.reason = "execution_already_acquired"`,应当安静退出并删本机 cron; * 3. 提交到 Reddit; * 4. 带 `executionToken` 回写 `succeeded` / `failed`。 * * ⛔ **绝不能先提交再抢 running** —— 那样排他就完全失效了。 * * 🔴 抢占时**务必带 `claimRequestId`**(客户端生成的幂等键)。没有它, * 「claim 已提交但 HTTP 响应丢失」会留下一条永久卡 `running`、 * 却没有任何人握着 token 的条目,只能人工强制终结。带上它重试即可拿回原 token。 * * ⚠️ `execution.token` 是 **bearer 凭据**:写进本机 state 文件,别打日志、别回显。 */ updateCalendarEntry(teamId: string, entryId: string, body: UpdateCalendarEntryBody): Promise<{ ok: boolean; warning?: string; /** 仅在本次真正取得(或幂等重取)执行权时出现。老服务端不会回这个字段。 */ execution?: CalendarEntryExecutionGrant; }>; /** * 与 {@link SocialHubClient.listCalendarEntries} **同一个端点**,只是把返回体 * 标成 {@link CalendarEntryListItemDto}。 * * 为什么另开一个方法而不是收紧旧方法的返回类型:旧方法返回 `unknown[]`,既有调用方 * 普遍在外面自己 `as` 成本地形状;把它改成具名类型会让那些断言从「无操作」变成 * 「类型不兼容」,是一次纯粹为了好看的破坏性变更。新方法零风险,想要 * `deliveryMode` / `directiveRevision` 的调用方直接用它。 * * ⚠️ 运行时**不做校验**(SDK 不引 zod):老服务端不会回这两个字段,所以它们在 * DTO 里是可选的——读到 `undefined` 一律按 `hub_managed` / 未知 revision 处理, * 不要当成 0。 */ listCalendarEntriesTyped(teamId: string, params?: ListCalendarEntriesParams): Promise<{ items: CalendarEntryListItemDto[]; }>; /** * GET .../publishing-calendar-directives/:calendarEntryId/preflight?revision=N * —— 外部 agent 的 cron 到点后、**真正发帖前**必须调的只读校验(方案 §8 D5)。 * * 这是整条链路上正文的**唯一出口**:directive 事件 payload 里只有身份 + 版本号。 * 服务端在同一个只读快照里确认「条目仍 scheduled / 仍 external_agent / revision 仍是 * 你手上那条 / 审核仍 passed」,四条全成立才把内容交出来。 * * ⚠️ **业务拒绝是 HTTP 200 + `decision: "rejected"`,不是 4xx**——本方法不会为 * `rejected` 抛错。调用方必须显式判 `decision`;`if (await preflight(...))` 这种写法 * 会把每一次拒绝都当成放行。4xx/5xx 只留给请求本身或系统的错误。 * * `revision` 必须是调用方手上那条 directive 的版本号,**不能省、不能猜**:服务端 * 拒绝空值与非数字,而 revision 0 是每条条目的合法初始值,任何「默认值」都会放行 * 真实条目。这里先在客户端挡一道,免得把明显非法的值打到网络上。 */ preflightPublishingCalendarDirective(teamId: string, calendarEntryId: string, revision: number): Promise; /** * 归属桶(teams.kind='bucket':未配置/外部)。这些团队没有 membership、不出现 * 在 `listTeams()` 里,所以外部来源帖要回填进桶就只能先从这里拿到 UUID, * 再把它当 `teamId` 传给 createRedditPostSnapshot / batchUpsertRedditPostSnapshots。 */ listRedditPostTeamBuckets(): Promise<{ items: Array<{ id: string; slug: string; name: string; kind: string; }>; }>; listRedditPostSnapshots(teamId: string, params?: ListRedditPostSnapshotsParams): Promise<{ items: unknown[]; total?: number; limit?: number; offset?: number; }>; getRedditPostSnapshotStats(teamId: string, params?: RedditPostSnapshotStatsParams): Promise; getRedditPostRefreshStatus(teamId: string): Promise; getRedditPostRefreshLogs(teamId: string, params?: { limit?: number; }): Promise<{ items: unknown[]; }>; refreshAllRedditPostSnapshots(teamId: string): Promise<{ queued: number; batchId?: string; warning?: string; }>; getRedditRefreshAllProgress(teamId: string, batchId: string): Promise; /** 停止批量刷新:移除排队任务(执行中的由 worker 软取消在写库前拦截)。 */ cancelRedditRefreshAll(teamId: string, batchId: string): Promise<{ batchId: string; status: string; alreadyCancelled: boolean; removed: number; activeKept: number; alreadyDone?: number; missing?: number; warning?: string; }>; refreshRedditPostSnapshot(teamId: string, snapshotId: string): Promise<{ queued: number; warning?: string; }>; /** * 更新单条帖子快照。 * * ⚠️ 把 `opsStatus` 改成 `deleted` / `banned` 时,**必须**同时带上必选项 * `repostRequired`(布尔:是否需补发),否则服务端 400 —— 这是刻意的 fail-closed, * 「需补发」不会默认成 false。「需补发1 / 需补发2」的轮次由服务端按**原帖**的 * `repostType` 派生(首发→1、补发一次→2、补发二次→不问也不写),客户端传数字无效。 * 原帖 `repostType` 已是 `second` 时不需要带,带了也会被忽略。 */ updateRedditPostSnapshot(teamId: string, snapshotId: string, body: Record & { repostRequired?: boolean; }): Promise<{ ok: boolean; }>; deleteRedditPostSnapshot(teamId: string, snapshotId: string): Promise<{ ok: boolean; }>; batchUpsertRedditPostSnapshotsByPermalink(teamId: string, items: Array<{ permalink?: string; feishuRecordId?: string; baseToken?: string; tableId?: string; subreddit?: string; title?: string; postedAt?: string; source?: "manual" | "worker" | "feishu" | "openclaw"; score?: number; commentsCount?: number; viewCount?: number | null; seqNo?: number | null; upvoteRatio?: number | null; brandId?: string; brandRaw?: string | null; campaignId?: string; campaignRaw?: string | null; socialAccountId?: string; primaryProducts?: string[]; relatedPosts?: string[]; publishDoc?: string | null; firstRepostLink?: string | null; secondRepostLink?: string | null; sourceUpdatedAt?: string; feishuAutoNumber?: string | null; feishuSerial?: string | null; feishuNote?: string | null; rawPayload?: Record; body?: string | null; opsStatus?: string | null; draftStatus?: string | null; postType?: string | null; accountType?: string | null; containsBrandKeyword?: boolean | null; }>): Promise<{ imported: number; updated: number; skipped: number; conflicted?: number; }>; /** * 批量改帖快照的字段(含**跨团队迁移**:`patch.teamId`)。 * * ⚠️ 与 `batchUpsert…` 的区别不是「字段多少」而是**语义**: * upsert 的冲突键是 `(teamId, permalink|feishuRecordId)`,往别的团队 upsert * 是**插新行**、原行还在(同一个帖子留下两条快照);本方法是就地改,行只有一份。 * 迁移场景⛔ 不要用 upsert。 * * 🔴 `patch.teamId` 有主体门:`session`,或**不受限的 admin** * (有 userId、非 api_key、未绑 job/账号、无动作白名单),且目标团队必须在 * `visibleTeamIds` 内。api_key 一律 403,`role=admin` 也不行。 * ⚠️ `ids` 上限 500。 */ bulkPatchRedditPostSnapshots(teamId: string, body: { ids: string[]; patch: Record; repostRequired?: boolean; }): Promise<{ updated: number; requested: number; }>; listScheduledJobs(teamId: string, params?: ListScheduledJobsParams): Promise<{ items: unknown[]; }>; getScheduledJobsAutomationSummary(teamId: string, hours?: number): Promise<{ summary: Record; }>; createScheduledJob(teamId: string, body: Record): Promise; createScheduledJobsBatch(teamId: string, body: { jobs: Record[]; }): Promise; retryScheduledJob(teamId: string, jobId: string): Promise; cancelScheduledJob(teamId: string, jobId: string): Promise; listContentDrafts(teamId: string, params?: { limit?: number; offset?: number; brandId?: string; campaignId?: string; status?: string | string[]; /** 目标板块过滤(单值或数组;数组按逗号拼,同字段内 OR)。 */ subreddit?: string | string[]; /** * R2:按创建幂等键反查(单值或数组;数组按逗号拼,同字段内 OR)。 * 迁移期用它把本地 legacy key 映射到 Hub 草稿,不必靠标题去猜。 */ sourceRef?: string | string[]; search?: string; } | number): Promise<{ items: unknown[]; total: number; limit: number; offset: number; }>; /** * `POST /content-drafts`。 * * R2:传了 `body.sourceRef` 时,`reused` 表示这次是**复用**了既有草稿(HTTP 200) * 而不是新建(HTTP 201)。 * * ⚠️ 没传 `sourceRef` 时服务端**不返回** `reused`(响应保持 R2 之前的 `{ id }`), * 所以这里的类型是可选的 —— 别写 `if (!res.reused)` 当作「新建」的判据, * 那在没传 key 时恒真但毫无意义;要判复用请先确认自己传了 key。 */ createContentDraft(teamId: string, body: CreateContentDraftBody): Promise<{ id: string; reused?: boolean; }>; /** * GET /content-review-targets — team-scoped 审核目标列表(status/contentType 过滤 + * limit/offset 分页)。status 传数组时按逗号拼(与 commaSeparatedQueryList 契约一致)。 */ listReviewTargets(teamId: string, params?: ListReviewTargetsParams | number): Promise<{ items: unknown[]; total: number; limit: number; offset: number; }>; /** * GET /content-review-records — team-scoped 审核记录列表 + **概览统计**。 * * 返回体里的 `stats` **不受分页影响**:分页切的是 `items` 这一页,统计的分母是 * 整个筛选集合。⛔ 不要拿 `items` 自己算通过率 —— 那算的是当前页,不是全集。 */ listReviewRecords(teamId: string, params?: ListReviewRecordsParams | number): Promise<{ items: unknown[]; total: number; limit: number; offset: number; stats: unknown; }>; /** GET /content-review-records/:id — 审核记录详情(逐维明细 + 当时的阈值快照)。 */ getReviewRecord(teamId: string, id: string): Promise; /** GET /content-review-targets/:id — 审核目标详情(target + 待审正文 + 若有的 review 摘要)。 */ getReviewTarget(teamId: string, id: string): Promise; /** * POST /content-review-targets — 建 pending 审核目标 + 存待审正文快照(幂等:dedupeKey * 命中且 payload 未漂移返既有 target,HTTP 200;新建 201)。body 为完整 createReviewTargetBody。 */ createReviewTarget(teamId: string, body: CreateReviewTargetBody): Promise<{ target: unknown; }>; /** GET /comment-drafts — team-scoped 评论草稿列表(brand/campaign/status/subreddit/search 过滤 + 分页)。 */ listCommentDrafts(teamId: string, params?: ListCommentDraftsParams | number): Promise<{ items: unknown[]; total: number; limit: number; offset: number; }>; /** GET /comment-drafts/:draftId — 单条评论草稿。 */ getCommentDraft(teamId: string, draftId: string): Promise; /** POST /comment-drafts — 建评论草稿(返回 { id })。 */ createCommentDraft(teamId: string, body: CreateCommentDraftBody): Promise<{ id: string; }>; /** * PATCH /comment-drafts/:draftId — 编辑评论草稿。expectedVersion 开乐观并发(版本不匹配 409); * status 仅接受人工态(draft/approved/archived),scheduled/published 由排期/complete 系统驱动。 */ updateCommentDraft(teamId: string, draftId: string, body: UpdateCommentDraftBody): Promise<{ ok: boolean; version: number; updatedAt: string; }>; /** DELETE /comment-drafts/:draftId — 删评论草稿(204)。 */ deleteCommentDraft(teamId: string, draftId: string): Promise; /** * POST /comment-drafts/:draftId/schedule — 评论草稿排期(唯一 publish_comment 建 job 入口)。 * 一事务:锁 draft + 一稿一投 + 建 job + 建 pending review target + draft→scheduled;过 publish * guardrail。入队失败返 warning(job 仍 scheduled,可被 ops 领)。 */ scheduleCommentDraft(teamId: string, draftId: string, body: ScheduleCommentDraftBody): Promise<{ scheduledJobId: string; /** 同事务建的 pending 审核目标(审核门追踪入口)。 */ reviewTargetId: string; reviewTargetStatus: string; /** BullMQ 入队失败时的告警(job 仍 scheduled,可被 ops 领)。 */ warning?: string; }>; private clientDeliveryBase; /** * GET client-delivery/bundles?brandId=(必填)&status=&limit=&cursor= — 客户面 bundle 列表。 * `nextCursor === null` 表示已到末页;非 null 时原样回传到下次调用的 `cursor`。 */ clientDeliveryListBundles(teamId: string, params: ListClientDeliveryBundlesParams): Promise<{ items: unknown[]; nextCursor?: string | null; }>; /** GET client-delivery/bundles/:id — 单个 bundle(client 视图脱敏 / staff 完整)。 */ clientDeliveryGetBundle(teamId: string, bundleId: string): Promise; /** GET client-delivery/bundles/:id/items — bundle 下条目列表。 */ clientDeliveryListItems(teamId: string, bundleId: string): Promise<{ items: unknown[]; }>; /** GET client-delivery/items/:id — 单个条目。 */ clientDeliveryGetItem(teamId: string, itemId: string): Promise; /** GET client-delivery/items/:id/revisions — 条目下 revision 列表。 */ clientDeliveryListRevisions(teamId: string, itemId: string): Promise<{ items: unknown[]; }>; /** GET client-delivery/revisions/:id — 单个 revision(含 sourceContentHash 作 CAS 凭证)。 */ clientDeliveryGetRevision(teamId: string, revisionId: string): Promise; /** GET client-delivery/revisions/:id/comments — revision 字段级评论列表。 */ clientDeliveryListComments(teamId: string, revisionId: string): Promise<{ items: unknown[]; }>; /** GET client-delivery/revisions/:id/events — revision 决策事件链(append-only)。 */ clientDeliveryListEvents(teamId: string, revisionId: string): Promise<{ items: unknown[]; }>; /** * POST client-delivery/revisions/:id/approve — 客户批准(CAS)。expectedRevisionHash 与服务端 * 当前 revision hash 不符 → 409(SDK 透传为 `HTTP 409:` Error);须 user 凭证(拒 api_key)。 */ clientDeliveryApproveRevision(teamId: string, revisionId: string, body: ApproveClientDeliveryRevisionBody): Promise<{ revision: unknown; }>; /** * POST client-delivery/revisions/:id/request-changes — 客户请求修改 + 字段级评论(CAS)。 * hash 不符 → 409;须 user 凭证。 */ clientDeliveryRequestChanges(teamId: string, revisionId: string, body: RequestChangesClientDeliveryRevisionBody): Promise<{ revision: unknown; }>; /** POST client-delivery/revisions/:id/comments — 客户字段级讨论(非决策);须 user 凭证。 */ clientDeliveryAddComment(teamId: string, revisionId: string, body: AddClientDeliveryCommentBody): Promise<{ comment: unknown; }>; /** POST client-delivery/comments/:id/resolve — 客户关闭讨论(幂等);须 user 凭证。 */ clientDeliveryResolveComment(teamId: string, commentId: string): Promise<{ comment: unknown; }>; /** * POST client-delivery/products/:productId/fact-versions(**客户面**)— 客户提交产品事实版本。 * provenance 服务端强制 'client';正文/draft 字段物理不接受。须 user 凭证。 */ clientDeliveryCreateClientFactVersion(teamId: string, productId: string, body: ClientDeliveryFactVersionBody): Promise<{ factVersion: unknown; }>; /** POST client-delivery/bundles — staff 建 bundle。 */ clientDeliveryCreateBundle(teamId: string, body: CreateClientDeliveryBundleBody): Promise<{ bundle: unknown; }>; /** POST client-delivery/bundles/:id/items — staff upsert 条目(帖/评 + 矩阵字段)。 */ clientDeliveryUpsertItem(teamId: string, bundleId: string, body: UpsertClientDeliveryItemBody): Promise<{ item: unknown; }>; /** POST client-delivery/items/:id/revisions — staff 建不可变送审快照(无 body)。 */ clientDeliveryCreateRevision(teamId: string, itemId: string): Promise<{ revision: unknown; }>; /** POST client-delivery/revisions/:id/submit — staff 送审(not_submitted|withdrawn → pending)。 */ clientDeliverySubmitRevision(teamId: string, revisionId: string, body?: SubmitClientDeliveryRevisionBody): Promise<{ revision: unknown; }>; /** POST client-delivery/products — staff 建产品身份。 */ clientDeliveryCreateProduct(teamId: string, body: CreateClientDeliveryProductBody): Promise<{ product: unknown; }>; /** * POST client-delivery/products/:productId/fact-versions(**staff 面**)— staff 提交事实版本。 * provenance 由 body 决定(默认 staff)。须 user 凭证。 */ clientDeliveryCreateFactVersion(teamId: string, productId: string, body: StaffClientDeliveryFactVersionBody): Promise<{ factVersion: unknown; }>; /** POST client-delivery/products/:productId/fact-versions/:fvId/activate — staff 激活 + stale 传播。 */ clientDeliveryActivateFactVersion(teamId: string, productId: string, factVersionId: string): Promise<{ factVersion: unknown; }>; /** POST client-delivery/policy-versions — staff 建品牌内容政策版本。 */ clientDeliveryCreatePolicyVersion(teamId: string, body: CreateClientDeliveryPolicyVersionBody): Promise<{ policyVersion: unknown; }>; /** POST client-delivery/policy-versions/:id/activate — staff 激活政策版本。 */ clientDeliveryActivatePolicyVersion(teamId: string, policyVersionId: string): Promise<{ policyVersion: unknown; }>; listReports(teamId: string, options?: ListReportsParams | number): Promise<{ items: unknown[]; }>; ingestReport(teamId: string, body: { reportType: "reddit-post-snapshot" | "account-status-snapshot" | "campaign-digest" | "agent-run-summary" | "style-curation-report" | "quota-violation-report" | "content-weekly"; source: string; traceId?: string; }): Promise; listAccounts(teamId: string, params?: ListAccountsParams): Promise<{ items: unknown[]; /** 满足过滤条件的总数(与 limit/offset 无关);翻页终止判据。 */ total?: number; limit?: number; offset?: number; }>; createAccount(teamId: string, body: Record): Promise<{ id: string; }>; listSystemBrands(params?: { status?: string; }): Promise<{ items: unknown[]; }>; createSystemBrand(body: { name: string; slug?: string; /** 内部自建品牌(非合作):Reddit 指标快照剔除其帖子。 */ isInternal?: boolean; }): Promise<{ id: string; }>; updateSystemBrand(brandId: string, body: Record): Promise; deleteSystemBrand(brandId: string): Promise; getBrandKeywords(brandId: string): Promise<{ keywords: { keyword: string; weight?: number; kind?: string; }[]; competitorKeywords: string[]; }>; setBrandKeywords(brandId: string, keywords: { keyword: string; weight?: number; kind?: string; }[]): Promise<{ keywords: { keyword: string; weight?: number; kind?: string; }[]; }>; getBrandCompetitors(brandId: string): Promise<{ competitorBrandIds: string[]; }>; setBrandCompetitors(brandId: string, competitorBrandIds: string[]): Promise<{ competitorBrandIds: string[]; }>; listSystemPersonas(params?: { status?: string; category?: string; brandFit?: string; sourceRef?: string; search?: string; limit?: number; offset?: number; }): Promise<{ items: unknown[]; total?: number; limit?: number; offset?: number; }>; getSystemPersona(personaId: string): Promise>; getPersonaBrandFits(personaId: string): Promise>; /** mode=validate_only(dry-run,返回 snapshot 哈希)/ apply(带 expectedSnapshotHash/RequestHash)。 */ personaBrandFitBulkUpsert(body: Record): Promise>; listBrandRegistry(): Promise>; getBrandRegistry(brandKey: string): Promise>; createBrandRegistry(body: Record): Promise>; addBrandRegistrySystemBrand(brandKey: string, systemBrandId: string): Promise>; addBrandRegistryAlias(brandKey: string, alias: string): Promise>; addBrandRegistryKbSlug(brandKey: string, kbSlug: string): Promise>; retireBrandRegistry(brandKey: string): Promise>; getBrandRegistryAudit(brandKey: string, opts?: { limit?: number; cursor?: string; }): Promise>; /** 结构化请求(proposal 端点):非 ok → RegistryProposalSdkError(带 status/code/staleCode)。 */ private brandFitRequest; proposeRegistryChange(body: { input: Record; idempotencyKey: string; expiresAt?: string; }): Promise<{ proposal: Record; idempotentHit: boolean; }>; approveRegistryProposal(id: string, body?: { approvalComment?: string; }): Promise<{ proposal: Record; }>; rejectRegistryProposal(id: string, body?: { rejectionReason?: string; }): Promise<{ proposal: Record; }>; cancelRegistryProposal(id: string): Promise<{ proposal: Record; }>; /** apply approved proposal。stale(target 已变)→ 409,brandFitRequest 抛 isStale 的 error(保留 staleCode)。 */ applyRegistryProposal(id: string): Promise<{ outcome: "applied"; applyResult: Record; proposal: Record; }>; listRegistryProposals(opts?: { status?: string; }): Promise<{ items: Record[]; }>; getRegistryProposal(id: string): Promise>; listBrandFitTriggers(): Promise<{ items: Record[]; }>; getBrandFitTrigger(key: string): Promise>; listBrandFitAvoidContexts(): Promise<{ items: Record[]; }>; getBrandFitAvoidContext(key: string): Promise>; listBrandFitRuleSignals(): Promise<{ items: Record[]; }>; getBrandFitRuleSignal(key: string): Promise>; createSystemPersona(body: Record): Promise<{ id: string; }>; /** * PATCH a system persona's main record. * * Include `expectedVersion` (the `version` read from getSystemPersona) in * `body` to opt into optimistic concurrency: a stale version yields 409 * PERSONA_STALE (whose details carry the current version). Omit it for * last-writer-wins. On success the response echoes the new `version`. */ updateSystemPersona(personaId: string, body: Record): Promise<{ ok: boolean; version?: number; }>; deleteSystemPersona(personaId: string): Promise; /** * Upsert a persona's default style guide. * * Pass `expectedVersion` (the `version` read from getSystemPersona) to opt * into optimistic concurrency: a stale version yields 409 PERSONA_STALE * (whose details carry the current version). Omit it for last-writer-wins. * On success the response echoes the new `version`. */ setSystemPersonaStyleGuide(personaId: string, defaultStyleGuide: PersonaDefaultStyleGuide | null, opts?: { expectedVersion?: number; }): Promise<{ ok: boolean; version?: number; }>; /** * Upsert a persona's subreddit pools (pass null to clear). * * Pass `expectedVersion` (the `version` read from getSystemPersona) to opt * into optimistic concurrency: a stale version yields 409 PERSONA_STALE * (whose details carry the current version). Omit it for last-writer-wins. * On success the response echoes the new `version`. */ setSystemPersonaSubredditPools(personaId: string, subredditPools: { schemaVersion?: string; starter: string[]; advanced: string[]; pet: "common" | string[]; } | null, opts?: { expectedVersion?: number; }): Promise<{ ok: boolean; version?: number; }>; listBrands(teamId: string): Promise<{ items: unknown[]; }>; createBrand(teamId: string, body: { name: string; slug?: string; }): Promise<{ id: string; }>; updateBrand(teamId: string, brandId: string, body: Record): Promise; listCampaigns(teamId: string, params?: ListCampaignsParams): Promise<{ items: unknown[]; }>; createCampaign(teamId: string, body: { brandId: string; name: string; startsAt?: string; endsAt?: string; }): Promise<{ id: string; }>; listPublishingPlans(teamId: string, params?: ListPublishingPlansParams): Promise<{ items: unknown[]; }>; createPublishingPlan(teamId: string, body: CreatePublishingPlanBody): Promise; listAuditLogs(teamId: string, params?: ListAuditLogsParams): Promise<{ items: unknown[]; }>; getPermissionsMatrix(teamId: string): Promise; /** * 权限 **override** 变更轨迹(迁移 0128)。 * * 记录的是**谁在何时插入 / 修改 / 删除了哪一条**权限或字段可见性 override —— * 不只是「deny 改成 allow」,收紧与删除同样留痕。 * * 🔴 gate 是 `permissionMatrix:**update**`(不是 `read`):能看见「谁改了权限」 * 本身就是治理信息,还会露出 actor user id。 * * ⚠️ 口径:记录的是 **override 存储值**的变更,**不是 effective 权限**的变更 * (effective = 静态 authz 矩阵 ⊕ override ⊕ 硬钳制)。`authzPolicyVersion` 是为了 * 重放时能用**当时**的规则解释**当时**的 override。 * ⚠️ 它**可能为 null**:只有经标准应用写路径(设了审计 GUC)产生的行才带版本号, * **未显式设置该 GUC 的** raw DML / psql 直插留下的行没有 —— 那些行只能看出 override 变了什么, * **无法**可靠还原当时的 effective 权限。 * * 翻页是 keyset:把响应里的 `nextPermissionRevisionNo` / `nextFieldRevisionNo` * **原样**回传成下一次的 `beforePermissionRevisionNo` / `beforeFieldRevisionNo`。 * 它们是 continuation token 而不是 has-more 信号:本页非空就一定有值,要再请求一次 * 拿到空数组才算到底。 * * ⚠️ 服务端对互斥组合是 **400 而不是空结果**:`action` 只属于 permission 轨迹、 * `field` 只属于 field 轨迹,同时给两个、或给了与 `kind` 矛盾的游标都会被拒。 */ listPermissionsMatrixRevisions(teamId: string, params?: PermissionsMatrixRevisionsParams): Promise; listAccountGraphEdges(teamId: string, params?: ListAccountGraphParams): Promise<{ items: unknown[]; nextCursor?: string; }>; runEntityGraphProjection(teamId: string, params?: { dryRun?: boolean; }): Promise<{ teamId: string; runId: string | null; nodes: number; edges: number; skippedEdges: number; dryRun: boolean; }>; createRedditPostSnapshot(teamId: string, body: Record): Promise<{ id: string; }>; appendEventsBatch(teamId: string, body: { events: Record[]; }): Promise; triggerOpenClawIngest(teamId: string, body: { mode: "dry-run" | "apply"; sourceRoot?: string; sourcePaths?: { accountStatus?: string; contentCalendar?: string; verticalRoster?: string; multiAccountTasks?: string; calendarsDir?: string; accountProfilesDir?: string; envMappings?: string; sanctions?: string; contentDraftsDir?: string; sharedDocsDir?: string; interactionQuotasDir?: string; campaignsDir?: string; interactionHistoryFile?: string; opReportsDirs?: string[]; opReportFiles?: string[]; postPerformanceLog?: string; }; idempotencyKey?: string; wait?: boolean; }): Promise; getOpenClawIngestRun(teamId: string, runId: string): Promise<{ ok: boolean; run: unknown; }>; listOpenClawIngestRuns(teamId: string, params?: { status?: string; mode?: string; limit?: number; offset?: number; }): Promise<{ items: unknown[]; total: number; }>; getAccountGuardrails(teamId: string, accountId: string): Promise<{ guardrails: unknown; }>; upsertAccountGuardrails(teamId: string, accountId: string, body: { stage?: string | null; /** Shallow-merged into the stored payload (null clears it). */ payload?: Record | null; syncSource?: string | null; }): Promise<{ guardrails: unknown; }>; listAccountSubredditAffinities(teamId: string, accountId: string): Promise<{ items: unknown[]; }>; upsertAccountSubredditAffinity(teamId: string, accountId: string, subreddit: string, body: Record): Promise<{ item: unknown; }>; appendBrowserEnvironmentTelemetryEvent(teamId: string, envId: string, body: Record): Promise<{ eventId: string; inserted: boolean; }>; listBrowserEnvironmentTelemetryEvents(teamId: string, envId: string, params?: { limit?: number; }): Promise<{ items: unknown[]; }>; listWorkspaceDocumentRefs(teamId: string, documentId: string): Promise<{ items: unknown[]; }>; triggerOpenClawRuntimeMaintenance(teamId: string, body: { jobType: "affinity-recompute" | "ip-drift-evaluate" | "workspace-docs-parse" | "offline-reconcile"; socialAccountId?: string; browserEnvironmentId?: string; documentId?: string; dryRun?: boolean; idempotencyKey?: string; }): Promise; importInteractionHistory(teamId: string, body: Record & { dryRun?: boolean; autoMarkThreshold?: boolean; sourceFile?: string; }): Promise; getInteractionImportBatch(teamId: string, batchId: string): Promise; listStyleMarks(teamId: string, params?: { socialAccountId?: string; extractStatus?: string; approved?: boolean; limit?: number; offset?: number; }): Promise<{ items: unknown[]; total: number; }>; patchStyleMark(teamId: string, styleMarkId: string, body: Record): Promise; createEventStyleMark(teamId: string, eventId: string, body: Record): Promise; upsertEventStyleMarkByExternalRef(teamId: string, eventExternalRef: string, body: { reasonTags?: string[]; styleTags?: string[]; note?: string; markExternalRef?: string; approved?: boolean; onConflict?: "update" | "skip"; }): Promise; getAccountStyleGuide(teamId: string, accountRef: string): Promise; runStyleCurator(teamId: string, accountRef: string, body?: { dryRun?: boolean; limit?: number; }): Promise; createStyleMark(teamId: string, body: { eventExternalRef: string; markExternalRef?: string; socialAccountId?: string; approved?: boolean; reasonTags?: string[]; styleTags?: string[]; note?: string; onConflict?: "update" | "skip"; }): Promise; extractStyleMark(teamId: string, styleMarkId: string, body?: { dryRun?: boolean; }): Promise; listSubredditSanctions(teamId: string, params?: ListSubredditSanctionsParams): Promise<{ items: unknown[]; }>; createSubredditSanction(teamId: string, body: { socialAccountId: string; subreddit: string; status: SubredditSanctionStatus; reason?: string; expiresAt?: string; }): Promise<{ id: string; created: boolean; }>; updateSubredditSanction(teamId: string, id: string, body: { status?: SubredditSanctionStatus; reason?: string; expiresAt?: string | null; }): Promise<{ item: unknown; }>; deleteSubredditSanction(teamId: string, id: string): Promise<{ ok: boolean; }>; /** * 列出团队下账号的风险画像。 * * `limit` 上限 200(contracts `listAccountRiskProfilesQuerySchema`),所以账号数 * 超过 200 的 team 必须用 `offset` 翻页,否则会**静默截断**。第二参数保留 * `number` 形式只为兼容已发布包的老调用方(等价于 `{ limit }`)。 */ listAccountRiskProfiles(teamId: string, paramsOrLimit?: number | ListAccountRiskProfilesParams): Promise<{ items: unknown[]; /** * 满足过滤条件的总数(与 limit/offset 无关)。集合在翻页期间不变时可用作 * 翻页终止判据;offset 分页不是快照分页,并发写入下 total 会漂移, * 同一行也可能重复出现或被跳过。 */ total?: number; limit?: number; offset?: number; }>; getAccountRiskProfile(teamId: string, socialAccountId: string): Promise; upsertAccountRiskProfile(teamId: string, socialAccountId: string, body: Record): Promise; getAccount(teamId: string, accountId: string): Promise; updateAccount(teamId: string, accountId: string, body: { handle?: string | null; profileUrl?: string | null; status?: string; credentialsRef?: string | null; externalId?: string | null; externalRef?: string | null; personaId?: string | null; karma?: number | null; accountAgeDays?: number | null; registeredAt?: string | null; workflowStage?: string | null; metricsSource?: string | null; metricsUpdatedAt?: string | null; primaryProducts?: string[] | null; }): Promise<{ ok: boolean; /** * D-08:这次写入若顺带把账号的 workflow stage 自动推进了(karma 与账号年龄 * 双双达标、且当前阶段在 `warming`/`onboarded` 白名单内),这里带回「从哪个 * 阶段推到哪个阶段、按哪条规则、触发时的 karma 与年龄是多少」。`null` = 没推进。 * * 调用方(互动 cron)靠它写回执,不必回查 `interaction_events`。 */ workflowStageAdvance: { fromStage: string; toStage: string; rule: string; karma: number; accountAgeDays: number; minKarma: number; minAccountAgeDays: number; status: string; } | null; }>; /** * Move an account (and its operating config: browser env, personas, subreddit * affinities, sanctions) from this team to another. Pure history stays; pending * scheduled jobs are cancelled. A target-team collision returns 409. */ reassignAccount(teamId: string, accountId: string, body: { toTeamId: string; }): Promise<{ ok: boolean; }>; getCampaign(teamId: string, campaignId: string): Promise; updateCampaign(teamId: string, campaignId: string, body: UpdateCampaignBody): Promise; getContentDraft(teamId: string, draftId: string): Promise; updateContentDraft(teamId: string, draftId: string, body: UpdateContentDraftBody): Promise; deleteContentDraft(teamId: string, draftId: string): Promise; cancelPublishingPlan(teamId: string, planId: string): Promise; listAgentTeams(): Promise<{ items: unknown[]; }>; listWorkspaceDocuments(teamId: string, params?: { kind?: string; limit?: number; }): Promise<{ items: unknown[]; }>; createTeam(body: { slug: string; name: string; }): Promise<{ id: string; }>; updateTeam(teamId: string, body: { slug?: string; name?: string; }): Promise<{ ok: boolean; }>; addTeamMember(teamId: string, body: { userId: string; role: string; }): Promise<{ id: string; }>; listSubredditEvidence(params?: { subreddit?: string; evidenceType?: string; status?: string; freshOnly?: boolean; limit?: number; offset?: number; }): Promise<{ items: unknown[]; total: number; limit: number; offset: number; }>; getSubredditEvidenceStatus(subreddit: string): Promise<{ subreddit: string; items: unknown[]; }>; dispatchSubredditEvidenceFetch(body: { subreddits?: string[]; source?: "watchlists"; industryKey?: string; evidenceTypes?: string[]; idempotencyKey?: string; }): Promise<{ runId: string; queued: boolean; reused?: boolean; batches?: Array<{ runId: string; queued: boolean; reused: boolean; subreddits: string[]; }>; skippedBatches?: Array<{ subreddits: string[]; warning: string; }>; totalSubreddits?: number; }>; createSopHandoff(body: { phase: string; runId?: string; payload: Record; }): Promise<{ id: string; readyForNextSkill: boolean; missingFields: string[]; }>; listSopHandoffs(params?: { phase?: string; runId?: string; readyOnly?: boolean; limit?: number; offset?: number; }): Promise<{ items: unknown[]; total: number; }>; listRedditCommentSamples(params?: { postId?: string; subreddit?: string; keyword?: string; intent?: string; sentiment?: string; origin?: "hub_collector" | "import"; limit?: number; offset?: number; }): Promise<{ items: unknown[]; /** * ⚠️ 0140 起**不再恒为精确值**,必须连着 `totalRelation` 一起读。 * `null` = 未计算(keyword 路径)。想判断"还有没有下一页"请用 `hasMore`, * 不要写 `offset + limit >= total` —— 封顶/未知时它会误判成没有下一页。 */ total: number | null; /** "eq"=精确;"gte"=实际 ≥ total(封顶 10000);"unavailable"=未计算(total 为 null)。 */ totalRelation: "eq" | "gte" | "unavailable"; /** 权威分页信号:由服务端多取一行得出,与 total 精度无关。 */ hasMore: boolean; }>; /** * 评论样本行的**精确 count**(与 list 同一组过滤谓词,无分页、不封顶)。 * ⚠️ 故意**不接受 keyword**:keyword 走全文 ILIKE,精确 count 要全表扫 * (服务端契约 strict,传了直接 400);keyword 口径请用 listRedditCommentSamples * (totalRelation=unavailable)。 * ⚠️ 无过滤时是全表精确 count,属昂贵读——基线用途,**禁止轮询**。 * 口径是库内 reddit_comment_samples 行数,不是 Reddit 线程真实评论总数。 */ countRedditCommentSamples(params?: { postId?: string; subreddit?: string; intent?: string; sentiment?: string; origin?: "hub_collector" | "import"; }): Promise<{ /** 精确匹配数(不封顶;恒精确,故无 relation 字段)。 */ count: number; /** 采样口径恒为部分样本。 */ sampleMode: "partial_sample"; }>; dispatchRedditCommentCapture(body: { permalinks: string[]; idempotencyKey?: string; }): Promise<{ runId: string; queued: boolean; reused?: boolean; }>; importRedditComments(body: { items: Array>; provenance: { sourceUrl?: string; provider: string; collectorVersion: string; capturedAt?: string; }; }): Promise<{ total: number; accepted: number; inserted: number; updated: number; unchanged: number; rejected: Array<{ index: number; reason: string; }>; }>; importSubredditEvidence(body: { subreddit: string; evidenceType: string; observation: { contentMd?: string; signalsJson?: Record; notVisible?: boolean; visibilityEvidence?: string; }; provenance: { sourceUrl: string; provider: string; collectorVersion: string; capturedAt?: string; }; }): Promise<{ id: string; status: string; deduped: boolean; }>; /** 批量证据回灌:每项 = 单条 importSubredditEvidence 的 body(≤200);逐条独立成败。 */ importSubredditEvidenceBatch(body: { items: Array<{ subreddit: string; evidenceType: string; observation: { contentMd?: string; signalsJson?: Record; notVisible?: boolean; visibilityEvidence?: string; }; provenance: { sourceUrl: string; provider: string; collectorVersion: string; capturedAt?: string; }; }>; }): Promise<{ total: number; imported: number; deduped: number; rejected: number; results: Array<{ index: number; id?: string; status?: string; outcome: "imported" | "deduped" | "rejected"; reason?: string; }>; }>; vocQuery(body: { query: { brandKeywords?: string[]; competitorKeywords?: string[]; subreddits?: string[]; timeWindow?: "day" | "week" | "month"; evidenceTypes?: string[]; }; requirements?: { minPosts?: number; minSubreddits?: number; needComments?: boolean; minComments?: number; needRulesEvidence?: boolean; freshnessHours?: number; }; policy?: { dispatchOnGap?: boolean; }; }): Promise>; getVocCoverage(params: { brandKeywords?: string[]; competitorKeywords?: string[]; subreddits?: string[]; timeWindow?: "day" | "week" | "month"; evidenceTypes?: string[]; minPosts?: number; minSubreddits?: number; needComments?: boolean; minComments?: number; needRulesEvidence?: boolean; freshnessHours?: number; }): Promise>; /** * 板块运营数据 A1:账号汇总薄 MVP——「热帖与采样评论中的高频作者」。窗口固定 * 30 天 captured-window;返回 top-N 作者 + coverage 诊断,品牌关联只标 own/unknown。 * subreddit 先剥 r/ 前缀再 encodeURIComponent(path 只收 bare subreddit)。 */ getSubredditAccountSummary(subreddit: string, params?: { limit?: number; offset?: number; sortBy?: "captured_hot_posts" | "sampled_comments" | "post_score_sum" | "comment_score_sum" | "last_activity"; }): Promise>; listSubredditWatchlists(params?: ListSubredditWatchlistsParams): Promise<{ items: unknown[]; total: number; limit: number; offset: number; }>; createSubredditWatchlist(body: Record): Promise<{ id: string; }>; updateSubredditWatchlist(watchlistId: string, body: Record): Promise<{ ok: boolean; }>; deleteSubredditWatchlist(watchlistId: string): Promise; listSubredditMonitors(params?: ListSubredditMonitorsParams): Promise<{ items: SubredditMonitorItem[]; total: number; /** 最近一轮 run(全局,一轮覆盖所有监听);从未跑过 → null。 */ latestRun: SubredditMonitorRunSummary | null; limit: number; offset: number; }>; /** * 加入监听(**幂等**):同一 watchlist 已停止的会被重新启用,不新建行 —— * observation ledger 因此保留,重启后已报告过的帖子不会重复报告。 */ enableSubredditMonitor(body: EnableSubredditMonitorBody): Promise; /** * **只改监听配置,⛔ 不碰启停。** * * 🔴 与 {@link SocialOpsHubClient.enableSubredditMonitor} 的区别就是全部要点: * 那个 `POST` 是 upsert,`DO UPDATE` 里写死 `enabled = true`,所以在一个**被软停**的 * 监听上用它改 `fetchLimit` 会把监听静默重新启用。本方法背后的 SQL 里不出现 * `enabled` 列,改完启停状态原样不动。 * * ⚠️ **不 upsert**:该 watchlist 没配置过监听 -> 404(不会被悄悄创建成监听)。 */ updateSubredditMonitorConfig(watchlistId: string, body: UpdateSubredditMonitorConfigBody): Promise; /** * 停止监听:DELETE 语义但服务端做**软停**(enabled=false),不删 observation ledger。 * 幂等 —— 本来就没在监听时返回 `{ ok: true, updated: false }`,不是 404。 */ /** * 停止监听。默认是**软停**(`enabled=false`):保留观察 ledger,重新启用后已经报告过的 * 帖子不会被再报一遍。 * * `purge: true` 是另一个动作 —— **连历史观察一起硬删**。只有在要删掉整个 watchlist 时 * 才需要:monitor 配置对 watchlist 的外键是 `ON DELETE RESTRICT`,不先 purge 就删不掉 * watchlist(服务端会返回 409 并提示走这条路)。 */ disableSubredditMonitor(watchlistId: string, options?: { purge?: boolean; }): Promise<{ ok: boolean; updated: boolean; purged?: boolean; }>; /** run 列表:keyset 分页,翻页用上一页最后一行的 windowStart + id 当游标。 */ listSubredditMonitorRuns(params?: ListSubredditMonitorRunsParams): Promise<{ items: SubredditMonitorRunSummary[]; hasMore: boolean; }>; /** run 详情:唯一会带回 fetchDiagnostics / reportPayload 两个大字段的读口。 */ getSubredditMonitorRun(runId: string): Promise; /** 手工触发一轮(202 入队)。没有任何启用中的监听时服务端返回 400,不制造空跑 run。 */ runSubredditMonitors(): Promise<{ status: string; monitors: number; jobId?: string; warning?: string; }>; listSubredditMonitorRuleSets(monitorId: string, params?: { status?: "draft" | "active" | "retired"; limit?: number; }): Promise<{ items: SubredditMonitorRuleSet[]; }>; getSubredditMonitorRuleSet(monitorId: string, ruleSetId: string): Promise; /** 新建 draft 版本(可带初始规则)。**不会自动激活** —— 激活是单独的显式动作。 */ createSubredditMonitorRuleSet(monitorId: string, body?: { /** 有限枚举:720(12h)/ 1440(24h)。run 窗口取所有启用监听中最长的一档。 */ lookbackMinutes?: 720 | 1440; note?: string | null; rules?: SubredditMonitorKeywordRuleInput[]; }): Promise; /** 整批替换 draft 的规则。非 draft → 409(active 版本不可原地改)。 */ replaceSubredditMonitorRules(monitorId: string, ruleSetId: string, body: { rules: SubredditMonitorKeywordRuleInput[]; lookbackMinutes?: 720 | 1440; }): Promise; /** 激活:当前 active 版本自动 retire。空规则集会被拒(400)。 */ activateSubredditMonitorRuleSet(monitorId: string, ruleSetId: string): Promise<{ activated: SubredditMonitorRuleSetDetail; retiredRuleSetId: string | null; }>; runSubredditMonitorReplay(monitorId: string, body: { /** 省略 = 该监听当前 active 的那版。可显式传 draft(主要用法)。 */ ruleSetId?: string; /** ISO 时间串或 Date;窗口按帖子的 postedAt 取,左闭右开,最长 30 天。 */ windowStart: string | Date; windowEnd: string | Date; /** 硬顶(默认 500,最大 2000)。撞顶时结果 `truncated=true`。 */ limit?: number; note?: string | null; }): Promise; listSubredditMonitorReplays(monitorId: string, params?: { limit?: number; }): Promise<{ items: SubredditMonitorReplayRun[]; }>; getSubredditMonitorReplay(monitorId: string, replayRunId: string, params?: { decision?: SubredditMonitorReplayDecision; limit?: number; }): Promise; /** 停用当前版本 ⇒ 该监听回到「不设关键词门」,报告**恢复全量新帖**。 */ retireSubredditMonitorRuleSet(monitorId: string, ruleSetId: string): Promise<{ ok: boolean; retired: boolean; }>; /** 删 draft。active/retired 会被服务端拒绝(历史版本必须留痕)。 */ deleteSubredditMonitorRuleSetDraft(monitorId: string, ruleSetId: string): Promise<{ ok: boolean; deleted: boolean; }>; /** * 🔴 **试跑**:拿该板块过去 lookback 内的历史帖子跑一遍规则,看会命中什么。 * **只读:不写命中账本、不推进水位、不发任何通知。** * 没有它,配关键词就是盲填 —— 要等下一轮 cron 才知道效果,而一个宽词的代价是 * 负责人被刷屏一整天。`termHitCounts` / `unusedTerms` 是判断词过宽/拼错的直接信号。 */ dryRunSubredditMonitorRules(monitorId: string, ruleSetId: string, body?: { lookbackMinutes?: 720 | 1440; limit?: number; /** 默认 false —— 运营需要看到「没命中的长什么样」才能判断词是不是配窄了。 */ matchedOnly?: boolean; }): Promise; /** * 从品牌词取**导入候选**(只读)。 * ⚠️ 语义是**快照复制**,不是订阅:导入后品牌词再变,已激活的规则集不会跟着变 * (响应里的 `semantics` 字段把这一点写死)。要跟进就再导一次并新建版本。 */ listSubredditMonitorBrandKeywordCandidates(monitorId: string, params?: { brandIds?: string[]; limit?: number; }): Promise<{ monitorId: string; watchlistId: string; brandIds: string[]; semantics: "snapshot_copy_not_synced"; items: Array<{ brandKeywordId: string; brandId: string; keyword: string; weight: number; kind: string; }>; }>; listSubredditMonitorExpansionSets(monitorId: string, params?: { status?: "proposed" | "approved" | "retired"; limit?: number; }): Promise<{ items: SubredditMonitorExpansionSet[]; }>; getSubredditMonitorExpansionSet(monitorId: string, expansionSetId: string): Promise; /** 人工录入候选词表(provider 没配时的路径)。产出恒为 `proposed`。 */ createSubredditMonitorExpansionSet(monitorId: string, body: { baseTerm: string; language?: string; terms: string[]; model?: string | null; promptVersion?: string | null; }): Promise; /** * 🔴 **离线生成**候选(走仓内统一 llm-review runner)。产出恒为 `proposed`。 * provider 没配 ⇒ 409(功能未启用,可改用 `createSubredditMonitorExpansionSet` * 手工录词),⛔ 不是 500。 */ generateSubredditMonitorExpansion(monitorId: string, body: { baseTerm: string; language?: string; }): Promise; /** 批准。🔴 批准之后**内容就冻住了**(DB 焊死)—— 这是「固化」的落点。 */ approveSubredditMonitorExpansionSet(monitorId: string, expansionSetId: string, body?: { reviewNote?: string | null; }): Promise<{ ok: boolean; approved: boolean; }>; /** * 否决 / 下架。⚠️ 两者共用一条状态转移但痕迹不同: * `proposed → retired` 是**否决**(此后永远进不了 active 规则集); * `approved → retired` 是**下架**(已复制出去的规则行不受影响 —— 它们拿的是拷贝)。 */ retireSubredditMonitorExpansionSet(monitorId: string, expansionSetId: string, body?: { reviewNote?: string | null; }): Promise<{ ok: boolean; retired: boolean; }>; /** 删候选。approved/retired 会被服务端拒绝(审核过的必须留痕)。 */ deleteSubredditMonitorExpansionSet(monitorId: string, expansionSetId: string): Promise<{ ok: boolean; deleted: boolean; }>; /** * 🔴 **物化**:把 approved 的扩展词复制成 draft 规则集里的真实规则行。 * ⚠️ 物化 ≠ 生效 —— 之后还要显式 `activateSubredditMonitorRuleSet`。 * ⚠️ 点名的扩展集里有未批准的 ⇒ 409(⛔ 不静默少物化几条)。 */ applyExpansionsToRuleSet(monitorId: string, ruleSetId: string, body?: { expansionSetIds?: string[]; }): Promise<{ ruleSetId: string; inserted: number; skippedDuplicate: number; appliedSets: Array<{ expansionSetId: string; baseTerm: string; contentHash: string; terms: number; }>; /** * 🔴 被跳过的扩展集会**显式报出来**(原词不在这个 draft 里)。 * ⛔ 不静默少物化 —— 那会让你以为词已经生效了,而卡片上什么都不会变。 */ skippedSets: Array<{ expansionSetId: string; baseTerm: string; reason: "base_term_not_in_rule_set"; }>; semantics: "materialized_copy_into_draft_not_active"; }>; listSubredditMonitorLabelSpecs(params?: { status?: "draft" | "active" | "retired"; limit?: number; }): Promise<{ items: SubredditMonitorLabelSpec[]; }>; createSubredditMonitorLabelSpec(body: { taxonomyVersion: string; labelKeys: string[]; promptVersion: string; model?: string | null; temperature?: number | null; note?: string | null; }): Promise; activateSubredditMonitorLabelSpec(specId: string): Promise<{ activated: SubredditMonitorLabelSpec; retiredSpecId: string | null; }>; /** ⚠️ 停用 = 标签功能整体关闭(卡片不再显示标签行)。 */ retireSubredditMonitorLabelSpec(specId: string): Promise<{ ok: boolean; retired: boolean; }>; deleteSubredditMonitorLabelSpecDraft(specId: string): Promise<{ ok: boolean; deleted: boolean; }>; listHotPosts(params?: ListHotPostsParams): Promise<{ items: HotPostListItem[]; total: number; limit: number; offset: number; }>; createHotPost(body: Record): Promise<{ id: string; }>; /** 批量热帖回灌(matrix/VOC 本地采集写回;批≤200,逐条拒收报告)。 */ importHotPosts(body: { items: unknown[]; }): Promise<{ createdCount: number; updatedCount: number; unchangedCount: number; rejectedCount: number; importedCount: number; imported: Array<{ index: number; id: string; outcome: "created" | "updated" | "unchanged"; }>; results: Array<{ index: number; id: string; outcome: "created" | "updated" | "unchanged"; }>; rejected: Array<{ index: number; reason: string; }>; }>; exportHotPostsCsv(params?: ListHotPostsParams): Promise; listKolIntents(params?: ListKolIntentsParams): Promise<{ items: unknown[]; total: number; limit: number; offset: number; }>; createKolIntent(body: Record): Promise<{ id: string; }>; dispatchTaskFromHotPost(teamId: string, body: { hotPostId: string; socialAccountId: string; action: string; runAt?: string; }): Promise; listUsers(teamId: string): Promise<{ items: unknown[]; }>; createUser(teamId: string, body: Record): Promise<{ id: string; }>; listSystemBrandMembers(params?: { userId?: string; }): Promise<{ items: unknown[]; }>; createSystemBrandMember(body: { brandId: string; userId: string; role?: string; }): Promise<{ id: string; }>; deleteSystemBrandMember(memberId: string): Promise; listBrandMembers(teamId: string, params?: { userId?: string; }): Promise<{ items: unknown[]; }>; createBrandMember(teamId: string, body: { brandId: string; userId: string; role?: string; }): Promise<{ id: string; }>; listNotificationChannels(teamId: string, params?: ListNotificationChannelsParams): Promise<{ items: unknown[]; }>; createNotificationChannel(teamId: string, body: { provider: string; externalId: string; minRole?: string; campaignId?: string; }): Promise<{ id: string; }>; updateNotificationChannel(teamId: string, channelId: string, body: UpdateNotificationChannelBody): Promise<{ ok: boolean; }>; deleteNotificationChannel(teamId: string, channelId: string): Promise; testNotificationChannel(teamId: string, channelId: string): Promise<{ ok: boolean; warning?: string; jobId?: string; }>; listApiKeys(teamId: string, params?: ListApiKeysParams): Promise<{ items: unknown[]; }>; createApiKey(teamId: string, body: Record): Promise<{ id: string; key: string; }>; deleteApiKey(teamId: string, apiKeyId: string): Promise; rotateApiKey(teamId: string, apiKeyId: string): Promise<{ id: string; key: string; }>; batchRevokeApiKeys(teamId: string, apiKeyIds: string[]): Promise; batchRotateApiKeys(teamId: string, apiKeyIds: string[]): Promise; listEventsFiltered(teamId: string, params?: ListEventsParams): Promise<{ items: unknown[]; }>; exportEventsCsv(teamId: string, params?: ListEventsParams): Promise; createReport(teamId: string, body: Record): Promise<{ id: string; }>; updateJobAgentStatus(teamId: string, jobId: string, body: { status: "running" | "succeeded" | "failed"; lastError?: string; payload?: Record; }): Promise; updatePermissionsMatrix(teamId: string, body: Record): Promise; getAccountGraphDrilldown(teamId: string, params: AccountGraphDrilldownParams): Promise; sessionLogin(body: { email: string; password: string; }): Promise; sessionLogout(): Promise; sessionMe(): Promise; sessionActiveTeam(body: { teamId: string; }): Promise; /** * 管理员替他人重置密码。需要**不受限的交互式 admin 会话**(role=admin 的 api_key * 不行),且不能重置自己(自助改密走 `PUT /v1/auth/password`,它校验旧密码)。 * * `userId` / `email` **恰好给一个**。原来的 `teamId` 参数已删除:密码是用户级全局 * 凭证,团队参数从来没有限制过影响范围,只是误导。 */ sessionSetPassword(body: { userId?: string; email?: string; password: string; }): Promise; sessionUpdateProfile(body: { name: string; }): Promise; listTeamBrowserEnvironments(teamId: string): Promise; listAccountBrowserEnvironments(teamId: string, accountId: string): Promise; createBrowserEnvironment(teamId: string, accountId: string, body: { browserProvider: string; environmentId: string; name?: string | null; timezone?: string; telemetry?: BrowserEnvironmentTelemetryInput; }): Promise; updateBrowserEnvironment(teamId: string, envId: string, body: { telemetry?: BrowserEnvironmentTelemetryInput | null; timezone?: string; lastHealthAt?: string | null; /** Rebind (or null to unbind) the environment to another account. */ socialAccountId?: string | null; name?: string | null; }): Promise<{ ok: boolean; }>; listAccountPersonas(teamId: string, accountId: string): Promise; createAccountPersona(teamId: string, accountId: string, body: { markdown: string; }): Promise; listCampaignAccounts(teamId: string, campaignId: string): Promise; assignCampaignAccount(teamId: string, campaignId: string, body: { socialAccountId: string; visibility?: string; }): Promise; removeCampaignAccount(teamId: string, campaignId: string, socialAccountId: string): Promise; listBrandMentionAuthorizations(teamId: string, query?: { accountId?: string; includeRevoked?: boolean; }): Promise; grantBrandMentionAuthorization(teamId: string, body: { accountId: string; brandKey: string; expiresAt?: string; reason?: string; }): Promise; revokeBrandMentionAuthorization(teamId: string, body: { accountId: string; brandKey: string; reason?: string; }): Promise; deleteBrandMember(teamId: string, memberId: string): Promise; deleteAccount(teamId: string, accountId: string): Promise<{ ok: boolean; }>; listAccountBrandBindings(teamId: string, accountId: string): Promise<{ items: unknown[]; }>; addAccountBrandBinding(teamId: string, accountId: string, body: { brandId: string; }): Promise; removeAccountBrandBinding(teamId: string, accountId: string, brandId: string): Promise<{ ok: boolean; }>; listAccountStatusSnapshots(teamId: string, accountId: string, limit?: number): Promise<{ items: unknown[]; }>; createAccountStatusSnapshot(teamId: string, accountId: string, body: { externalRef?: string; stage?: string; karma?: number; postCount?: number; followersCount?: number; riskLevel?: string; flags?: string[]; note?: string; }): Promise<{ id: string; inserted?: boolean; }>; /** * 团队存在性探针。存在 → `{ id }`;不存在 → 抛 404 `TEAM_NOT_FOUND`。 * * ⚠️ 判据是**错误码**而不是 HTTP 状态:同一个 404 也可能来自「路由不存在」 * 或下游资源缺失,只有 `TEAM_NOT_FOUND` 才代表「你指的这个团队不存在」。 * ⚠️ 拿到 403/其它错误说明 team **存在**、只是当前身份用不了它 —— * ⛔ 不要把它们一并当成「不存在」,团队不是访问边界。 */ probeTeam(teamId: string): Promise<{ id: string; }>; getAuthContext(): Promise; revokeCurrentCliToken(): Promise<{ ok: boolean; }>; opsClaimNext(teamId: string, body: { agentId: string; socialAccountId?: string; leaseSeconds?: number; }): Promise; opsClaim(teamId: string, body: { agentId: string; jobId: string; leaseSeconds?: number; }): Promise<{ job: unknown; }>; opsHeartbeat(teamId: string, body: { agentId: string; jobId: string; leaseSeconds?: number; payload?: Record; }): Promise<{ ok: boolean; }>; opsComplete(teamId: string, body: { agentId: string; jobId: string; permalink?: string; calendarEntryId?: string; redditPostSnapshotId?: string; payload?: Record; /** execution fencing token pair(认领时拿到的;两者须同时给出或同时省略)。 */ executionGeneration?: number; executionId?: string; }): Promise<{ ok: boolean; jobId: string; status: string; /** True when the job was already succeeded — idempotent no-op retry. */ alreadyCompleted?: boolean; trace: Record; updated: Record; } | OpsBlockedReceipt>; opsFail(teamId: string, body: { agentId: string; jobId: string; reason: string; message?: string; payload?: Record; /** execution fencing token pair(认领时拿到的;两者须同时给出或同时省略)。 */ executionGeneration?: number; executionId?: string; }): Promise<{ ok: boolean; jobId: string; status: string; /** True when the job was already in this terminal — idempotent retry. */ alreadyTerminal?: boolean; } | OpsBlockedJobAck>; opsSkip(teamId: string, body: { agentId: string; jobId: string; reason: string; message?: string; payload?: Record; /** execution fencing token pair(认领时拿到的;两者须同时给出或同时省略)。 */ executionGeneration?: number; executionId?: string; }): Promise<{ ok: boolean; jobId: string; status: string; /** True when the job was already in this terminal — idempotent retry. */ alreadyTerminal?: boolean; } | OpsBlockedJobAck>; /** * 评论内容复审:独立 LLM 六维评审+服务端权威计分。 * executionStatus=degraded 时 verdict 不可作为权威判定,调用方应降级本地自审。 * * 作者关系轴:响应里的 `authorRelation` / `authorRelationEvidence` 是**服务端派生**的 * (见 {@link ContentReviewAuthorRelation}),请求里的 `authorRelationHint` 只是提示。 * 声称 `post_author` 而证据证伪 → 422 `AUTHOR_RELATION_MISMATCH` * ({@link isAuthorRelationMismatchError})。 */ contentReview(teamId: string, body: ContentReviewRequestInput | Record): Promise; /** 帖子审核(发帖前质量闸;与评论审核独立 rubric/七维/模型 key)。 */ postReview(teamId: string, body: Record): Promise<{ reviewId: string; executionStatus: "completed" | "degraded"; verdict: "pass" | "revise" | "block"; score: number; modelScore: number | null; dimensions: Record | null; optimized: { title: string; body: string; } | null; rationale: string | null; reviewModel: string; rubricVersion: string; thresholdSnapshot: Record; latencyMs: number; degradedReason: string | null; } & ReviewScopeReceipt>; /** 互动时序 P2:批量调度上下文(team 级,best_effort 一致性,keyset 分页)。 */ getInteractionScheduleContext(teamId: string, params?: { limit?: number; cursor?: string; }): Promise; getAccountAgentContext(teamId: string, accountRef: string, params?: { expand?: string; }): Promise>; getJobAgentContext(teamId: string, jobId: string): Promise>; listAccountPools(teamId: string, accountRef: string, params?: { status?: string; industry?: string; limit?: number; personaAware?: boolean; tier?: "starter" | "advanced" | "all"; includeDebug?: boolean; }): Promise; listSubredditCandidates(teamId: string, accountRef: string, params?: { industry?: string; action?: string; limit?: number; personaAware?: boolean; tier?: "starter" | "advanced" | "all"; includeDebug?: boolean; }): Promise<{ items: unknown[]; personaPools?: unknown; systemPools?: unknown; fallback?: unknown; exhaustion?: SubredditCandidatesExhaustion; /** * 分页方案 B8b:本次生效的 limit / 返回条数 / 是否被截断(配额、limit 或单 key 行数上限)。 * 该端点没有 offset。配额 / limit 截断可调大 limit 或收窄 industry/tier;单 key 行数上限是 * 上游固定帽,调 limit 补不全,只能收窄 industry 或人工处理。评论动作下配额阶段尚未过冷却 / * affinity,`truncated: true` 可能偏多报;`false` 是精确的「没被截」。 * 旧服务端不返回这三个字段。 */ selectionLimit?: number; returned?: number; truncated?: boolean; }>; listSubredditPostAnnotations(teamId: string, params?: { subreddit?: string; permalink?: string; annotationType?: SubredditPostAnnotationType; brandId?: string; source?: SubredditPostAnnotationSource; limit?: number; offset?: number; }): Promise<{ items: unknown[]; total?: number; }>; upsertSubredditPostAnnotation(teamId: string, body: Record): Promise; listSubredditStyleProfiles(params?: { subreddit?: string; scope?: string; brandId?: string; /** 逗号分隔多值,如 `proposed,approved`。 */ status?: string; limit?: number; offset?: number; }): Promise<{ items: unknown[]; total?: number; }>; /** 响应里的 `profile.rowVersion` 就是写操作要回传的 If-Match 值。 */ getSubredditStyleProfile(id: string): Promise<{ profile: SubredditStyleProfileView; }>; getActiveSubredditStyleProfile(params: { subreddit: string; scope?: string; brandId?: string; }): Promise; proposeSubredditStyleProfile(body: Record): Promise<{ profile: SubredditStyleProfileView; }>; /** 四眼审批 `proposed → approved`。提案人本人不可批(服务端 409 SELF_APPROVE)。 */ approveSubredditStyleProfile(input: SubredditStyleProfileTransitionInput): Promise<{ profile: SubredditStyleProfileView; }>; /** 审批人驳回 `proposed → retired`(`retire_reason=rejected:`)。 */ rejectSubredditStyleProfile(input: SubredditStyleProfileTransitionInput & { reason: string; }): Promise<{ profile: SubredditStyleProfileView; }>; /** 提案人本人撤回。worker/LLM 提案(proposedByUserId=NULL)不可 cancel,请用 reject。 */ cancelSubredditStyleProfile(input: SubredditStyleProfileTransitionInput & { reason: string; }): Promise<{ profile: SubredditStyleProfileView; }>; /** 停用已生效画像 `approved → retired`。 */ retireSubredditStyleProfile(input: SubredditStyleProfileTransitionInput & { reason: string; }): Promise<{ profile: SubredditStyleProfileView; }>; /** * 四个 transition 的共同形状。 * * 🔴 **`rowVersion` 是必传的,SDK 刻意不提供「缺省自动 GET 当前值」那种便利。** * CLI 有这个便利(交互式使用,人就在现场),但库里自动 GET 会让一个**本意保护 * 特定版本**的写悄悄作用到最新版本 —— 乐观锁就此被架空,而且失败时毫无痕迹。 * 调用方要先 `getSubredditStyleProfile` 拿 `profile.rowVersion` 再传进来。 * * 🔴 **409 PROFILE_STALE 一律原样抛出,绝不自动重取重试。** 自动重试等于替用户 * 认可了他没看过的那一版内容。 */ private styleProfileTransition; listSettings(): Promise<{ items: Array<{ key: string; value: string; }>; }>; getSetting(key: string): Promise<{ key: string; value: string; }>; upsertSetting(key: string, body: { value: string; description?: string; }): Promise<{ key: string; value: string; }>; upsertSettingsBatch(body: { settings: Array<{ key: string; value: string; description?: string; }>; }): Promise<{ updated: number; }>; listTeamInvites(teamId: string, params?: { status?: string; }): Promise<{ items: unknown[]; }>; createTeamInvite(teamId: string, body: { email: string; role: string; brandId?: string; }): Promise; revokeTeamInvite(teamId: string, inviteId: string): Promise<{ ok: boolean; }>; listSystemMembers(): Promise<{ items: unknown[]; }>; updateSystemMemberRole(userId: string, body: { role: string; }): Promise<{ updated: boolean; }>; deleteSystemMember(userId: string): Promise<{ updated: boolean; }>; listSystemTeams(): Promise<{ items: unknown[]; }>; listSystemMemberTeams(userId: string): Promise<{ items: unknown[]; }>; addSystemMemberTeam(userId: string, body: { teamId: string; role?: string; }): Promise; removeSystemMemberTeam(userId: string, teamId: string): Promise<{ updated: boolean; }>; listTeamMembers(teamId: string): Promise<{ items: unknown[]; }>; updateTeamMemberRole(teamId: string, userId: string, body: { role: string; }): Promise<{ updated: boolean; }>; removeTeamMember(teamId: string, userId: string): Promise<{ updated: boolean; }>; listSystemUsers(): Promise<{ items: unknown[]; }>; /** * `GET /v1/system/feishu-identities` —— Hub 用户 ↔ 飞书 open_id 的映射现状。 * * 返回**全体用户**(users LEFT JOIN 映射表),所以 `resolveStatus === null` 表示 * 「从未同步过」,与 `not_found`(查过、通讯录里没这人)是两回事。 * * ⚠️ 非「不受限交互式 admin 登录」读到的 `openId`/`unionId` 是脱敏的 `ou_***`。 * 判据是响应里的 `redacted`,不要拿 `openId` 的字符串形态去猜 —— 脱敏值也以 * `ou_` 开头,直接透传会写出一个永远 @ 不到人的映射。 */ listFeishuIdentities(params?: { provider?: string; appId?: string; status?: FeishuIdentityListStatusFilter; includeInactiveUsers?: boolean; }): Promise; /** * `PUT /v1/system/feishu-identities` —— 批量落库(幂等,同一批重放结果相同)。 * * 写入面只认不受限的交互式 admin 登录:**API key 一律 403**,所以这个方法在 * 服务端到服务端的集成里不可用,它是给交互式运维工具(CLI/后台)用的。 * * 语义上最贵的一条:`not_found` 会清空既有 `openId`(离职语义)。查询失败必须用 * `lookup_failed`(服务端会保住上次成功解析的值),把「没问出来」写成 `not_found` * 等于静默注销一个本来能收到告警的人。 */ upsertFeishuIdentities(body: { provider?: "feishu"; appId: string; entries: FeishuIdentityUpsertEntry[]; }): Promise; createSystemUser(body: { email: string; name: string; password?: string; role?: string; }): Promise<{ id: string; }>; listIndustryPools(teamId: string, params?: { limit?: number; offset?: number; status?: string; industryKey?: string; poolKind?: string; }): Promise<{ items: unknown[]; total?: number; }>; /** * 行业池目录(catalog)。**表本身是系统级共享数据**:`(industryKey, subreddit)` 全局唯一, * 行上的 `teamId` 只是建行时写下的**不可变历史归因署名**,不是隔离边界。 * * ⚠️ 因此默认列表是一个 **attribution-scoped view**:只列出当前 team 署名的行。 * 想跨团队看(例如提交前查重「这个板块是不是已经有人录过了」)必须给 * `aggregateVisibleTeams: true` —— 否则会漏掉别的团队署名的同名行, * 提交后被系统级 upsert 静默更新掉那一行。 * * ⚠️ `aggregateVisibleTeams` 在这里同样退化成**署名过滤器**,且范围是**当前主体可见的团队**, * 不是无条件全表:不可见团队署名的行仍然看不到。它与「真团队隔离资源」上的同名参数 * (那里是数据边界)语义不同,别照搬理解。 */ listSubredditPoolsCatalog(teamId: string, params?: { limit?: number; offset?: number; status?: string; industryKey?: string; poolKind?: string; /** 跨**可见团队**的署名聚合视图(不是无条件全表)。 */ aggregateVisibleTeams?: boolean; }): Promise<{ items: unknown[]; total: number; }>; createSubredditPool(teamId: string, body: Record): Promise<{ id: string; }>; updateSubredditPool(teamId: string, poolId: string, body: Record): Promise<{ id: string; }>; deleteSubredditPool(teamId: string, poolId: string): Promise<{ id: string; deleted: boolean; }>; listPoolAliases(teamId: string, params?: { limit?: number; offset?: number; aliasRef?: string; industryKey?: string; poolKind?: string; tier?: string; status?: string; }): Promise<{ items: unknown[]; total: number; }>; createPoolAlias(teamId: string, body: Record): Promise<{ id: string; }>; updatePoolAlias(teamId: string, aliasId: string, body: Record): Promise<{ id: string; }>; deletePoolAlias(teamId: string, aliasId: string): Promise<{ id: string; deleted: boolean; }>; /** * S0–S3 准入规则。与 catalog 同款:**系统级共享**(`subreddit` 全局唯一), * 行上的 `teamId` 只是历史归因署名。默认列表按署名过滤, * 跨团队查重必须 `aggregateVisibleTeams: true`(范围 = 当前主体可见的团队)。 */ listSubredditPoolTierRules(teamId: string, params?: { limit?: number; offset?: number; tier?: string; enabled?: boolean; /** 跨**可见团队**的署名聚合视图(不是无条件全表)。 */ aggregateVisibleTeams?: boolean; }): Promise<{ items: unknown[]; total: number; }>; createSubredditPoolTierRule(teamId: string, body: Record): Promise<{ id: string; }>; updateSubredditPoolTierRule(teamId: string, ruleId: string, body: Record): Promise<{ id: string; }>; deleteSubredditPoolTierRule(teamId: string, ruleId: string): Promise<{ id: string; deleted: boolean; }>; listAccountPoolContext(teamId: string, accountRef: string, params?: Record): Promise; getGraphV2Ego(teamId: string, params: Record): Promise; getGraphV2Timeline(teamId: string, params: Record): Promise; getGraphV2Explain(teamId: string, edgeId: string): Promise; getGraphV2Path(teamId: string, params: Record): Promise; getGraphV2Clusters(teamId: string, params: Record): Promise; getGraphV2Impact(teamId: string, params: Record): Promise; listSubredditRuleCaches(teamId: string, params?: { includeExpired?: boolean; }): Promise<{ items: unknown[]; }>; upsertSubredditRuleCache(teamId: string, subreddit: string, body: { rulesMarkdown: string; sourceUrl?: string; expiresInDays?: number; }): Promise<{ id: string; }>; /** * ⚠️ **单页**,不是全量 —— 服务端按 `createdAt DESC` 排序,`limit` 上限 200、默认 100。 * * 历史上这个方法**一个分页参数都不传**,于是永远只拿服务端默认的第一页 100 条, * 而同一个返回体里的 `total` 是 112 —— 调用方看到 `items` 就以为是全量,漏掉的 * 恰恰是**最早加入**的那 12 条(排序是 DESC),往往是最确定该封的那批。 * * 🔴 **板块准入这类安全判定不要用这个方法**,用 * {@link SocialHubClient.listAllSystemSubredditBlocklists}(自动翻页 + 一致性校验)。 * 这里保留分页版只是为了后台列表分页展示,以及不破坏已发布 SDK 的方法签名。 */ listSystemSubredditBlocklists(params?: { kind?: string; includeExpired?: boolean; limit?: number; offset?: number; }): Promise<{ items: SystemSubredditBlocklistEntry[]; total: number; limit: number; offset: number; }>; /** * 取**全量** system subreddit blocklist(自动翻页),供板块准入这类安全判定使用。 * * 为什么必须有这个方法:分页版每页上限 200(contracts 硬上限,本次不改),所以 * 「让调用方自己传一个够大的 limit」根本表达不了「全量」;而把正确性交给调用方 * 记得传参,正是本次要消灭的失效模式。 * * **翻页期间集合变了怎么办**:offset 分页**不是快照分页**,并发增删会导致跨页 * 重复或跳过。口径是「要么给出可自证一致的结果,要么报错」, * **绝不静默拼一个可能不一致的结果**: * - 一趟内所有页的 `total` 必须一致、跨页不得出现重复行、去重后条数必须 === `total`; * - 任一条不满足 → 整趟从 `offset=0` 重跑,最多 {@link BLOCKLIST_FULL_SCAN_MAX_ATTEMPTS} 趟; * - 全部失败 → **抛错**,并带上每趟的**具体**原因(漂移 / 重复 / 空页 / 超预算)。 * * 🔴 **这是尽力校验,不是快照证明 —— 不要把它当成后者**(Codex 评审提出)。 * 反例:翻页期间「删一条 + 加一条」会让 `total` 纹丝不动、去重后条数也刚好对上, * 于是校验通过,而结果里含着已删的、缺着新加的。`expiresAt` 过滤同理 —— * 没人写库,集合也会随时间变化。要真正的快照语义需要服务端提供 * as-of / ETag / keyset cursor;在那之前这里只能做到「**能查出来的**不一致一律 * 拒绝」,查不出来的那一类仍会漏过去。所以本方法的承诺是 * 「没报错 = 没发现不一致」,**不是**「保证是某一时刻的完整快照」。 * * 抛错而不是「返回一个带 inconsistent 标记的结果」是有意的:这是安全闸的取数路径, * 「没有答案」是安全的(调用方重跑一次即可),而「一个可能不全、但看起来完整的答案」 * 正是本次缺陷本身 —— 标记会被 `jq '.items'` 一秒钟丢掉。 */ listAllSystemSubredditBlocklists(params?: { kind?: string; includeExpired?: boolean; }): Promise<{ items: SystemSubredditBlocklistEntry[]; total: number; /** 为达成一致所重跑的趟数,1 表示一次就一致。 */ attempts: number; }>; /** 一趟完整翻页;`consistent` 为假时调用方应整趟重来(见上)。 */ private scanAllSystemSubredditBlocklistsOnce; upsertSystemSubredditBlocklist(subreddit: string, body: { kind?: string; reason?: string; source?: string; expiresInDays?: number; }): Promise<{ id: string; }>; deleteSystemSubredditBlocklist(subreddit: string, params?: { kind?: string; }): Promise<{ ok: boolean; }>; listTierRules(teamId: string, params?: { tier?: string; enabled?: boolean; limit?: number; offset?: number; }): Promise; createTierRule(teamId: string, body: Record): Promise<{ id: string; }>; updateTierRule(teamId: string, ruleId: string, body: Record): Promise<{ id: string; }>; deleteTierRule(teamId: string, ruleId: string): Promise<{ id: string; deleted: boolean; }>; listIntelRuns(params?: { dimension?: string; status?: string; limit?: number; offset?: number; }): Promise; /** 单 run 进度投影(voc-acquire 编排轮询用);仅返状态/计数,不含 request/result payload。 */ getIntelRunProgress(runId: string): Promise<{ id: string; dimension: string; status: string; startedAt: string | null; finishedAt: string | null; inserted: number; failedCount: number; }>; listKolProfiles(params?: { platform?: string; limit?: number; offset?: number; }): Promise; getKolProfile(kolProfileId: string): Promise; fetchHotPostsByIndustry(body: Record): Promise; fetchHotPostsByTier(body: Record): Promise; fetchInsightsByKeywords(body: Record): Promise; getBrandMentionRadar(body: Record): Promise; getOpportunityMap(body: Record): Promise; getInsightsSov(body: Record): Promise; getSentimentIntent(params?: { keywords?: string[]; brandId?: string; }): Promise; getCampaignLift(body: Record): Promise; getSubredditVocSummary(body: { keywords: string[]; brandId?: string; }): Promise<{ postCount: number; totalComments: number; totalScore: number; avgUpvoteRatio: number | null; uniqueSubreddits: number; topSubreddits: { subreddit: string; count: number; }[]; }>; getLatestInsightSnapshot(query?: { brandId?: string; }): Promise; saveInsightSnapshot(body: { payload: Record; brandId?: string; }): Promise; /** * 通用网页抓取。Reddit .json(mode "reddit-json" 或 .json URL)走 API 内直连 * curl_cffi+住宅代理返回结构化 JSON;HTML/普通网页经自托管 Firecrawl 返回 * markdown/html/rawHtml/links。 * * **务必先看 `ok`/`outcome`,不要只看 HTTP 状态**(立项 A):Firecrawl 返回 200 * 但正文是登录页/验证码页时,Hub 会回 HTTP 200 + `ok:false` + `outcome` * (`login_wall`/`captcha`/`empty_content` 等)并**不回传正文字段**,同时给出 * `classificationEvidence` 说明凭什么这么判。`outcome:"unknown"`(证据不足)仍会 * 带正文,但它**未经确认**,`warnings` 里有显式提示——不要当成已验证正文使用。 * * 技术层失败(provider 非 2xx / Hub 等待超时 / 网络错误)抛 `SocialHubHttpError` * (429/502/504),`code` 为 `SCRAPE_RATE_LIMITED`/`SCRAPE_TIMEOUT`/ * `SCRAPE_UPSTREAM_ERROR`,`details` 里带 provider typed code/error/request id、 * 失败阶段与尝试次数。 */ scrapeUrl(body: { url: string; formats?: Array<"markdown" | "html" | "rawHtml" | "links">; mode?: "raw" | "reddit-json"; timeoutMs?: number; }): Promise; getScrapeStatus(): Promise<{ configured: boolean; firecrawl?: boolean; redditDirect?: boolean; inFlight: number; /** 立项 A 新增:当前 2xx 页面分类器版本。 */ classifierVersion?: string; /** 立项 A 新增:生效的尝试次数上限(含首次)。 */ maxAttempts?: number; }>; /** GET subjects/:hotPostId — active spec 下该 hot_post 的 current proposed 分类(proposed|missing)。 */ getContentClassificationSubject(hotPostId: string, params?: { purpose?: string; }): Promise<{ purpose: string; classification: unknown; }>; /** GET specs — pilot 暴露该 purpose 当前 active spec(activeSpec:null 表示尚未激活)。 */ listContentClassifierSpecs(params?: { purpose?: string; }): Promise<{ purpose: string; activeSpec: unknown; }>; /** GET specs/:id — 按 id 查任意 spec(含 draft / retired)。 */ getContentClassifierSpec(specId: string): Promise<{ spec: unknown; }>; /** POST specs — 建 draft spec(admin only)。同 fingerprint 已存在则幂等返回(created=false)。 */ createContentClassifierSpec(body: { purpose: string; taxonomyVersion: string; taxonomySchemaVersion: string; promptVersion: string; modelPolicy: Record; inputFingerprintVersion?: string; truncationPolicyVersion?: string; formatDetectorVersion?: string; }): Promise<{ spec: unknown; created: boolean; }>; /** POST specs/:id/activate — 激活 spec(admin only)。同 purpose 至多一 active;已 active 幂等。 */ activateContentClassifierSpec(specId: string): Promise<{ spec: unknown; }>; /** GET presentation — content_classification.presentation.v1(缺则契约默认;source 标注来源)。 */ getContentClassificationPresentation(): Promise<{ presentation: unknown; source: string; }>; /** GET runs/recent — 最近成功 run + 帖子上下文(shadow 观测;非权威 current)。 */ listContentClassificationRecentRuns(params?: { purpose?: string; contentForm?: string; limit?: number; }): Promise<{ purpose: string; activeSpecId: string | null; /** * ⚠️ 本页条数,**不是总数**。保留仅为兼容,与 `returned` 恒相等;新代码读 `returned`。 * @deprecated 用 `returned`。 */ count: number; /** 本页实际返回的条数。 */ returned: number; /** 本次生效的条数上限(请求的 limit,缺省 20)。 */ selectionLimit: number; /** * 是否还有更多未返回的 run(服务端多取一条哨兵判定,精确)。 * 这是 recent 投影,**没有分页** —— 为 true 时请调大 limit 或改用精确查询,⛔ 不要自拼 offset。 */ truncated: boolean; runs: unknown[]; }>; /** * POST subreddits — 只登记 canonical 板块身份,**不建资产**(幂等)。 * * 「r/foo 这个板块存在」与「r/foo 是我们的」是两件事:后者会让绑定到它的账号在 * enforce 档下被全面阻断。分开是为了能先把 20 个板块名录进来核对拼写, * 再逐个确认所有权。 */ registerSubredditIdentity(body: { name: string; displayName?: string; }): Promise<{ subredditId: string; normalizedName: string; created: boolean; }>; /** POST — 登记一条自建板块资产(幂等:同板块已有资产则返回 created=false)。 */ registerOwnedSubreddit(body: { name: string; displayName?: string; /** 可为空:板块是公司资产,不必挂品牌(代价由行级授权承担)。 */ brandId?: string | null; assetStatus?: "active" | "archived" | "lost"; notes?: string | null; }): Promise<{ ownedSubredditId: string; created: boolean; }>; /** * GET — 列出自建板块资产(含版主绑定与只读风险投影)。 * * `moderatorRisk=zero|single` 是**零/单版主的发现入口**:存量 20 条全是单版主, * 这是查出「哪些板块只剩一个版主、而那个号刚被封」的地方。 * ⛔ 它只做发现,不做写入硬拒绝。 */ listOwnedSubreddits(params?: { assetStatus?: "active" | "archived" | "lost"; moderatorRisk?: "zero" | "single" | "redundant"; brandId?: string; limit?: number; offset?: number; }): Promise<{ items: unknown[]; }>; /** GET :id — 单条资产详情(不可见时返 404,不返 403:避免变成枚举面)。 */ getOwnedSubreddit(ownedSubredditId: string): Promise; /** * PATCH :id/status — 改资产状态(CAS,rowVersion 漂移返 409)。 * * ⚠️ `lost` **解除**该板块所有绑定账号的阻断并停止核验;`archived` **保留**阻断。 */ updateOwnedSubredditStatus(ownedSubredditId: string, body: { assetStatus: "active" | "archived" | "lost"; rowVersion: number; reason?: string | null; }): Promise<{ rowVersion: number; }>; /** * POST :id/moderators — 给板块加一个版主绑定。 * * 🔴 返回的 `accountAtRisk=true` 表示**这个账号已经是 banned/restricted 了**。 * 建绑不构成状态转变,所以封禁告警那条链一条都不会发 —— 服务端另落了一条 * critical 风险事件,而这个返回值是人工录入 20 条存量时当场能看见的信号。 */ addSubredditModerator(ownedSubredditId: string, body: { socialAccountId: string; isPrimary?: boolean; declaredPermissions?: string[]; notes?: string | null; }): Promise<{ bindingId: string; accountAtRisk: boolean; }>; /** POST moderators/:id/revoke — 撤销绑定(CAS)。行永久保留,历史不可抹。 */ revokeSubredditModerator(bindingId: string, body: { rowVersion: number; reason?: string | null; }): Promise<{ rowVersion: number; }>; /** POST moderators/replace — 更替版主 = **关旧建新**(不是就地改 accountId)。 */ replaceSubredditModerator(body: { bindingId: string; rowVersion: number; nextSocialAccountId: string; declaredPermissions?: string[]; reason?: string | null; }): Promise<{ revokedBindingId: string; bindingId: string; }>; /** GET — 读全局版主保护档位(off / shadow / enforce)。 */ getModeratorProtection(): Promise<{ mode: "off" | "shadow" | "enforce"; rowVersion: number; note: string | null; updatedAt: string; updatedByUserId: string | null; }>; /** * PATCH — 改全局版主保护档位(CAS + 审计)。 * * 🔴 这是**全局**开关,无个体 override:`enforce` 之下所有持有 active 绑定的账号, * 四类自动化动作(publish_post / publish_comment / warmup / engage)一律阻断。 * 切档前请先跑完 `shadow` 的验证窗口,并暂停 dispatcher 与外部 agent cron * (已在外部执行中的动作不可撤回,那是 cutover 前的已知风险)。 */ setModeratorProtection(body: { mode: "off" | "shadow" | "enforce"; rowVersion: number; note?: string | null; }): Promise<{ mode: string; rowVersion: number; }>; private subscriptionsBase; private eventsBase; private sourcesBase; /** * GET .../notification-subscriptions * 非 manager/admin 只看得到自己拥有(`ownerUserId`)的订阅 —— 别人机器的消费位点 * 露出来只会诱发误 ack。 */ listNotificationSubscriptions(teamId: string, params?: { status?: NotificationSubscriptionStatus; sourceId?: string; /** 常驻消费方解析订阅名时传入,便于 Ctrl-C 立即中断。 */ signal?: AbortSignal; }): Promise<{ items: NotificationSubscriptionDto[]; }>; /** GET .../notification-subscriptions/:id —— 返回**裸** subscription(无包装)。 */ getNotificationSubscription(teamId: string, subscriptionId: string, /** 常驻消费方轮询游标时传入,便于 Ctrl-C 立即中断。 */ opts?: { signal?: AbortSignal; }): Promise; /** * POST .../notification-subscriptions(201,裸 subscription)。 * * ⚠️ **命名空间分区**(跨团队聚合订阅的滥用控制):调用方在目标 team 上**没有 active * membership** 时,`name` 必须是 `xsub/<你的 userId>/`;是成员时则**不得**以 * `xsub/` 开头。违反分别是 400 `SUBSCRIPTION_NAME_PREFIX_REQUIRED` / * `SUBSCRIPTION_NAME_PREFIX_RESERVED`(前者的 `details.requiredPrefix` 直接给出该用的 * 前缀)。这与 409 `SUBSCRIPTION_NAME_TAKEN`(名字被占,换名即可)是**两回事**。 * * ⚠️ **配额**:超限是 409 `SUBSCRIPTION_QUOTA_EXCEEDED`,`details.basis === "total"` * 那档统计**含软删**——`(teamId, name)` 永久唯一,删订阅**不会**归还额度。 */ createNotificationSubscription(teamId: string, body: CreateNotificationSubscriptionSdkBody): Promise; /** * PATCH .../notification-subscriptions/:id —— 乐观锁 CAS(`expectedRowVersion` 必填)。 * `status` 只能是 active|paused;pause/resume 另有语义化捷径端点(走同一把锁)。 */ updateNotificationSubscription(teamId: string, subscriptionId: string, body: UpdateNotificationSubscriptionSdkBody): Promise; /** * POST .../notification-subscriptions/:id/pause|resume。 * `expectedRowVersion` 可省(服务端用当前值),显式传则是真 CAS。 */ setNotificationSubscriptionPaused(teamId: string, subscriptionId: string, paused: boolean, body?: { expectedRowVersion?: number; }): Promise; /** DELETE .../notification-subscriptions/:id —— 软删(保留审计),**204 无响应体**。 */ deleteNotificationSubscription(teamId: string, subscriptionId: string): Promise; /** * POST .../notification-subscriptions/:id/reset —— 重设游标起点(stale 恢复的唯一出口)。 * `startPosition` 与 `cursorSeq` **互斥**;后者是运维手工修复面,服务端会 clamp 在 * `[floor - 1, latestSeq]`。reset 会 bump `receiptEpoch`,作废全部在途 receipt。 * * ⚠️ `expectedRowVersion` **必填**:reset 是破坏性写,服务端做真 CAS,冲突返回 * `409 SUBSCRIPTION_ROW_VERSION_CONFLICT`。调用前先 GET 订阅拿当前 `rowVersion`。 * * ⚠️ reset **不是** resume:`paused` 订阅 reset 后**仍是** paused;只有 `stale` * 会被恢复成 `active`。 */ resetNotificationSubscriptionCursor(teamId: string, subscriptionId: string, body: { expectedRowVersion: number; startPosition?: NotificationSubscriptionStartPosition; cursorSeq?: string; }): Promise; /** * POST .../notification-events/pull —— long-poll 拉事件。**幂等**:不推进游标。 * * `limit` 默认 1:游标是**连续 watermark**(`cursorSeq = N` 表示 seq ≤ N 的匹配事件均已完成), * 一次拉 N 条却只在中间某条失败,就无法安全 ack 其后的任何一条。 */ pullNotificationEvents(teamId: string, body: { subscriptionId: string; /** 1..50,默认 1。 */ limit?: number; /** 0..30 秒;0 = 立即返回(可能空)。 */ waitSeconds?: number; }, /** long-poll 可能挂满 30 秒;常驻消费方必须能被 Ctrl-C 立即中断。 */ opts?: { signal?: AbortSignal; }): Promise; /** * POST .../notification-events/ack —— 推进连续 watermark。 * 语义是「本订阅所有匹配事件中 seq ≤ throughSeq 的**均已完成**」,**不是**执行回执。 * 409 `SUBSCRIPTION_CURSOR_CONFLICT` = CAS 冲突(含重放);410 = 游标已过保留 floor。 */ ackNotificationCursor(teamId: string, body: { subscriptionId: string; expectedCursorSeq: string; throughSeq: string; receipt: NotificationPullReceiptDto; }): Promise<{ subscriptionId: string; cursorSeq: string; }>; /** * GET .../notification-events —— **运维查看面**(元数据摘要,不含 payload)。 * * ⚠️ 这不是订阅消费面:它没有 watermark / receipt、不推进任何游标,因此可以自由 * 倒序与跳页。要不漏地消费事件请走 {@link SocialOpsHubClient.pullNotificationEvents}。 * * 边界语义(服务端冻结): * - `afterSeq` 排他**下界**、`beforeSeq` 排他**上界**; * - `order` **只**决定从区间的哪一端取 N 条与返回顺序,不改变上下界含义; * - 默认 `order: "desc"` = 最近 N 条; * - `afterSeq >= beforeSeq` → 400(矛盾区间恒为空,静默返回空会伪装成「没有更多了」)。 * * 翻页写法:`order: "desc"` 首屏 → 下一页传 `beforeSeq = 上一页 page.lastSeq`。 */ listNotificationEvents(teamId: string, params?: { afterSeq?: string; beforeSeq?: string; order?: "asc" | "desc"; sourceId?: string; limit?: number; }): Promise<{ items: NotificationEventSummaryDto[]; /** 本页首尾 seq —— **不是** stream 的 latest/earliest。 */ page: { order: "asc" | "desc"; firstSeq: string | null; lastSeq: string | null; }; latestSeq: string; earliestAvailableSeq: string; }>; /** * 节点 schedule 的**运维可观测面** —— `notification_node_schedules` 的读取端点。 * * 🔴 为什么不能从事件流反推:**kill switch 期间的 `missed` 与锚点消失的 `cancelled` * 根本不产生任何事件**。靠事件流反推只会把它们算成 0,于是「今晚一条都没发」 * 与「今晚本来就没有到期节点」在页面上长得一模一样。 * * ⚠️ 时间窗**必须有界**(服务端默认最近 30 天、上限 90 天)。窗口上限只限制**跨度**, * 不限制窗口必须在最近 90 天内 —— 查更早的窗口完全合法,但可能已经在保留期之外, * 因此 summary / list 都随响应回 `retention`(见 {@link NotificationScheduleRetentionDto})。 */ private schedulesBase; /** GET .../notification-node-schedules/summary —— 按 source × status 聚合计数。 */ getNotificationNodeSchedulesSummary(teamId: string, params?: { sourceId?: string; dueFrom?: string; dueTo?: string; }): Promise; /** * GET .../notification-node-schedules —— 按 source / status / dueAt 窗口列表。 * * 分页是 **keyset**(`nextCursor` 为 opaque 串,原样回传即可);`nextCursor = null` * 表示没有下一页。⚠️ 弱一致:并发 backfill 可能插入更早 `dueAt` 的新行。 */ listNotificationNodeSchedules(teamId: string, params?: { sourceId?: string; status?: NotificationScheduleStatusDto; subjectId?: string; dueFrom?: string; dueTo?: string; cursor?: string; limit?: number; }): Promise<{ items: NotificationNodeScheduleSummaryDto[]; window: { dueFrom: string; dueTo: string; }; nextCursor: string | null; retention: NotificationScheduleRetentionDto; }>; /** * GET .../notification-node-schedules/:scheduleId —— 单条状态原因与关联事件 seq。 * * ⚠️ **没有** `lastError` 原文:那是任意异常字符串(可能含 SQL / 内部路径)。 * 可分支的原因在 `failureReason`(归一化枚举)与 `cancelReason`; * `hasLastError` 只说明「存在原始异常」,原文在服务端日志里。 */ getNotificationNodeSchedule(teamId: string, scheduleId: string): Promise; /** * GET /v1/settings/notification-schedule-purge-cron/status —— schedule 保留期 GC 的 * cron 运行状态。 * * 🔴 **系统级、不带 teamId**:状态是 worker 写的全局单 key,不是某个 team 的清理进度。 * 权限 `systemSetting:read`(仅 admin/manager)—— 响应带 `workerId` 与异常原文摘要。 * * 🔴 **`null` ≠ `0`**:计数为 `null` 表示这次快照没有这个读数(从没跑过 / 只 registered / * failed 未产出),为 `0` 才是「跑了且真的是零」。判「跑没跑过」看 * `hasRunSinceRegistration`,别拿 `deleted ?? 0` 糊。 * * ⚠️ 三个**非错误**字段:`protectedGroups`(sentinel 不变量有意保护)、 * `lockBusyGroups`(被物化/dispatcher 持锁,下轮重试)、`backlog`(独立 `LIMIT 1` probe * 判定的「确实还有候选」)。只有 `failedGroups > 0` 与 `status === "failed"` 是真异常。 */ getNotificationSchedulePurgeCronStatus(): Promise; /** GET .../notification-events/:seq —— 单条完整 envelope(按事件行冻结的权限重验)。 */ getNotificationEventBySeq(teamId: string, seq: string): Promise; /** GET .../notification-sources —— registry 声明 + 本 team 的 active config 摘要。 */ listNotificationSources(teamId: string): Promise<{ items: { definition: unknown; activeConfig: NotificationSourceConfigDto | null; }[]; }>; /** GET .../notification-sources/:sourceId/configs —— 该 source 的历史 version 列表。 */ listNotificationSourceConfigs(teamId: string, sourceId: string, params?: { limit?: number; }): Promise<{ items: NotificationSourceConfigDto[]; }>; /** GET .../notification-sources/:sourceId/configs/:configId(裸 config)。 */ getNotificationSourceConfig(teamId: string, sourceId: string, configId: string): Promise; /** * POST .../notification-sources/:sourceId/configs —— 创建**新的不可变 version** * (`activate` 默认 true;除 enabled 外任何变更都是新 version,只影响新物化的 schedule)。 * 409 `SOURCE_CONFIG_REQUIRED_FIELDS_CONFLICT` = 会打掉某些 active 订阅的 requiredFields。 */ createNotificationSourceConfig(teamId: string, sourceId: string, body: UpsertNotificationSourceConfigSdkBody, params?: { activate?: boolean; }): Promise; /** * POST .../notification-sources/:sourceId/configs/:configId/activate。 * * `expectedActive` 是**可选的 compare-and-set**: * - 省略 = 保持历史的 last-serialized-writer-wins(两个管理员会互相静默覆盖); * - `null` = 「我认为当前没有 active」; * - `{ configId, version? }` = 「我认为当前 active 就是这一行」。 * * 不符 → **409 `SOURCE_CONFIG_ACTIVE_STATE_CONFLICT`**,`details` 带真实的 * `actualActiveConfigId` / `actualActiveVersion`,无需再 GET 一次即可重试。 * * 🔴 人工写路径(CLI / 后台页)**应当总是带上它** —— 服务端无法替调用方判断 * 「你有没有看过当前状态」。 */ activateNotificationSourceConfig(teamId: string, sourceId: string, configId: string, body?: { expectedActive?: NotificationExpectedActiveDto; }): Promise; /** * POST .../configs/:fromConfigId/narrow-history。默认是服务端真实 dry-run;apply 必须 * 携带预览返回的 selectionHash。 */ narrowNotificationHistory(teamId: string, sourceId: string, fromConfigId: string, body: { targetConfigId: string; apply?: boolean; expectedSelectionHash?: string; }): Promise; /** * PUT .../notification-sources/:sourceId/enabled —— 实时 kill switch。 * * 🔴 按 **sourceId 身份**而不是 version id:DB 语义是改 `(teamId, sourceId)` 下 * **全部历史 version 行**(在途 schedule 引用的是物化时那一行,只改 active 行 = * 开关形同虚设)。本操作**不** bump sourceConfigVersion。 * disabled 期间到点的 schedule 标 `missed`,重新 enable **不补发**。 */ setNotificationSourceEnabled(teamId: string, sourceId: string, enabled: boolean): Promise<{ totalVersions: number; updatedVersions: number; enabled: boolean; }>; private templatesBase; /** GET /v1/system/notification-source-templates —— 每个已发版 source 的当前 active 模板。 */ listNotificationSourceTemplates(): Promise<{ items: { sourceId: string; canonicalTopic: string; /** source **类型**展示名的 i18n key(registry 真源;不是模板的 displayName)。 */ labelKey: string; sourceDefinitionVersion: number; payloadSchemaVersion: string; activeTemplate: NotificationSourceTemplateDto | null; }[]; }>; /** GET .../templates —— 该 source 的模板版本历史(version 降序)。 */ listNotificationSourceTemplateVersions(sourceId: string, params?: { limit?: number; }): Promise<{ items: NotificationSourceTemplateDto[]; }>; /** GET .../templates/:id(裸模板行)。 */ getNotificationSourceTemplate(sourceId: string, templateId: string): Promise; /** * POST .../templates —— 创建新的不可变模板 version(201)。 * * ⚠️ `activate` **默认 false**(与 team config 的 create 相反):已开启 auto-derivation 的 * identity 创建新版本会继承 `true`,若再默认立即激活,一次「存个草稿」就当场改掉了 * 全局派生模板。 */ createNotificationSourceTemplate(sourceId: string, body: UpsertNotificationSourceTemplateSdkBody, params?: { activate?: boolean; }): Promise; /** POST .../templates/:id/activate —— 激活某个已建版本(仍需总闸才会派生)。 */ activateNotificationSourceTemplate(sourceId: string, templateId: string): Promise; /** * PUT .../templates/auto-derivation —— 🔴 **自动派生总闸**(系统 admin only)。 * * 打开 = 全体「该 source 一条 config 都没有」的 team 在下一次帖子物化 / backfill / * **建订阅**时各自派生出一条 team config,其已发布帖随即开始产生节点事件。 * 按 identity 改**全部** version 行(rollout 状态是 identity 级共享状态)。 */ setNotificationSourceTemplateAutoDerivation(sourceId: string, autoDerivationEnabled: boolean): Promise<{ totalVersions: number; updatedVersions: number; autoDerivationEnabled: boolean; }>; /** GET .../accounts/:accountId/reddit-verification — current + 最近 N 条 observation。 */ getRedditAccountVerification(teamId: string, accountId: string, params?: { limit?: number; }): Promise; /** GET .../reddit-verification/accounts — 列表 + coverage 计数。 */ listRedditAccountVerificationAccounts(teamId: string, params?: { needsAttention?: boolean; limit?: number; offset?: number; }): Promise; /** * POST .../accounts/:accountId/reddit-verification/observations — 幂等键 * `requestId`;响应固定三态(201 recorded=true / 200 duplicate_delivery / * 409 两种专用冲突码,见 `SocialHubHttpError.code`)。 */ recordRedditAccountVerificationObservation(teamId: string, accountId: string, body: { observedState: "verification_required" | "app_labeled" | "verified"; observedAt: string; requestId: string; sourceEventType: string; deadlineRaw?: string | null; /** ⛔ 已作废(0165):由错误公式算出。改送 `deadlineDays`。 */ deadlineAt?: string | null; /** 页面显示的剩余天数(正整数);消歧由采集端按 DOM 结构做,服务端据此推区间。 */ deadlineDays?: number | null; /** Reddit 自述状态原值(如 `ACTION_NEEDED`);⛔ 原样送,不要归一(迁移 0164)。 */ platformStatusRaw?: string | null; /** Reddit `human-verification-case-id`;同一次判定的多次观测共享。 */ platformCaseId?: string | null; /** 取证时 `documentElement.lang` 实测值;⛔ 不可由时区/出口 IP 推断。 */ documentLang?: string | null; evidence: Array<{ kind: "dom_snapshot" | "screenshot" | "agent_narrative"; ref?: string | null; host?: string | null; capturedAt?: string | null; expiresAt?: string | null; }>; }): Promise<{ observationId: string; recorded: boolean; advanced: boolean; state: "unknown" | "verification_required" | "app_labeled" | "verified"; reason: "advanced" | "duplicate_delivery" | "out_of_order" | "same_instant_same_state"; }>; /** * PATCH .../reddit-verification/evidence/:evidenceId — 只改 availability; * `checkedAt` 必填,较旧的检查覆盖较新的结果会被拒(409)。 */ patchRedditAccountVerificationEvidence(teamId: string, evidenceId: string, body: { availability: "available" | "expired" | "missing" | "revoked"; checkedAt: string; }): Promise<{ ok: boolean; evidenceId: string; availability: string; availabilityCheckedAt: string; }>; /** GET /v1/system/subreddit-restrictions — 列表(state 多值、updatedSince 增量拉取)。 */ listSubredditRestrictions(params?: ListSubredditRestrictionsParams): Promise; /** GET /v1/system/subreddit-restrictions/:id — 单条(含全量 evidence)。 */ getSubredditRestriction(id: string): Promise; /** POST /v1/system/subreddit-restrictions — 新建(已有活跃限制 → 409 RESTRICTION_EXISTS)。 */ createSubredditRestriction(body: CreateSubredditRestrictionInput): Promise; /** POST .../:id/confirm — provisional / verification_overdue → confirmed(409 VERSION_MISMATCH / INVALID_STATE)。 */ confirmSubredditRestriction(id: string, body?: ConfirmSubredditRestrictionInput): Promise; /** POST .../:id/revoke — 解封(唯一的解封路径)。 */ revokeSubredditRestriction(id: string, body: RevokeSubredditRestrictionInput): Promise; /** GET .../:id/fanout — 扇出汇总 + 回执明细(阶段 B 之前为空、complete=false)。 */ getSubredditRestrictionFanout(id: string, params?: SubredditRestrictionFanoutParams): Promise; /** * GET /v1/teams/:teamId/subreddit-restrictions — team 级只读列表(`compliance:read`,manager 可用)。 * 过滤参数与系统级 list 相同,只回证据摘要。运营侧 agent 拉取状态走这个,不走系统级 admin 面。 */ listTeamSubredditRestrictions(teamId: string, params?: ListSubredditRestrictionsParams): Promise; /** * GET /v1/teams/:teamId/subreddit-restrictions/:id/fanout — 只统计、只返回**本 team** 的回执; * outbox 的 summary / lastError 为 null(跨 team 信息)。complete 仍按全局 outbox 判断。 */ getTeamSubredditRestrictionFanout(teamId: string, id: string, params?: Omit): Promise; /** POST /v1/teams/:teamId/subreddit-restrictions/report — runner 上报(只写 provisional 或 sanction)。 */ reportSubredditRestriction(teamId: string, body: ReportSubredditRestrictionInput): Promise; /** * GET /v1/teams/:teamId/subreddit-restrictions/check — runner 动手前自查。 * 账号可以用 `socialAccountId`(与 contracts 同名)或 `account`(CLI flag 同名)给;两者都给且不同 * → 直接抛错,绝不静默丢掉其中一个 —— 丢了账号就只判系统级,会得到假的 `allowed:true`。 */ checkSubredditRestriction(teamId: string, params: { subreddit: string; socialAccountId?: string; account?: string; }): Promise; } export type NotificationSubscriptionStatus = "active" | "paused" /** 游标落后于保留 floor,已无法保证不漏;需人工确认后 reset。 */ | "stale" | "deleted"; export type NotificationSubscriptionStartPosition = "now" | "earliest"; /** 同字段内多值 = OR,跨字段 = AND;空数组 = 不过滤该维度。 */ export type NotificationFiltersDto = { brandIds?: string[]; socialAccountIds?: string[]; subreddits?: string[]; }; export type NotificationSubscriptionDto = { id: string; teamId: string; name: string; sourceIds: string[]; filters: NotificationFiltersDto; requiredFields: string[]; /** 匹配面(sourceIds/filters/requiredFields)变化时 +1。 */ filterVersion: number; /** 「哪一代 pull 结果仍可推进游标」的身份;变了 → 在途 receipt 全部作废。 */ receiptEpoch: number; /** 连续 watermark,十进制字符串。 */ cursorSeq: string; /** 乐观锁,update DTO 的 CAS 用;**不进** receipt。 */ rowVersion: number; startPosition: NotificationSubscriptionStartPosition; status: NotificationSubscriptionStatus; lastPulledAt: string | null; createdAt: string; updatedAt: string; /** 服务端附加的归属信息(不在冻结契约里,仅供展示/排障)。 */ ownerUserId?: string | null; isOwner?: boolean; lastAckedAt?: string | null; staleAt?: string | null; }; /** `GET .../notification-events` 的运维摘要行(**不含 payload / includedFields**)。 */ export type NotificationEventSummaryDto = { eventId: string; seq: string; teamId: string; sourceId: string; canonicalTopic: string; dedupeKey: string; sourceConfigVersion: number; fieldSetVersion: string; payloadStatus: "active" | "expired"; maxSensitivity: string; requiredPermissions: string[]; brandId: string | null; socialAccountId: string | null; subreddit: string | null; occurredAt: string; emittedAt: string; }; export type CreateNotificationSubscriptionSdkBody = { name: string; /** **升序去重**(进 filterVersion 计算,不规范化会误伤在途 receipt)。 */ sourceIds: string[]; filters?: NotificationFiltersDto; /** 「没有这些字段就别发给我」;必须 ⊆ 当前生效 config 的 configuredFields。 */ requiredFields?: string[]; /** 默认 `now`,避免新机器一上线被历史事件淹没。 */ startPosition?: NotificationSubscriptionStartPosition; }; export type UpdateNotificationSubscriptionSdkBody = { /** 必填:并发改配置时的 CAS。 */ expectedRowVersion: number; name?: string; sourceIds?: string[]; filters?: NotificationFiltersDto; requiredFields?: string[]; /** pause/resume 就走这里(不能改成 stale/deleted)。 */ status?: "active" | "paused"; }; export type NotificationNodeSpecDto = { key: string; offsetSeconds: number; maxLatenessSeconds: number; }; export type UpsertNotificationSourceConfigSdkBody = { sourceId: string; nodes: NotificationNodeSpecDto[]; filters?: NotificationFiltersDto; /** 只能勾 registry `optionalFields`;required 不可勾也不可取消。 */ selectedFields?: string[]; retentionDays?: number; displayName: string; /** 展示用,**不参与**路由/过滤/去重。 */ displayTopicLabel?: string | null; /** * 创建即激活(`activate` 默认 true)时的 CAS 期望值 —— 与独立 activate 端点同语义。 * 🔴 少了它,「新建一个版本并激活」就是绕过 CAS 的后门。 * `activate: false` 时带它会 400(矛盾请求)。 */ expectedActive?: NotificationExpectedActiveDto; }; /** * 激活的 compare-and-set 期望值。 * - 字段缺省 = 不启用 CAS(last-serialized-writer-wins); * - `null` = 「我认为当前没有 active」; * - 对象 = 「我认为当前 active 就是这一行」(`version` 给了就一起校验)。 * * ⚠️ 刻意是**一个不可自相矛盾的对象**而不是两个平行字段: * 「只校验其中一个、静默忽略另一个」正是 CAS 最典型的失效方式。 */ export type NotificationExpectedActiveDto = null | { configId: string; version?: number; }; export type NotificationScheduleStatusDto = "scheduled" | "emitted" | "missed" | "cancelled" | "superseded"; export type NotificationNodeScheduleStatusCountsDto = { scheduled: number; emitted: number; missed: number; cancelled: number; superseded: number; }; /** * 该状态在请求窗口内的**完整性** —— 空结果的解释权全在这里。 * * - `full`:窗口整体在保证保留期内,**没有就是真的没有**; * - `partial`:窗口横跨保留 floor,更早的部分**可能**已被清理; * - `outside`:窗口整体早于 floor,看到的行只是每组永久保留的 sentinel 残留, * **不能**当作完整历史。 */ export type NotificationScheduleWindowCompletenessDto = "full" | "partial" | "outside"; /** * schedule 的保留期披露。 * * `notification_node_schedules` 按状态分档清理:`emitted` / `superseded` 180 天、 * `missed` / `cancelled` 730 天(判据是 `dueAt`);`scheduled` 永不删。 * ⚠️ `guaranteedRetainedFrom` 是**保证**保留到的最早 `dueAt`,不是「实际最老行」—— * GC 落后、批预算用完、每组永久 sentinel 都会让更老的行继续存在。 */ export type NotificationScheduleRetentionDto = { policy: { scheduled: { mode: "indefinite"; retentionDays: null; guaranteedRetainedFrom: null; }; } & Record<"emitted" | "superseded" | "missed" | "cancelled", { mode: "bounded"; retentionDays: number; guaranteedRetainedFrom: string; }>; completenessByStatus: Record; /** 仅在请求带 `status` 过滤时给出;不带过滤时为 null(五种状态 floor 不同,压不成标量)。 */ windowCompleteness: NotificationScheduleWindowCompletenessDto | null; }; export type NotificationNodeSchedulesSummaryDto = { /** 服务端补齐后的实际时间窗(`dueFrom` 含、`dueTo` 不含)。 */ window: { dueFrom: string; dueTo: string; }; items: { sourceId: string; counts: NotificationNodeScheduleStatusCountsDto; total: number; }[]; totals: NotificationNodeScheduleStatusCountsDto; /** 🔴 `missed: 0` 到底是「没异常」还是「已过保留期」,只能靠它区分。 */ retention: NotificationScheduleRetentionDto; /** * 逐 source 的就绪度。🔴 与 `counts` 同样不可省:全 0 的两种成因 *(窗口内没有到期节点 / 这个 team 压根没有 active config,物化一路静默跳过) * 只有它分得清。归属桶(未配置/外部)上「没人配过 config」是常态。 * * `reason` / `enabled` 只对能读 source config 的主体返回(桶上 = admin); * 其余主体只拿得到 `status`。 */ sourceReadiness: { sourceId: string; status: "ready" | "not_configured"; reason?: "active_config" | "configs_all_inactive" | "template_missing" | "template_disabled" | "template_registry_mismatch" | "template_invalid" | "derivable"; enabled?: boolean; }[]; }; /** * `notification_node_schedules` 保留期 GC cron 的运行状态 * (`GET /v1/settings/notification-schedule-purge-cron/status`)。 * * 🔴 **每个计数都可能是 `null`,而 `null` ≠ `0`**:`null` = 这次快照没有这个读数, * `0` = 真的跑了且是零。前端空态措辞只能依据 `hasRunSinceRegistration` + `status`。 */ export type NotificationSchedulePurgeCronStatusDto = { /** API 侧 env 回退值。⚠️ 不权威 —— worker 只认自己进程的环境变量。 */ schedule: string; /** * 推算 `nextRunAt` 时实际用到的表达式 = `registeredCron ?? schedule`。 * ⚠️ **不证明 worker 还活着**(没有 heartbeat),`nextRunAt` 因此是推算值。 */ effectiveCron: string; /** 按 `effectiveCron` **推算**的下一次触发;表达式非法时为 null。 */ nextRunAt: string | null; /** * 🔴 「跑过没有」的唯一判据(`runId !== null`)。 * ⚠️ 是「**自本次 worker 注册以来**」——worker 重启会把运行字段清回 null。 */ hasRunSinceRegistration: boolean; registeredCron: string | null; registeredAt: string | null; runId: string | null; /** `HOSTNAME#pid`:多实例下判断 success/failed 是否同一次运行。 */ workerId: string | null; startedAt: string | null; finishedAt: string | null; /** ⚠️ `null` = **没有可读的状态快照**(可能没起过,也可能状态写失败/被清/坏了)。 */ status: "registered" | "running" | "success" | "failed" | null; durationMs: number | null; /** 逐档删除数(状态档 → 行数);`{}` = 没有任何一档删到东西。 */ deletedByTier: Record; deleted: number | null; groupsPurged: number | null; /** ⚠️ **不是错误**:被 sentinel / `has_scheduled` 不变量有意保护、本轮跳过的组。 */ protectedGroups: number | null; /** ⚠️ **不是错误**:锁被物化/dispatcher 占着,下一轮自然重试。 */ lockBusyGroups: number | null; /** 🔴 唯一的组级真异常信号。 */ failedGroups: number | null; /** 撞到单次删除预算上限(容量信号,不是错误)。 */ budgetExhausted: boolean | null; /** 🔴 独立 `LIMIT 1` probe 判定:`true` = **确实还有**候选,不是「可能还有」。 */ backlog: boolean | null; lastError: string | null; }; /** * schedule 的归一化失败原因。 * * ⚠️ `processing_error` **不代表终态**:记账只 bump attempts,绝不按次数判死, * 所以它会出现在仍会重试的 `scheduled` 行上。要判终态看 `status`。 */ export type NotificationScheduleFailureReasonDto = "kill_switch" | "max_lateness_exceeded" | "processing_error"; /** `GET .../notification-node-schedules` 的行(**不含 payload、不含 lastError 原文**)。 */ export type NotificationNodeScheduleSummaryDto = { id: string; teamId: string; sourceId: string; subjectType: string; subjectId: string; sourceConfigVersion: number; policyVersion: string; nodeKey: string; offsetSeconds: number; maxLatenessSeconds: number; status: NotificationScheduleStatusDto; anchorAt: string; dueAt: string; emittedAt: string | null; attempts: number; failureReason: NotificationScheduleFailureReasonDto | null; cancelReason: string | null; brandId: string | null; socialAccountId: string | null; subreddit: string | null; createdAt: string; updatedAt: string; }; /** * 单条详情。 * * ⚠️ `status = "emitted"` 且 `eventId = null` 是**合法历史状态**:事件过 tombstone 期后 * 整行删除,FK 的 `ON DELETE SET NULL` 会清掉 `event_id`。别当数据损坏。 */ export type NotificationNodeScheduleDetailDto = NotificationNodeScheduleSummaryDto & { sourceDefinitionVersion: number; sourceConfigVersionId: string; fieldSetVersion: string; retentionDays: number; eventId: string | null; /** 关联事件的 seq(十进制串,**绝不 Number 化**);事件已清理或从未发射时为 null。 */ eventSeq: string | null; supersededByScheduleId: string | null; /** 是否存在原始异常文本(原文不出网)。 */ hasLastError: boolean; }; export type NotificationSourceConfigDto = { id: string; teamId: string; sourceId: string; version: number; enabled: boolean; nodes: NotificationNodeSpecDto[]; filters: NotificationFiltersDto; selectedFields: string[]; fieldSetVersion: string; retentionDays: number; displayName: string; displayTopicLabel: string | null; activatedAt: string | null; createdAt: string; updatedAt: string; /** 以下是服务端在冻结契约之外附加的只读字段(后台展示/排障用)。 */ isActive?: boolean; supersededAt?: string | null; sourceDefinitionVersion?: number; payloadSchemaVersion?: string; /** 配置级暴露政策(= fieldSetVersion 的哈希输入)。 */ configuredFields?: string[]; /** registry 推导的字段级 authz 并集(AND 语义),只读。 */ requiredPermissions?: string[]; /** * provenance(迁移 0127):`"template"` = 系统从模板派生,`"manual"` = 人工建的。 * 派生行**就是**真正的 team config(events / schedules 的组合租户外键指着它)。 */ origin?: "manual" | "template"; templateId?: string | null; templateVersion?: number | null; }; /** 历史通知字段收窄的服务端预览/执行结果;不会修改 subscription cursor。 */ export type NotificationHistoryNarrowResultDto = { applied: boolean; ready: boolean; sourceId: string; fromConfig: { id: string; version: number; fieldSetVersion: string; }; targetConfig: { id: string; version: number; fieldSetVersion: string; }; removedFields: string[]; restrictedFieldsRemoved: string[]; events: { scanned: number; active: number; expired: number; toNarrow: number; alreadyNarrowed: number; firstSeq: string | null; lastSeq: string | null; }; schedules: { toMigrate: number; subjectGroups: number; earliestDueAt: string | null; latestDueAt: string | null; conflictingTargetRows: number; }; blockers: string[]; selectionHash: string; changed: { eventsNarrowed: number; schedulesMigrated: number; auditEventId: string | null; }; }; /** * 系统级 source 模板(迁移 0127)。**没有 teamId** —— 它是一份系统级默认档位 + 字段集, * team 首次需要时派生出一条真正的 team config 行。 * * 🔴 `autoDerivationEnabled` **不是** kill switch:它只控制「还要不要给**新** team * bootstrap」,对已派生出来的 config 完全无效(那要用 team config 的 `enabled`)。 */ export type NotificationSourceTemplateDto = { id: string; sourceId: string; version: number; isActive: boolean; autoDerivationEnabled: boolean; nodes: NotificationNodeSpecDto[]; filters: NotificationFiltersDto; selectedFields: string[]; /** 创建时的 registry 快照,**仅供展示/审计**:派生时用当时的 registry 重算。 */ configuredFields: string[]; fieldSetVersion: string; requiredPermissions: string[]; retentionDays: number; displayName: string; displayTopicLabel: string | null; /** 与当前 registry **不完全相等**时派生会跳过(template_registry_mismatch)。 */ sourceDefinitionVersion: number; payloadSchemaVersion: string; activatedAt: string | null; supersededAt: string | null; createdAt: string; updatedAt: string; }; /** 创建模板新版本的写 body(镜像 `upsertNotificationSourceTemplateBodySchema`)。 */ export type UpsertNotificationSourceTemplateSdkBody = { /** 必须与路径上的 sourceId 一致,否则 400。 */ sourceId: string; nodes: NotificationNodeSpecDto[]; filters?: NotificationFiltersDto; /** 只能是 registry `optionalFields` 的子集,ASCII 升序去重。 */ selectedFields?: string[]; retentionDays?: number; displayName: string; displayTopicLabel?: string | null; }; /** * 事件信封。四层版本 + `includedFields` 决定形状,消费方按 `payloadSchemaVersion` 分支; * **遇到未知版本应拒绝处理但不 ack**(并打印 eventId/sourceId/payloadSchemaVersion)。 * * `payloadStatus === "expired"` 是 **tombstone**:`payload` 为 null、`includedFields` 为空。 * 收到它**不得**当业务事件处理,但**必须**照常 ack —— 否则一条过期事件会把游标永久卡死。 */ export type NotificationEventEnvelopeDto = { eventId: string; /** 十进制字符串(bigint)。 */ seq: string; teamId: string; sourceId: string; canonicalTopic: string; /** agent 侧幂等键(**不是** seq —— seq 只表示顺序)。 */ dedupeKey: string; envelopeVersion: number; sourceDefinitionVersion: number; payloadSchemaVersion: string; sourceConfigVersion: number; fieldSetVersion: string; occurredAt: string; emittedAt: string; payloadStatus: "active" | "expired"; includedFields: string[]; payload: Record | null; }; /** 不落库的签名凭据;ack 必须原样回传**整个对象**(不只是 signature)。 */ export type NotificationPullReceiptDto = { subscriptionId: string; fromCursorSeq: string; maxDeliveredSeq: string; filterVersion: number; receiptEpoch: number; expiresAt: string; signature: string; }; export type NotificationPullResponseDto = { /** 严格按 seq 升序的 `seq > cursorSeq` 前 N 条匹配事件。 */ events: NotificationEventEnvelopeDto[]; receipt: NotificationPullReceiptDto; /** = receipt.fromCursorSeq。 */ cursorSeq: string; /** 流末尾,供 agent 估算落后量;不参与任何校验。 */ latestSeq: string; }; /** * 十进制 seq 字符串的数值比较(BigInt,不走 Number —— 53 bit 之后会静默丢精度)。 * 与 contracts `compareNotificationSeq` 同口径;SDK 侧复刻是因为不能引 contracts。 */ export declare function compareNotificationSeqStrings(left: string, right: string): number; /** * 这条日历条目由谁发布 —— **防重复发帖的唯一闸门**(方案 D1)。 * - `hub_managed`(默认):Hub 同事务建内部 `publish_post` job,自己发;**不**产生指令事件。 * - `external_agent`:**不**建内部 job,改由 `publishing-calendar-directive.v1` 事件 * 驱动用户自己机器上的 agent 建 UTC 一次性 cron 去发。 * * 两者互斥。老服务端不回这个字段 —— 读到 `undefined` 按 `hub_managed` 处理。 */ export type CalendarEntryDeliveryMode = "hub_managed" | "external_agent"; /** 指令变体:期望状态是「该有一个 cron」还是「不该有」。 */ export type PublishingCalendarDirectiveKind = "upsert" | "cancel"; /** cancel 的原因(枚举而非自由文本:agent 按它分流告警 vs 正常运营动作)。 */ export type PublishingCalendarDirectiveCancelReason = /** 运营主动取消这条日历条目。 */ "entry_cancelled" /** 条目行被删除(含 plan/account 级联)。 */ | "entry_deleted" /** 审核由 passed 回退,内容不再允许发布。 */ | "review_revoked" /** 条目改回 `hub_managed`,改由 Hub 内部发布。 */ | "delivery_mode_changed" /** 关联草稿被解绑(`content_draft_id SET NULL`)。 */ | "draft_detached"; /** * preflight 的拒绝原因。**每一条都必须有本地处置动作**,不能笼统当成「失败重试」—— * 其中大部分是终态(该删 cron),轮询重试只会把一条已经不该发的内容一直挂着。 */ export type PublishingCalendarPreflightRejectionReason = /** 条目不存在、不属于本 team,或调用方品牌作用域看不到它。此时 `currentRevision` 恒为 null。 */ "entry_not_found" /** 条目已不是 `scheduled`(取消/发布中/已发/失败)。删 cron。 */ | "entry_not_scheduled" /** 条目已改回 `hub_managed`,Hub 自己发。删 cron。 */ | "delivery_mode_not_external" /** 手上的 revision 比 Hub 当前的旧 —— 还有一条更新的指令在路上。 */ | "revision_stale" /** 手上的 revision 比 Hub 当前的**新**。正常不该发生,一律不给内容。 */ | "revision_unknown" /** 条目没绑草稿,或草稿行已不在。 */ | "draft_detached" /** 当前草稿的 canonical review target 不存在、身份漂移,或状态不是 `passed`。 */ | "review_not_passed" /** * 发布护栏判定当前不可发布(账号被封/受限、cluster 暂停等)。 * **与其余 reason 不同:这条通常是暂时的,不要删 cron**,按原节奏重试即可。 * 护栏评估本身失败不走这条(那是 5xx 系统故障)。 */ | "guardrail_blocked"; /** preflight 放行:**唯一**能拿到正文的分支。 */ export type PublishingCalendarPreflightOkDto = { decision: "ok"; calendarEntryId: string; publishingPlanId: string; /** 服务端当前 revision(ok 时必然 === 请求里那个)。 */ revision: number; socialAccountId: string; subreddit: string; /** **Z 结尾的 UTC**;直接拿去建 UTC one-shot cron,不要先转本地时区。 */ plannedAt: string; content: { contentDraftId: string; contentDraftVersion: number; title: string; body: string; }; review: { targetId: string; /** 恒为 `passed` —— 没有第二种 ok。 */ status: "passed"; /** 服务端 canonical 内容 hash,可留痕对账。 */ contentHash: string; }; }; export type PublishingCalendarPreflightRejectedDto = { decision: "rejected"; calendarEntryId: string; reason: PublishingCalendarPreflightRejectionReason; /** * Hub 当前的 revision,**仅供诊断对账**。 * * 🔴 绝不能拿它推进本地的「已见最高 revision」:收到 rev 5 的 cron 被判 * `revision_stale/currentRevision=6` 后若把本地状态升到 6,随后真正到达的 rev 6 * `upsert` 会被当成旧消息忽略 —— 结果是**一个 cron 都没有**。只等真实的指令。 * * `entry_not_found` 时恒为 null(那个 reason 同时覆盖「真不存在」与「作用域看不到」)。 */ currentRevision: number | null; }; /** * preflight 判定结果。**业务拒绝走 200 + 本判别联合,不用 404/409**: * cron 路径要能无歧义分流每个 reason。判 `decision` 之前不要碰任何字段。 */ export type PublishingCalendarPreflightResultDto = PublishingCalendarPreflightOkDto | PublishingCalendarPreflightRejectedDto; /** * `GET .../calendar-entries` 的行形状(含 joined labels)。 * * 只被 {@link SocialHubClient.listCalendarEntriesTyped} 使用;旧的 * `listCalendarEntries` 仍返回 `unknown[]`,故意不动。 */ export type CalendarEntryListItemDto = { id: string; teamId: string; publishingPlanId: string; /** * R12(迁移 0153):**可为 null** —— `null` 表示这条条目所属计划显式声明了 * 无品牌提及(`plan.brandId` 与 `campaign.brandId` 都是 NULL)。 * * 读侧对 `brands` 已由 innerJoin 改成 **leftJoin**,无品牌行不再被静默丢掉, * 所以服务端**确实会**在这里回 null。契约真源: * `packages/contracts/src/publishing-plans.ts` 的 `calendarEntryListItemSchema`。 */ brandId: string | null; /** legacy;brand-first 建的计划为 null。 */ campaignId: string | null; contentDraftId: string | null; socialAccountId: string; subreddit: string; plannedAt: string; status: CalendarEntryStatus; /** 见 {@link CalendarEntryDeliveryMode};老数据/老服务端缺省视为 `hub_managed`。 */ deliveryMode?: CalendarEntryDeliveryMode; /** * 单调递增的指令版本,**高版本覆盖低版本**。只对 `external_agent` 有业务意义, * 但 `hub_managed` 也照常递增(改回外部时不会倒退)。缺省 = 老服务端,不是 0。 */ directiveRevision?: number; publishedAt: string | null; permalink: string | null; failureReason: string | null; createdAt: string; /** 迁移 0149 起维护;更早的真实修改时间已不可考。 */ updatedAt?: string; /** * R12:无品牌条目没有 `brands` 行可 join → **null**(不是空串)。 * ⚠️ 直接 `.toUpperCase()` / `.slice()` 会在运行时炸;先判空再用。 */ brandName: string | null; campaignName: string | null; socialAccountHandle: string | null; socialAccountPlatform: string; draftTitle: string | null; draftExcerpt: string | null; reviewGate?: { targetId: string | null; status: ReviewTargetStatus | null; contentVersion: number | null; /** target 是否对应当前草稿版本(canonical 全校验)。 */ isCurrent: boolean; gateState: ReviewGateState; lastError: string | null; } | null; }; /** 契约 `calendarEntryStatusSchema` 的镜像。 */ export type CalendarEntryStatus = "draft" | "scheduled" | "exported" | "running" | "succeeded" | "failed" | "cancelled"; /** 契约 `reviewTargetStatusSchema` 的镜像(审核 target 的原生状态)。 */ export type ReviewTargetStatus = "pending" | "running" | "passed" | "needs_revision" | "blocked" | "degraded" | "superseded"; /** 契约 `reviewGateStateSchema` 的镜像(日历条目上的审核门投影)。 */ export type ReviewGateState = "passed" | "waiting" | "revision_required" | "blocked" | "stale" | "untracked"; //# sourceMappingURL=index.d.ts.map