import type { BilibiliInternal } from './bilibili' /** * LAPLACE 内部 typings */ export declare namespace LaplaceInternal { /** * HTTP(S) 协议类型的 API */ export namespace HTTPS { /** * LAPLACE Workers */ export namespace Workers { /** * 获取云端弹幕机配置 * /laplace/remote-config */ export interface RemoteConfig { headerText?: string headerSubtitle?: string headerLink?: string headerLogo?: string headerLogoWidth?: number headerLogoHeight?: number footerExtraText?: string footerText?: string footerLink?: string } /** * Gatus status response * /laplace/network */ export interface NetworkItem { /** "miscs" */ group: string /** miscs_bilibili-fingerprint-reporter */ key: string /** bilibili-fingerprint-reporter */ name: string results: { status: number duration: number hostname?: string success: boolean /** 2024-04-28T08:22:07.19385135Z */ timestamp: string errors?: string[] conditionResults?: { /** "[CERTIFICATE_EXPIRATION] > 48h" */ condition: string success: boolean }[] }[] } /** * Login Sync info * A subset of `BilibiliInternal.HTTPS.Prod.LiveGetUserInfo` */ export interface LoginSyncInfo { /** 0: 成功 */ code: number message: string data: { isLogin: boolean face: string mid: number uname: string /** The actual `buvid` */ ack: string fetchMode: 'login-sync' | 'anonymous' | 'built-in' ackMode: 'guest' | 'passthrough' | 'builtin-passthrough' fetcher: number } } /** * 获取云端表情包 * /laplace/remote-emotes */ export interface PspRemoteEmotesProps { /** * - 0: valid result * - 403: not valid */ status: number data: { emoji: { server_base: string map: { [key: string]: string } } } } /** * 获取直播间自定义表情包 * /laplace/emotes/:roomId */ export interface Emotes { /** * - 0: valid result * - non-zero: error */ status: number data: { emotes: { map: { [key: string]: number } } } } /** * 直播间排名列表,由外部 API 聚合而来 * /laplace/ranking */ export interface RankingItem { /** * @minimum 1 */ _laplace_rank: number cate: string uname: string uid: number face: string cover: string system_cover: string start_time: number roomid: number watched?: number watched_avg?: number ten_minutes_counter: number interaction_avg?: number income?: number income_avg?: number danmakus?: number danmakus_avg?: number title: string tags?: string[] commentCount?: number lastLiveIncome?: number /** * laplace/loveava 独有的热度/人气字段 */ count?: number /** * 平均观看人数 */ count_avg?: number } /** 关注 API 的数据库扩展 */ interface FollowingResponseExtended extends BilibiliInternal.HTTPS.Prod.GameCenterFollowingList { /** * 最后 KV 缓存写入时间戳 */ last: number /** * 数据来源 */ from: 'cache' | 'origin' } /** FollowingV2 enrich 用户信息,仅查数据库(不请求上游),无记录时字段为空字符串 */ interface FollowingV2EnrichedUser { uid: number /** 用户名,数据库无记录时为空字符串 */ username: string /** * 关注状态(bilibili attribute 语义,从被查询用户视角): * - 0: 已取关(unfollowed 条目) * - 2: 单向关注 * - 6: 互相关注(仅当对方的关注列表也已入库时可判定) */ relation: number } /** FollowingV2 (feed attention list) 的数据库扩展,`?history=1` / `?enrich=1` 时携带 `laplace` 块 */ interface FollowingV2ResponseExtended extends BilibiliInternal.HTTPS.Prod.FeedAttentionList { laplace?: { /** 曾关注但已取关的用户,按取关观测时间倒序;时间均为观测时间(毫秒时间戳)。`?history=1` 时返回 */ unfollowed?: Array<{ uid: number lastSeenAt: number unfollowedAt: number }> /** 用户信息,覆盖关注列表与 `laplace.unfollowed` 的全部 uid,保持上游关注列表顺序、取关条目在后。`?enrich=1` 时返回,此时 `data.list` 一律为空数组 */ users?: FollowingV2EnrichedUser[] } } /** * 由 vtbs 整理来的直播间开播状态 */ interface LiveState { name: string room: number liveStatus: number lastLive: { time: number } title: string time: number liveStartTime: number } /** * UID by Room */ interface UidByRoom { /** * 0 正常,404 未找到直播间 */ status: 0 | 404 uid: number /** 真实房间号 */ roomId: number /** 短房间号 */ shortRoomId: number /** 用户名 */ username: string | null } /** * 哔哩哔哩装扮列表(分页) * @link /laplace/suits * * Query params: * - `?page=1` — page number (min 1, default 1) * - `?pageSize=100` — items per page (1–500, default 100) * - `?all=true` — return every row without pagination; `data` holds everything, `pageSize` equals `total`, `hasMore` is `false` * - `?includeUnassigned=true` — include non-fan suits (`user = 0`); default excludes them * - `?user=` — filter to fan suits belonging to this streamer (fan_mid) * - `?publisher=` — filter by publisher/distributor (`owner`) * - `?withDetails=true` — include `ownerDetails` on each item (joined owner user row) * - `?useCache=all` — serve archived R2 URLs for all Bilibili CDN assets (when mirrored) * - `?useCache=video` — serve archived R2 URLs for video assets only; images stay at origin */ export interface ListSuits { data: Suit[] total: number /** Current page (1-indexed); always `1` when `all=true` */ page: number /** Items per page; equals `total` when `all=true` */ pageSize: number /** Whether more pages follow this one; always `false` when `all=true` */ hasMore: boolean } /** * Single suit item returned by {@link ListSuits}. * The `properties`, `raw`, `createdAt`, and `updatedAt` columns are omitted by the query. */ export interface Suit { id: number name: string description: string | null /** Unix timestamp (seconds) of sale start */ startTime: number | null totalPurchaseCount: number | null /** Associated fan user (streamer) UID; `0` for non-fan suits */ user: number username: string /** Publisher/distributor UID (代发行商) */ owner: number image_cover: string | null /** Present only when `?withDetails=true`; a subset of the joined owner Bilibili user row */ ownerDetails?: { id: number room: number | null username: string guild: number | null avatar: string | null /** ISO 8601 timestamp */ updatedAt: string } | null } /** * 哔哩哔哩收藏集列表(分页) * @link /laplace/collections * * Query params: * - `?page=1` — page number (min 1, default 1) * - `?pageSize=100` — items per page (1–500, default 100) * - `?all=true` — return every row without pagination; `data` holds everything, `pageSize` equals `total`, `hasMore` is `false` * - `?user=` — filter to collections whose `relatedUsers` contains this UID * - `?useCache=all` — serve archived R2 URLs for all Bilibili CDN assets (when mirrored) * - `?useCache=video` — serve archived R2 URLs for video assets only; images stay at origin */ export interface ListCollections { data: Collection[] total: number /** Current page (1-indexed); always `1` when `all=true` */ page: number /** Items per page; equals `total` when `all=true` */ pageSize: number /** Whether more pages follow this one; always `false` when `all=true` */ hasMore: boolean } /** * Single collection item returned by {@link ListCollections}. * The `raw`, `createdAt`, and `updatedAt` columns are omitted by the query. */ export interface Collection { id: number name: string description: string | null /** Unix timestamp (seconds) of sale start */ startTime: number | null relatedUsers: number[] | null totalPreorderCount: number | null totalPurchaseCount: number | null /** @deprecated use `lottery_image` */ act_square_img: string | null lottery_image: string | null } /** * 日程表项目 * @link https://laplace.live/schedule */ export interface WeekItem { /** 年份,例如 2024 */ year: number /** 当前周数,例如 19 */ week: number /** 哔哩哔哩动态对应 ID,只需要填入 ID 即可 */ bilibili_url?: string events: EventItem[] } /** * Item of WeekItem events * 此处 type 为简化版 `string`,在 laplace 本体仓库中有详细定义 */ export interface EventItem { /** 日期字符串,格式 YYYY-MM-DD */ date: string /** 日程类型 */ type: string /** 日程标题 */ title: string /** 录播链接,此应填入完整 URL */ rec?: string /** 是否被取消,此字段用于 iCal 日历 */ cancelled?: boolean /** 取消原因 */ reason?: string /** 录播是否为第三方录入 */ fans_rec?: boolean } /** 多媒体可用语言 */ export type MediaLanguages = 'zh' | 'en' | 'ja' | 'yue' | 'kr' | 'ru' | 'es' | 'fr' /** * 多媒体项目 */ export interface MediaItem { ver: number /** 当前媒体语言,例如 `en` */ lang: MediaLanguages /** 日期 */ date: string /** 是否有切片,完整 URL */ clips?: string /** 标题 */ title: string /** alternate 名称,通常用于提供标题的无翻译原文 */ altName?: string /** 是否为官方发布 */ official?: boolean /** 作者/艺术家 */ author: string /** 备注内容 */ note?: string /** 是否为本站首发,LAPLACE 内部字段 */ exclusive?: boolean /** * 标签 * 如果一场直播内唱了多次同一首歌曲,会导致声称的文件名重复,用此属性定义额外后缀防止冲突 */ tags?: string } /** * 时间线项目 * @link https://laplace.live/timeline */ export interface TimelineItem { /** * - `hitokoto`: 一言 * - `video`: 视频 * - `event`: 事件 */ type: 'hitokoto' | 'video' | 'event' /** 内容 */ content: string /** 事件关联人物 */ from_who: string /** 是否含有音声,LAPLACE 内部字段 */ hasVoice?: boolean /** 音声对应文件名,LAPLACE 内部字段 */ voiceFilename?: string /** 事件发生点 */ from?: string /** 事件消息来源 */ source?: string /** 事件日期 */ date?: string } /** * 开发日志项目 * @link https://laplace.live/blog */ export interface BlogItem { id: number date: string title?: string content: string | null /** 给 Atom RSS 用的纯文本输出 */ plain?: string author?: string tags?: string[] } /** * SuperChat FoS * @link /laplace/superchat-fos */ export interface SuperChatFoS { code: number /** 报错时会返回 */ message?: string content: string moderated: boolean } /** * 用户信息 * @link /laplace/bilibili-user/:uid */ export interface BilibiliUser { /** 对应 UID */ id: number username: string /** * 之前泄漏的工会字段 * * @deprecated use `guildInfo` instead */ guild: string room: number liveFansCount: number liveFansMedal: string | null avatar: string | null /** 2024-12-15T03:44:13.788Z */ updatedAt: string /** * 工会详细信息 * * @deprecated use `guildInfo` instead */ guildDetails: { id: number name: string } | null /** 工会详细信息 */ guildInfo: { /** * @deprecated use `history` instead, latest guild is now migrated to history in Sep 25, 2025, 4:51:27 AM PDT */ current: { name: string /** Unix timestamp * * @example 1758039520166 */ updatedAt: number } | null history: { name: string /** Unix timestamp * * @example 1758039520166 */ updatedAt: number }[] } | null /** MCN 机构详细信息 (mcnId is internal-only, not exposed in public API) */ mcnInfo: { history: { /** MCN agency name */ mcnName: string /** Unix timestamp * * @example 1758039520166 */ updatedAt: number }[] } | null /** Resolved tags with usage counts. Only present when `?fields=tags` is requested. */ tags?: LaplaceInternal.HTTPS.Workers.TagWithCount[] } /** * 列表哔哩哔哩用户(分页) * @link /laplace/bilibili-users * * Query params: * - `?page=1` — page number (min 1, default 1) * - `?pageSize=100` — items per page (1–500, default 100) * - `?all=true` — return all rows without pagination * - `?tags=1,2,3` — filter to users who have all listed tag IDs (AND) * - `?search=keyword` — filter by UID (exact) or username (case-insensitive partial match) */ export interface ListBilibiliUsers { data: ListBilibiliUsersItem[] total: number } /** * Single user item returned by the list endpoint. * * Differs from {@link BilibiliUser} (single-user endpoint) because the list * query returns raw DB rows without relation joins or sanitization: * - `guild` is a raw numeric FK (not a resolved name string) * - `tags` is a raw `int[]` (not resolved {@link TagWithCount} objects) * - `mcnInfo.history` entries include `mcnId` (stripped in single-user endpoint) * - No `guildDetails` relation (only present when joined via single-user query) */ export interface ListBilibiliUsersItem { id: number room: number | null username: string liveFansCount: number | null liveFansMedal: string | null liveStatus: number | null /** @deprecated use `guildInfo` instead */ guild: number | null avatar: string | null guildInfo: { history: { name: string /** Unix timestamp */ updatedAt: number }[] } | null mcnInfo: { history: { mcnId: number mcnName: string /** Unix timestamp */ updatedAt: number }[] } | null /** Raw integer tag IDs assigned to this user */ tags: number[] | null /** ISO 8601 timestamp */ updatedAt: string } // ============================================ // Tag Types // ============================================ /** Tag definition (viewer-facing, no internal fields) */ export interface Tag { id: number name: string color: string | null /** Emoji fallback -- shown when `icon` is not set. When both exist, `icon` takes priority. */ emoji: string | null /** Icon URL -- takes priority over `emoji` when both are set. */ icon: string | null description: string | null } /** Tag with usage count (viewer-facing, entity type is not exposed) */ export interface TagWithCount extends Tag { /** Number of entities using this tag, computed on-the-fly for the requested entity type. */ count: number } /** Full tag definition (admin-facing, includes internal fields) */ export interface TagAdmin extends Tag { scope: string[] | null /** Per-entity-type usage counts, computed on-the-fly. Keys are entity type names (e.g. `bilibili_user`, `bilibili_meme`). */ entityCounts: Record createdAt: string updatedAt: string } /** Request body for POST /laplace/tags (create tag) */ export interface TagCreateRequest { name: string color?: string emoji?: string icon?: string description?: string scope?: string[] } /** Request body for PATCH /laplace/tags/:id (update tag) */ export interface TagUpdateRequest { name?: string color?: string | null emoji?: string | null icon?: string | null description?: string | null scope?: string[] | null } /** Request body for POST /laplace/tags/assign (assign tags to entity) */ export interface TagAssignRequest { tagIds: number[] entityType: string entityId: number } /** Request body for DELETE /laplace/tags/assign (unassign tags from entity) */ export interface TagUnassignRequest { tagIds: number[] entityType: string entityId: number } /** * 礼物配置信息 */ export interface RoomGiftItem extends BilibiliInternal.HTTPS.Prod.RoomGiftItemPick { /** 出现的时间 */ firstSeen?: Date /** 出现的直播间 */ firstSeenRoom?: number } /** * 礼物配置列表 * @link /bilibili/room-gift-config/25034104?list=1 */ export interface RoomGiftConfigList { code: number message: string ttl: number gift_ttl: number gift_version: number data: { list: RoomGiftItem[] } } /** * 获取直播分区列表 * @link /bilibili/live-areas */ export interface LiveAreas { /** 0 代表成功 */ code: number /** 成功时返回 "success" */ message: string /** 直播分区列表 */ data: LiveAreaItem[] } /** * 直播分区项 */ export interface LiveAreaItem { /** 分区 ID */ id: string /** 分区名称 */ name: string /** 父分区 ID */ parentId: string /** 父分区名称 */ parentName: string /** 完整分区名称,格式为 `${parentName}-${name}` */ cate: string /** 分区图标 URL */ icon: string } /** * Response interface for the transformed LiveGiftEffect data * Used by frontend to consume the simplified gift effect data * @example /bilibili/live-gift-effect/25034104 */ export interface LiveGiftEffect { code: number message: string data: LiveGiftEffectItem[] } /** * Simplified gift effect item with only necessary fields */ export interface LiveGiftEffectItem { type: number id: number bind_gift_ids: number[] video: string /** json config URL, only the hash part */ json: string } /** * Successful response for guild members list * @example /laplace/bilibili-guilds/:guildName */ export interface ListGuildMembers { success: true guild: string count: number members: LaplaceInternal.HTTPS.Workers.BilibiliUser[] } /** * Error response when guild is not found * @example /laplace/bilibili-guilds/:guildName */ export interface ListGuildMembersNotFound { success: false error: string guild: string } /** * Request body for guild info update * @example POST /laplace/guild-update */ export interface UpdateGuildInfoRequest { uid: number guildName: string updatedAt?: number } /** * Successful response for guild info update * @example POST /laplace/guild-update */ export interface UpdateGuildInfoResponse { success: true message: string uid: number guildName: string updatedAt: number } /** * Error response for guild info update * @example POST /laplace/guild-update */ export interface UpdateGuildInfoError { success: false error: string uid?: number details?: string } // ============================================ // Fertility Cycle Types // ============================================ /** * Fertility status enum values * * @since Jan 29, 2026 * @example GET /bilibili/fertility/:uid * @example GET /bilibili/fertilities */ export type FertilityStatus = 'menstruating' | 'fertile' | 'ovulating' | 'normal' /** * A single cycle history entry - tracks each period event * Stored in the `cycleHistory` JSONB column */ export interface CycleHistoryItem { /** Unix timestamp (ms) - when this period started */ periodStart: number /** Optional: days this period lasted (user may not track) */ periodLength?: number /** Unix timestamp (ms) - when this entry was recorded */ submittedAt: number /** Optional: URL or description of the evidence source (e.g., livestream clip, tweet) */ source?: string /** Optional: prediction basis live when this entry was added; absent on legacy and backfilled entries */ predictedWith?: CyclePredictionBasis } /** * What the live status projects the next period from * * @since Oct 4, 2026 */ export interface CyclePredictionBasis { /** Unix timestamp (ms) - latest recorded period start at the time */ lastPeriodStart: number /** Cycle length (days) in effect at the time */ effectiveCycleLength: number } /** * How far a recorded period landed from its predicted start. Ovulation is predicted at a fixed offset before the * next period (cycle day L - 14), so this is also the ovulation-date error. * * @since Oct 4, 2026 */ export interface PeriodPrediction { /** ISO date string (YYYY-MM-DD, CST) */ predictedStart: string /** Recorded minus predicted, in days: positive = came later than predicted */ errorDays: number /** `snapshot` = stored when the entry was added; `backtest` = recomputed from the entries before it */ method: 'snapshot' | 'backtest' } /** * Per-user summary of PeriodPrediction errors. Only periods recorded ~1 cycle after the previous one are * scored; gaps with unrecorded periods in between (≥ 1.5 cycles) and sub-21-day noise are skipped. * * @since Oct 4, 2026 */ export interface PredictionAccuracy { /** Mean absolute error in days (1 decimal); null when nothing is scored */ meanAbsErrorDays: number | null /** Mean signed error in days (1 decimal), positive = periods come later than predicted; null when nothing is scored */ meanErrorDays: number | null /** Number of scored periods */ samples: number } /** * History entry as returned by GET /laplace/fertility/:uid */ export interface FertilityHistoryEntry extends Omit { /** Null when unscored: first entry, noise gap, or unrecorded periods in between */ prediction: PeriodPrediction | null } /** * Result of fertility status calculation * Returned by calculateFertilityStatus utility */ export interface CycleCalculationResult { status: FertilityStatus /** Current day in the menstrual cycle (1-indexed) */ dayInCycle: number /** Calculated from history or user-stated preference */ effectiveCycleLength: number /** Calculated from history or user-stated preference */ effectivePeriodLength: number /** Calculated ovulation day (cycleLength - 14) */ ovulationDay: number /** Predicted next period start date */ nextPeriod: Date /** How many history entries used for calculation */ dataPoints: number /** * Number of complete cycles estimated to have passed since the last * observed period. Zero when data is fresh; non-zero when no recent * observation exists, in which case dayInCycle, status, and nextPeriod * are projected forward assuming the cycle continued normally. */ cyclesElapsedSinceObservation: number } /** * Response for GET /bilibili/fertility/:uid */ export interface FertilityUserResponse { uid: number user: { username: string avatar: string | null room: number | null /** 0 = offline, 1 = live, 2 = rotating */ liveStatus: number | null /** Number of live room membership */ liveFansCount: number | null /** Guild membership info */ guildInfo: { history: { name: string /** Unix timestamp in milliseconds */ updatedAt: number }[] } | null /** MCN agency membership info */ mcnInfo: { history: { /** MCN agency ID */ mcnId: number /** MCN agency name */ mcnName: string /** Unix timestamp in milliseconds */ updatedAt: number }[] } | null } | null status: FertilityStatus dayInCycle: number ovulationDay: number /** ISO date string (YYYY-MM-DD) */ nextPeriod: string effectiveCycleLength: number effectivePeriodLength: number dataPoints: number /** * Complete cycles estimated to have passed since the last observed * period. Zero when data is fresh; non-zero indicates dayInCycle and * status are projected from a stale observation. */ cyclesElapsedSinceObservation: number /** How far recorded periods landed from our predictions */ predictionAccuracy: PredictionAccuracy userPreferences: { cycleLength: number periodLength: number } history: FertilityHistoryEntry[] } /** * Single user item in list response * @example GET /bilibili/fertilities */ export interface FertilityListItem { uid: number user: { username: string avatar: string | null room: number | null /** 0 = offline, 1 = live, 2 = rotating */ liveStatus: number | null /** Number of live room followers */ liveFansCount: number | null /** Guild membership info */ guildInfo: { history: { name: string /** Unix timestamp in milliseconds */ updatedAt: number }[] } | null /** MCN agency membership info */ mcnInfo: { history: { /** MCN agency ID */ mcnId: number /** MCN agency name */ mcnName: string /** Unix timestamp in milliseconds */ updatedAt: number }[] } | null } | null status: FertilityStatus dayInCycle: number /** Expected ovulation day in cycle (effectiveCycleLength - 14) */ ovulationDay: number /** ISO date string (YYYY-MM-DD) */ nextPeriod: string /** Calculated cycle length based on history (median) or user preference */ effectiveCycleLength: number /** Calculated period length based on history (median) or user preference */ effectivePeriodLength: number /** Number of history entries used for calculation */ dataPoints: number /** * Complete cycles estimated to have passed since the last observed * period. Zero when data is fresh; non-zero indicates dayInCycle and * status are projected from a stale observation. */ cyclesElapsedSinceObservation: number /** How far recorded periods landed from our predictions */ predictionAccuracy: PredictionAccuracy } /** * Response for GET /bilibili/fertilities * * Query params: * - `?showOutdated=true` — include users with no period recorded in the last 365 days; hidden by default since Oct 6, 2026 */ export interface FertilityListResponse { data: FertilityListItem[] total: number } /** * Request body for POST /bilibili/fertility-submit */ export interface FertilitySubmitRequest { /** Bilibili user ID */ uid: number /** Unix timestamp (ms) when period started */ periodStart: number /** Optional: How many days this period lasted (1-15) */ periodLength?: number /** Optional: Update user's preferred cycle length (15-60) */ cycleLength?: number /** Optional: Evidence URL (must be a valid URL, max 256 chars) */ source?: string } /** * Success response for POST /bilibili/fertility-submit */ export interface FertilitySubmitResponse { success: true uid: number historyCount: number /** ISO timestamp string */ lastPeriodStart: string } // ============================================ // Fertility Cycle Entry Management (Admin) // ============================================ /** * Request body for PATCH /laplace/fertility-entry/:uid * Updates a specific history entry identified by originalPeriodStart */ export interface FertilityEntryUpdateRequest { /** Required: periodStart of the entry to update (identifier) */ originalPeriodStart: number /** Optional: New periodStart value (Unix timestamp ms) */ periodStart?: number /** Optional: Period length (1-15 days), or null to clear */ periodLength?: number | null /** Optional: Evidence URL, or null to clear */ source?: string | null } /** * Request body for DELETE /laplace/fertility-entry/:uid * Deletes a specific history entry identified by periodStart */ export interface FertilityEntryDeleteRequest { /** Required: periodStart of the entry to delete */ periodStart: number } /** * Response for PATCH/DELETE /laplace/fertility-entry/:uid */ export interface FertilityEntryActionResponse { success: true uid: number historyCount: number /** ISO timestamp string */ lastPeriodStart: string } /** * Response for DELETE /laplace/fertility-delete/:uid * Deletes the entire fertility cycle record and any pending contributions */ export interface FertilityDeleteResponse { success: true uid: number /** Number of pending contributions that were also deleted */ deletedContributions: number } // ============================================ // Fertility Contribution Types (Anonymous Submissions) // ============================================ /** * Status of a fertility contribution */ export type ContributionStatus = 'pending' | 'approved' | 'rejected' /** * Request body for POST /laplace/fertility-contribute * Anonymous users submit period data with Turnstile verification */ export interface FertilityContributeRequest { /** Bilibili user ID */ uid: number /** Unix timestamp (ms) when period started */ periodStart: number /** Optional: How many days this period lasted (1-15) */ periodLength?: number /** Optional: Update user's preferred cycle length (15-60) */ cycleLength?: number /** Evidence URL (required, must be HTTP/HTTPS, max 256 chars) */ source: string /** Cloudflare Turnstile token for bot verification */ turnstileToken: string } /** * A fertility contribution record * Returned by GET /laplace/fertility-contributions */ export interface FertilityContribution { id: number uid: number periodStart: number periodLength?: number cycleLength?: number source?: string status: ContributionStatus contributorIp?: string /** ISO timestamp string */ createdAt: string /** ISO timestamp string */ reviewedAt?: string rejectReason?: string /** AI review state (see FertilityAiStatus). Null when never checked. */ aiStatus?: FertilityAiStatus | null /** AI review verdict (see FertilityAiVerdict). Null when running, errored, or skipped without a verdict. */ aiVerdict?: FertilityAiVerdict | null /** AI review per-attempt payload (see FertilityAiData). Null when never checked. */ aiData?: FertilityAiData | null } /** * Query parameters for GET /laplace/fertility-contributions */ export interface FertilityContributionsListQuery { /** Filter by status */ status?: ContributionStatus /** Filter by target user UID */ uid?: number /** Maximum number of results (1-500, default: 100) */ limit?: number /** Offset for pagination (default: 0) */ offset?: number /** Field to sort by (default: 'createdAt') */ sortBy?: 'createdAt' | 'reviewedAt' /** Sort order (default: 'desc') */ sort?: 'asc' | 'desc' } /** * Response for GET /laplace/fertility-contributions */ export interface FertilityContributionsListResponse { data: FertilityContribution[] total: number } /** * Success response for POST /laplace/fertility-contribute */ export interface FertilityContributeResponse { success: true id: number message: string } /** * Request body for POST /laplace/fertility-contribution-reject/:id */ export interface FertilityContributionRejectRequest { reason?: string } /** * Response for approve/reject endpoints */ export interface FertilityContributionActionResponse { success: true id: number status: ContributionStatus } /** * Request body for PATCH /laplace/fertility-contribution-update/:id * All fields are optional - only provided fields will be updated */ export interface FertilityContributionUpdateRequest { /** Bilibili user ID */ uid?: number /** Unix timestamp (ms) when period started */ periodStart?: number /** How many days this period lasted (1-15), or null to clear */ periodLength?: number | null /** User's preferred cycle length (15-60), or null to clear */ cycleLength?: number | null /** Evidence URL (must be HTTP/HTTPS, max 256 chars), or null to remove */ source?: string | null } /** * Response for PATCH /laplace/fertility-contribution-update/:id */ export interface FertilityContributionUpdateResponse { success: true id: number contribution?: FertilityContribution } /** * Response for GET /laplace/fertility-contribution-stats * Public endpoint showing contribution statistics */ export interface FertilityContributionStatsResponse { pending: number approved: number rejected: number total: number /** Average time from submission to approval/rejection in milliseconds */ avgProcessingTimeMs: number | null /** Average time from submission to approval/rejection in hours (1 decimal) */ avgProcessingTimeHours: number | null /** Site-wide accuracy of our period/ovulation predictions */ predictionAccuracy: SitePredictionAccuracy } /** * Site-wide prediction accuracy for one CST calendar month of recorded periods * * @since Oct 4, 2026 */ export interface PredictionAccuracyMonth extends PredictionAccuracy { /** YYYY-MM (CST) of the recorded period start */ month: string } /** * Every user's scored periods pooled (scoring rules: see PredictionAccuracy) * * @since Oct 4, 2026 */ export interface SitePredictionAccuracy { /** All scored periods across all users */ overall: PredictionAccuracy /** Users with at least one scored period */ users: number /** Ascending; months without scored periods are omitted */ months: PredictionAccuracyMonth[] } // ============================================ // Fertility AI Check Types // ============================================ /** * AI review session lifecycle on our side. * - `running` — Browser Use session dispatched, waiting for terminal status * - `done` — verdict landed (see aiVerdict) * - `errored` — Browser Use failed or returned non-conforming output * - `skipped` — source URL was off-list (e.g. not a Bilibili page); never dispatched */ export type FertilityAiStatus = 'running' | 'done' | 'errored' | 'skipped' /** Verdict produced by evaluating the AI's structured output against the contribution's claim. */ export type FertilityAiVerdict = 'valid' | 'invalid' | 'inconclusive' /** * What kicked off the check. * - `admin` — an admin pressed the button (POST /laplace/fertility-contribution-ai-check/:id) * - `submit` — fired automatically in the background when the contributor submitted * * Absent on rows written before the auto-check existed; treat undefined as `admin`. */ export type FertilityAiTrigger = 'admin' | 'submit' /** * What the system did automatically based on the verdict. * - `approved` — verdict was `valid`; approveFertilityContribution succeeded * - `flagged` — verdict was `invalid` or `inconclusive`; row stays pending for admin review * - `approve_failed_duplicate` — verdict was `valid` but approval bounced because the period already exists in production * - `skipped_unsupported` — source URL was not Bilibili; no AI call made */ export type FertilityAiAutoActionResult = | 'approved' | 'flagged' | 'approve_failed_duplicate' | 'skipped_unsupported' /** * Structured output the Browser Use agent returns (matches FERTILITY_AI_OUTPUT_SCHEMA). */ export interface FertilityAiEvidence { extractedUid: number | null uidMatches: boolean /** * YYYY-MM-DD in Asia/Shanghai; the date the content claims the period began. * * Disambiguation rules baked into the AI prompt: * - Explicit qualifier ("今天/昨天/上周三") → resolved against post date. * - No qualifier at all ("魔法期来了") → treated as "now", uses post date. * - Vague non-anchorable past ("前几天/最近/上周") → null. */ extractedPeriodStartDate: string | null /** YYYY-MM-DD in Asia/Shanghai; the post's publish date (auxiliary). */ postedAt: string | null contentMentionsPeriod: boolean evidenceQuote: string | null pageType: 'video' | 'dynamic' | 'livestream_replay' | 'space' | 'unknown' confidence: number notes: string | null } /** * Payload stored in bilibili_fertility_contributions.ai_data (jsonb). * Used both by the workers and by Drizzle via $type<>(). */ export interface FertilityAiData { sessionId: string model: string liveUrl?: string /** What kicked off the check. Undefined on rows predating the auto-check; treat as `admin`. */ trigger?: FertilityAiTrigger /** ISO 8601 */ startedAt: string /** ISO 8601, set when status leaves 'running' */ checkedAt?: string costUsd?: string confidence?: number evidence?: FertilityAiEvidence autoActionResult?: FertilityAiAutoActionResult errorMessage?: string } /** * Response shape for POST /laplace/fertility-contribution-ai-check/:id * and GET /laplace/fertility-contribution-ai-check/:id. */ export interface FertilityAiCheckResponse { id: number contributionStatus: ContributionStatus ai: { status: FertilityAiStatus | null verdict: FertilityAiVerdict | null data: FertilityAiData | null } } // ============================================ // Meme Types // ============================================ /** Meme list item with resolved tags and user info (GET /laplace/memes) */ export interface Meme { id: number uid: number content: string tags: TagWithCount[] copyCount: number lastCopiedAt: string | null createdAt: string updatedAt: string } /** Meme with user details (list response item) */ export interface MemeWithUser extends Meme { username: string | null avatar: string | null room: number | null } /** Query params for GET /laplace/memes */ export interface MemeListQuery { uid?: number roomId?: number tags?: number[] sortBy?: 'copyCount' | 'lastCopiedAt' | 'createdAt' sort?: 'asc' | 'desc' limit?: number offset?: number } /** Response for GET /laplace/memes */ export interface MemeListResponse { data: MemeWithUser[] total: number } /** Request body for POST /laplace/memes (admin direct add) */ export interface MemeCreateRequest { uid: number content: string } /** Request body for PATCH /laplace/memes/:id (update approved meme) */ export interface MemeUpdateRequest { content?: string tags?: number[] | null } /** Response for POST /laplace/memes and DELETE /laplace/memes/:id (admin only) */ export interface MemeActionResponse { success: true id: number } /** Request body for POST /laplace/meme-contribute */ export interface MemeContributeRequest { uid: number content: string note?: string /** Existing tag IDs to suggest for this meme (max 10) */ tags?: number[] turnstileToken: string } /** Success response for POST /laplace/meme-contribute */ export interface MemeContributeResponse { success: true id: number message: string } /** Meme contribution record (GET /laplace/meme-contributions) */ export interface MemeContribution { id: number uid: number content: string note?: string | null tags?: number[] | null status: ContributionStatus contributorIp?: string createdAt: string reviewedAt?: string rejectReason?: string } /** Query params for GET /laplace/meme-contributions */ export interface MemeContributionsListQuery { status?: ContributionStatus uid?: number limit?: number offset?: number sortBy?: 'createdAt' | 'reviewedAt' sort?: 'asc' | 'desc' } /** Response for GET /laplace/meme-contributions */ export interface MemeContributionsListResponse { data: MemeContribution[] total: number } /** Request body for POST /laplace/meme-contribution-reject/:id */ export interface MemeContributionRejectRequest { reason?: string } /** Response for approve/reject meme contribution */ export interface MemeContributionActionResponse { success: true id: number status: ContributionStatus } /** Request body for PATCH /laplace/meme-contribution-update/:id */ export interface MemeContributionUpdateRequest { uid?: number content?: string note?: string | null tags?: number[] | null } /** Response for PATCH /laplace/meme-contribution-update/:id */ export interface MemeContributionUpdateResponse { success: true id: number contribution?: MemeContribution } /** Response for GET /laplace/meme-contribution-stats */ export interface MemeContributionStatsResponse { pending: number approved: number rejected: number total: number avgProcessingTimeMs: number | null avgProcessingTimeHours: number | null } /** Response for POST /laplace/meme-copy/:id */ export interface MemeCopyResponse { success: true id: number copyCount: number } // ============================================ // Guild History Types // ============================================ /** * A single guild history entry - tracks each guild membership change * Stored in the `guildInfo.history` JSONB column on bilibili_users table */ export interface GuildHistoryItem { /** Guild name */ name: string /** Unix timestamp (ms) - when this guild membership was recorded */ updatedAt: number } // ============================================ // MCN History Types // ============================================ /** * A single MCN agency history entry - tracks each MCN membership change * Stored in the `mcnInfo.history` JSONB column on bilibili_users table */ export interface McnHistoryItem { /** MCN agency ID */ mcnId: number /** MCN agency name */ mcnName: string /** Unix timestamp (ms) - when this MCN membership was recorded */ updatedAt: number } /** * Response for GET /laplace/guild-entry/:uid * Returns guild history for a specific user */ export interface GuildHistoryResponse { success: true uid: number user: { id: number username: string avatar: string | null room: number | null } history: GuildHistoryItem[] historyCount: number } /** * Request body for POST /laplace/guild-entry/:uid * Adds a new guild history entry */ export interface GuildEntryAddRequest { /** Guild name (max 100 chars, no special characters) */ name: string /** Optional: Unix timestamp (ms) - defaults to current time */ updatedAt?: number } /** * Request body for PATCH /laplace/guild-entry/:uid * Updates a specific guild history entry identified by originalUpdatedAt */ export interface GuildEntryUpdateRequest { /** Required: updatedAt of the entry to update (identifier) */ originalUpdatedAt: number /** Optional: New guild name */ name?: string /** Optional: New timestamp (Unix timestamp ms) */ updatedAt?: number } /** * Request body for DELETE /laplace/guild-entry/:uid * Deletes a specific guild history entry identified by updatedAt */ export interface GuildEntryDeleteRequest { /** Required: updatedAt of the entry to delete */ updatedAt: number } /** * Response for POST/PATCH/DELETE /laplace/guild-entry/:uid */ export interface GuildEntryActionResponse { success: true uid: number historyCount: number } // ============================================ // MCN Batch Update Types // ============================================ /** * A single entry in the MCN batch update request * Maps to the raw export format from the MCN agency data (laplace-scripts) */ export interface McnBatchUpdateEntry { /** User's Bilibili UID (maps to bilibili_users.id) */ upper_mid: number /** MCN agency ID */ mcn_id: number /** MCN agency company name */ mcn_company_name: string } /** * Request body for POST /laplace/mcn-batch-update * Batch updates MCN info for multiple users (max 10,000 entries) */ export interface McnBatchUpdateRequest { entries: McnBatchUpdateEntry[] } /** * Response for POST /laplace/mcn-batch-update */ export interface McnBatchUpdateResponse { success: true /** Total entries received */ total: number /** Count of users whose mcnInfo was changed */ updated: number /** UIDs whose mcnInfo was changed (new MCN or MCN change) */ updatedUids: number[] /** Count of users whose MCN matched the latest history entry (no-op) */ unchanged: number /** UIDs whose MCN matched the latest history entry (no-op) */ unchangedUids: number[] /** Count of UIDs not found in bilibili_users */ skipped: number /** List of missing UIDs */ skippedUids: number[] } // ============================================ // Guild Batch Update Types // ============================================ /** * A single entry in the guild batch update request * Maps to the raw export format from the agency data */ export interface GuildBatchUpdateEntry { /** User's Bilibili UID (maps to bilibili_users.id) */ upper_mid: number /** Guild name (null if user is not in any guild) */ live_guild_name: string | null } /** * Request body for POST /laplace/guild-batch-update * Batch updates guild info for multiple users (max 10,000 entries) */ export interface GuildBatchUpdateRequest { entries: GuildBatchUpdateEntry[] } /** * Response for POST /laplace/guild-batch-update */ export interface GuildBatchUpdateResponse { success: true /** Total entries received */ total: number /** Count of users whose guildInfo was changed */ updated: number /** UIDs whose guildInfo was changed (new guild or guild change) */ updatedUids: number[] /** Count of users whose guild matched the latest history entry (no-op) */ unchanged: number /** UIDs whose guild matched the latest history entry (no-op) */ unchangedUids: number[] /** Count of UIDs not found in bilibili_users */ skipped: number /** List of missing UIDs */ skippedUids: number[] } } /** Migrated from experiments.sparanoid.net */ export namespace Experiments { /** * fixer.io API * @link http://data.fixer.io/api/latest?access_key= * */ interface Fixer { success: boolean timestamp: number base: string date: string rates: FixerRates } /** Props of Fixer */ interface FixerRates { [key: string]: number } /** * Spotify API */ export namespace Spotify { /** * Spotify get access token * @link https://developer.spotify.com/documentation/web-api/tutorials/getting-started */ interface Token { access_token: string token_type: 'Bearer' expires_in: 3600 } /** * Search result */ export interface Search { tracks: Tracks } /** Track list of `Search` */ export interface Tracks { href: string items: Track[] | [] limit: number next: string | null offset: number previous: null total: number } /** Track item of `Tracks` */ export interface Track { album: Album artists: Artist[] available_markets: string[] disc_number: number duration_ms: number explicit: boolean external_ids: ExternalIDS external_urls: ExternalUrls href: string id: string is_local: boolean name: string popularity: number preview_url: null track_number: number type: string uri: string } /** Album item of `Track` */ export interface Album { album_type: string artists: Artist[] available_markets: string[] external_urls: ExternalUrls href: string id: string images: Image[] | [] name: string release_date: Date release_date_precision: string total_tracks: number type: string uri: string } /** Artist props */ export interface Artist { external_urls: ExternalUrls href: string id: string name: string type: string uri: string } /** External URLs */ export interface ExternalUrls { spotify: string } /** 专辑封面 */ export interface Image { height: number url: string width: number } /** External IDs */ export interface ExternalIDS { isrc: string } } /** Apple API */ export namespace Apple { /** Apple Music API */ export interface Music { results: MusicSearchResults meta: MusicSearchMeta } /** Props of `Music` */ export interface MusicSearchMeta { results: MusicSearchMetaResults } /** Props of `MusicSearchMeta` */ export interface MusicSearchMetaResults { order: string[] rawOrder: string[] } /** Props of `Music` */ export interface MusicSearchResults { songs: Songs } /** Props of `Songs` */ export interface Songs { href: string next: string data: Datum[] } /** Props of `Datum` */ export interface Datum { id: string type: string href: string attributes: Attributes } /** Props of `Attributes` */ export interface Attributes { albumName: string genreNames: string[] trackNumber: number durationInMillis: number releaseDate: Date isrc: string artwork: Artwork playParams: PlayParams url: string discNumber: number hasCredits: boolean isAppleDigitalMaster: boolean hasLyrics: boolean name: string previews: Preview[] artistName: string composerName?: string } /** Props of `Attributes` */ export interface Artwork { width: number height: number url: string bgColor: string textColor1: string textColor2: string textColor3: string textColor4: string } /** Props of `Attributes` */ export interface PlayParams { id: string kind: string } /** Props of `Attributes` */ export interface Preview { url: string } } /** * Address Completion * @link /experiments/address-completion */ export interface AddressCompletion { code: number /** 报错时会返回 */ message?: string data: { /** 地址信息,通常为 `poiInfo` + `houseInfo` */ addrInfo: string /** 门牌号,通常为 address line 2 */ houseInfo: string /** 详细地址,通常为 address line 1 */ poiInfo: string /** 街道 */ street: string /** 城市 */ city: string /** 地区 */ district: string /** 省 */ province: string /** 收件人 */ person: string /** 手机/电话 */ phone: string } } } } /** * WebSocket 协议 */ export namespace WebSocket { /** * 线上环境 */ export namespace Prod { /** * 内置系统消息 */ export interface SYSTEM_MSG { timestamp: number username?: string message?: string } } } }