/** * Toy JS SDK 类型声明(面向创作者) * * 用法:把本文件放进你的 Toy 项目(如 `types/toy.d.ts`),TypeScript 会自动加载, * 无需 import 即可获得 `window.toy` 的类型检查与补全。 * * 前置:页面 `` 中引入 * * 加载后 SDK 把实例挂到全局 `window.toy`,所有方法返回 Promise。 * * 通用约定: * - 所有方法抛出的 Error,message 统一带 `[ToySDK]` 前缀。 * - 数据类能力(用户 / 作者 / 视频互动)不抛错时通过 `status` 字段表达结果, * 需要先判断 `status === 'ok'` 再读 `data` / `items`。 * - 云存储与排行榜失败时 Promise reject,需要 try/catch。 */ declare namespace ToySDK { // --------------------------------------------------------------------------- // 页面跳转 // --------------------------------------------------------------------------- /** 站内跳转的目标页面类型。 */ type NavigateType = 'video' | 'space' | 'search' | 'opus' | 'tribee' | 'toy' interface NavigateReq { /** 目标页面类型。 */ type: NavigateType /** * 资源标识,随 `type` 变化: * - video: BV 号,如 `BV1Hh411S7Ys` * - space: 用户 mid * - search: 搜索关键词(SDK 内部会做 URL 编码) * - opus: 图文 / 动态 id * - tribee: 小站 id * - toy: toy id */ id: string /** 额外查询参数,拼接到目标 URL 上透传给目标页面。 */ extra?: Record } // --------------------------------------------------------------------------- // 保存图片 // --------------------------------------------------------------------------- /** `url` 与 `base64Data` 二选一,两者都不传则由客户端返回错误。 */ interface SaveImageReq { /** 网络图片地址。 */ url?: string /** base64 图片数据。 */ base64Data?: string /** 申请相册权限时展示给用户的提示文案。 */ hintMsg?: string } interface SaveImageResp { /** 保存后的本地文件路径,由客户端返回。 */ localPath: string } // --------------------------------------------------------------------------- // 分享与二维码 // --------------------------------------------------------------------------- interface ShareReq { /** * 相对当前 Toy 页面根(`/toy//`)的路径,可带 query,如 `result.html?score=100`。 * * 完整分享链接由平台生成,Toy 不能自行指定完整 URL。路径只能落在当前 Toy 内: * 传绝对 URL 或用 `../` 越界到其他 Toy / 外域会抛 `invalid_param`。 */ path: string } interface QrCodeReq { /** * 相对当前 Toy 页面根(`/toy//`)的路径,可带 query。 * 不传(或传空串)时指向当前 Toy 首页 `index.html`。 * * 传了则与 `ShareReq.path` 同一约定:二维码内容由平台生成, * Toy 不能自行指定完整 URL;传绝对 URL 或用 `../` 越界到其他 Toy / 外域会抛 `invalid_param`。 */ path?: string /** 二维码边长(像素),取值区间 `[80, 1024]` 的整数。不传默认 `320`,越界或非整数抛 `invalid_param`。 */ size?: number } interface QrCodeResp { /** PNG 图片的完整 data URL(`data:image/png;base64,...`),可直接赋给 `img.src`。 */ base64: string /** 二维码实际编码的完整链接,由平台生成。 */ url: string } // --------------------------------------------------------------------------- // 用户信息 // --------------------------------------------------------------------------- interface UserProfileResp { /** 头像地址,SDK 已统一归一化为 https 与 p0 CDN 域名。 */ avatar: string /** 昵称。 */ nickname: string /** 当前登录用户在当前 Toy 内的稳定假名标识,不是鉴权凭证;功能未启用时可能不返回。 */ toyOpenId?: string } // --------------------------------------------------------------------------- // 数据类能力的状态枚举 // --------------------------------------------------------------------------- /** * 数据类能力的整体状态。 * - `ok`: 成功 * - `partial`: 批量请求部分成功,逐项状态见各 item 的 `status` * - `unauthorized`: 未登录 * - `denied`: 用户拒绝了数据使用确认 * - `unsupported`: 当前环境不支持(如外部手机浏览器,SDK 会引导打开 B站 App) * - `toy_context_unavailable`: 拿不到当前 toy 上下文 * - `author_mismatch`: 请求的资源不属于当前 Toy 作者(视频既非其投稿,也未以联合投稿身份参与创作) * - `video_not_found`: 视频不存在 * - `video_invisible`: 视频对当前用户不可见 * - `unavailable`: 依赖服务不可用 * - `invalid_argument`: 参数非法 */ type ToyDataStatus = | 'ok' | 'partial' | 'unauthorized' | 'denied' | 'unsupported' | 'toy_context_unavailable' | 'author_mismatch' | 'video_not_found' | 'video_invisible' | 'unavailable' | 'invalid_argument' /** 批量结果中单项的状态:与 `ToyDataStatus` 相同,但不会是 `partial`。 */ type ToyItemStatus = Exclude // --------------------------------------------------------------------------- // 作者资料 // --------------------------------------------------------------------------- /** 作者认证信息。`role` / `type` 为后端数字枚举,SDK 未定义其取值含义。 */ interface AuthorCertification { role: number title: string description: string type: number } /** 充电聚合信息。 */ interface ChargingSummary { /** 充电人数。 */ count: number display?: { show: boolean text?: string } } /** 粉丝勋章配置。 */ interface FanMedalConfig { /** 勋章名称。 */ name: string /** 是否已开启粉丝勋章。 */ enabled: boolean /** 各等级的亲密度区间;最高等级无上限时 `maxIntimacy` 缺省。 */ levels: Array<{ level: number minIntimacy: number maxIntimacy?: number }> } interface AuthorProfile { nickname: string avatar: string /** 个性签名。 */ sign: string /** 未认证时缺省。 */ certification?: AuthorCertification /** 关注数。 */ following: number /** 粉丝数。 */ follower: number /** 稿件数。 */ archiveCount: number /** 未开通充电时缺省。 */ charging?: ChargingSummary /** 未配置粉丝勋章时缺省。 */ fanMedal?: FanMedalConfig /** 生日,时间戳;未公开时缺省。 */ birthday?: number } interface AuthorProfileResp { status: ToyDataStatus /** `status` 非 `ok` 时缺省。 */ data?: AuthorProfile } // --------------------------------------------------------------------------- // 作者视频 // --------------------------------------------------------------------------- /** 视频引用:每项只能传 `aid` 或 `bvid` 之一,同时传或都不传会本地抛错。 */ type AuthorVideoRef = | { aid: number; bvid?: never } | { bvid: string; aid?: never } interface AuthorVideosReq { /** 1–50 项;SDK 会按 aid/bvid 去重并保留首次出现顺序。 */ videos: AuthorVideoRef[] } interface AuthorVideo { aid: number bvid: string title: string /** 封面地址,SDK 已归一化 CDN 域名。 */ cover: string description: string /** 发布时间,时间戳。 */ publishTime: number /** 总时长,单位由后端定义。 */ duration: number /** 分区名称。 */ partition?: string /** 分 P 列表。 */ pages: Array<{ page: number title: string duration: number }> /** 稿件统计数据。 */ stat: { view: number like: number coin: number favorite: number share: number comment: number danmaku: number } /** 付费相关标记。 */ pay: { /** 是否充电专属。 */ chargingPay: boolean /** 是否付费稿件。 */ paid: boolean } /** 所属合集 id;不属于任何合集时缺省。 */ seasonId?: number /** 排行信息;无排行数据时缺省。 */ rank?: { now: number highest: number } } interface AuthorVideoItem { /** 回显本次请求传入的引用,用于与请求项对应。 */ ref: AuthorVideoRef status: ToyItemStatus /** `status` 非 `ok` 时缺省(如作者未参与该视频创作、视频不可见)。 */ data?: AuthorVideo } interface AuthorVideosResp { /** 整体状态;部分项失败时为 `partial`。 */ status: ToyDataStatus items: AuthorVideoItem[] } // --------------------------------------------------------------------------- // 作者互动关系 // --------------------------------------------------------------------------- interface AuthorRelation { /** 当前访问用户是否已关注该作者。 */ isFollowing: boolean /** 当前访问用户是否就是该 Toy 作者本人。 */ isAuthor: boolean /** 是否为老粉。 */ isOldFan: boolean /** 是否持有该作者的粉丝勋章。 */ hasFanMedal: boolean /** 勋章名称;无勋章时缺省。 */ fanMedalName?: string /** 勋章等级;无勋章时缺省。 */ fanMedalLevel?: number /** 勋章是否处于点亮状态;无勋章时缺省。 */ isFanMedalActive?: boolean /** 当前是否正在对该作者进行包月充电。 */ isCharging: boolean /** 关注时间,时间戳;未关注时缺省。 */ followTime?: number } interface AuthorRelationResp { status: ToyDataStatus /** `status` 非 `ok` 时缺省。 */ data?: AuthorRelation } // --------------------------------------------------------------------------- // 视频互动数据 // --------------------------------------------------------------------------- interface VideoUserActionsReq { /** 1–50 个正整数 aid;SDK 会去重并保留首次出现顺序。 */ aids: number[] } interface VideoUserActionItem { aid: number status: ToyItemStatus /** 是否已点赞;`status` 非 `ok` 时缺省。 */ liked?: boolean /** 已投币数;`status` 非 `ok` 时缺省。 */ coinCount?: number /** 是否已收藏;`status` 非 `ok` 时缺省。 */ favorited?: boolean } interface VideoUserActionsResp { /** 整体状态;部分项失败时为 `partial`。 */ status: ToyDataStatus items: VideoUserActionItem[] } // --------------------------------------------------------------------------- // 排行榜 // --------------------------------------------------------------------------- /** 榜单周期:总榜(永久)/ 月 / 周 / 日。 */ type RankPeriod = 'all' | 'month' | 'week' | 'day' interface SubmitScoreReq { /** 榜位,固定 1 / 2 / 3(含义由 toy 自定义),不传默认 1;非法值本地抛错。 */ board?: number /** * 本次成绩的绝对分数,不是增量。整数,取值范围 -16777216 ~ 16777215, * 允许 0 与负数;超出范围本地抛错。服务端只保留该榜位的历史最高分。 */ score: number } interface SubmitScoreResp { /** 我的总榜(all)历史最高分,已合并本次提交。 */ score: number } interface RankListReq { /** 榜位,固定 1 / 2 / 3,不传默认 1。 */ board?: number /** 周期,不传按总榜 `all`。 */ period?: RankPeriod /** 返回名次数量;不传或超上限按后端默认(≤100)。 */ limit?: number } /** 榜单单行:名次 + 历史最高分 + 展示用昵称/头像,不含 uid。 */ interface RankItem { rank: number score: number nickname: string /** 头像地址,SDK 已归一化 CDN 域名。 */ avatar: string } interface MyRankReq { /** 榜位,固定 1 / 2 / 3,不传默认 1。 */ board?: number /** 周期,不传按总榜 `all`。 */ period?: RankPeriod } interface MyRankResp { /** 是否已上榜。分数允许 0 / 负,判断是否上榜必须用本字段,不能用 `score`。 */ ranked: boolean /** 我的名次,从 1 起,唯一不并列(同分先达成者靠前);未上榜为 0。 */ rank: number /** 我的历史最高分;未上榜为 0。 */ score: number } // --------------------------------------------------------------------------- // 媒体能力(摄像头 / 麦克风) // --------------------------------------------------------------------------- /** * 申请摄像头的可选项,仅 `requestCamera` 使用。 * * 只接受 `facingMode` 一个字段:传入其他字段、或传非普通对象(数组 / 字符串 / null 等) * 时 SDK 本地抛错。`requestMicrophone` 不接受任何参数。 */ interface MediaRelayOptions { /** 摄像头朝向:`'user'` 前置、`'environment'` 后置。不传默认前置。 */ facingMode?: 'user' | 'environment' } // --------------------------------------------------------------------------- // window.toy 的公开 API 面 // --------------------------------------------------------------------------- interface Toy { /** * 判断当前环境是否支持指定能力。传能力名(如 `'saveImageToAlbum'`)即可, * 带不带 `toy.` 前缀都能匹配。 * * 端外 Web 不支持 `saveImageToAlbum` / `share` / `closeBrowser`,其余能力两端一致 * (二维码 `getQrCode` 两端均可用)。 */ isSupport(ability: string): Promise /** * 跳转到指定页面。 * * 必须在用户手势事件(如 click)中调用:SDK 会检查 `navigator.userActivation`, * 无有效用户激活时抛错。端内为原生跳转,端外新开标签页。 */ navigate(req: NavigateReq): Promise /** * 保存图片到系统相册。**仅 B站 App 内可用**,Web 端调用直接抛错。 * * Web 端请改用标准浏览器下载能力(`` 或 canvas blob URL,需用户点击触发)。 */ saveImageToAlbum(req: SaveImageReq): Promise /** * 拉起 B站 App 的分享面板。**仅 B站 App 内可用**,Web 端调用直接抛错。 * * 只传相对当前 Toy 的 `path`,完整分享链接由平台生成, * 避免分享链接被伪造指向其他 Toy 或外部站点。`path` 越界抛 `invalid_param`, * 页面不在 `/toy//` 路径下时抛 `unsupported`。 */ share(req: ShareReq): Promise /** * 生成指向当前 Toy 内某个页面的二维码,返回可直接用作 `img.src` 的 PNG base64 图片。 * **App 端和 Web 端都可用**。 * * 用途不限于分享:跨设备接力(PC 上扫码到手机继续玩)、结算页海报、线下展示都适用。 * 区别于 `share`,本方法不拉起分享面板,而是把链接编码成二维码交给 Toy 自行展示。 * 两个入参都可省略:`toy.getQrCode()` 即当前 Toy 首页的二维码。 * * 只能编码当前 Toy 内的页面链接,不能编码任意文本:二维码内容同样由平台生成, * Toy 不能自行指定完整 URL。如需为任意字符串生成二维码,请自行在 Toy 内 * 打包二维码库。`path` 越界或 `size` 非法抛 `invalid_param`, * 页面不在 `/toy//` 路径下时抛 `unsupported`。 */ getQrCode(req?: QrCodeReq): Promise /** 关闭当前 WebView 容器。**仅 B站 App 内可用**,Web 端调用直接抛错。 */ closeBrowser(): Promise /** * 获取当前登录用户的头像、昵称与当前 Toy 内的稳定假名标识。 * * OpenID 模式启用后,已有未过期的 profile v1/v2 授权会直接复用,不重复弹窗;没有有效授权时,首次调用需由用户手势触发,并由平台展示“获取你的昵称、头像和当前 Toy 内用户标识”固定的用户数据确认弹窗(Toy 不能自定义弹窗内容),接受后写入 v2 授权。模式关闭时省略 toyOpenId。 * 用户拒绝、未登录或在外部手机浏览器中调用时 Promise reject(外部浏览器会先引导打开 B站 App)。用户资料确认统一使用正文“你的 B站昵称、头像和仅用于当前 Toy 的用户标识,将用于当前 Toy 内展示和关联数据;不会向 Toy 提供你的 UID,也不能用于跨 Toy 识别。”,不区分 v1/v2 文案配置。`toyOpenId` 仅用于当前 Toy 内关联用户,不得写入埋点或公开日志。 */ getUserProfile(): Promise /** * 获取当前 Toy 作者的公开资料、账号统计、稿件数、充电聚合与粉丝勋章配置。 * 不能指定作者,固定取当前 Toy 的作者。 */ getAuthorProfile(): Promise /** * 批量获取当前 Toy 作者的视频公开信息,含作者以联合投稿(共同创作)身份参与的视频。 * 作者未参与创作、或对当前用户不可见的视频,对应 item 只返回 `status`,不含 `data`。 */ getAuthorVideos(req: AuthorVideosReq): Promise /** * 获取当前访问用户与当前 Toy 作者的关注、老粉、粉丝勋章状态, * 以及当前是否正在对该作者进行包月充电。 * * 只校验登录态,不触发用户数据确认弹窗。外部手机浏览器返回 `status: 'unsupported'` * 并引导打开 B站 App。 */ getAuthorRelation(): Promise /** * 获取当前访问用户对当前作者视频(含作者以联合投稿身份参与的视频)的点赞、投币、收藏状态。 * * 只校验登录态,不触发用户数据确认弹窗。外部手机浏览器返回 * `status: 'unsupported'` 且 `items` 为空数组。 */ getVideoUserActions(req: VideoUserActionsReq): Promise /** * 读取云存储。不传或传空数组读取当前用户在该 Toy 下的全部数据; * 未命中的 key 不出现在结果中。 * * 需用户已登录,按「登录用户 + Toy」双维度隔离,不触发用户数据确认。 * key 不满足 `[a-zA-Z0-9_-]{1,128}` 时本地抛错。 */ getCloudStorage(keys?: string[]): Promise> /** * 批量写入云存储(upsert),同 key 覆盖旧值。 * * `items` 必须是普通对象(传数组 / 字符串 / null 会本地抛错)。 * key 只能含字母、数字、下划线、短横线且 ≤128 字节;value 为字符串, * 字节上限由服务端校验(存对象请自行 `JSON.stringify`)。 * 单个 Toy 的 key 数量上限由服务端拦截。 */ setCloudStorage(items: Record): Promise /** 批量删除云存储中指定的 key。key 格式非法时本地抛错。 */ removeCloudStorage(keys: string[]): Promise /** * 上报分数到排行榜。需用户已登录,首次提交前由平台完成用户数据确认。 * 按「toy + 榜位 + 周期」隔离,同榜位只保留历史最高分,本次更低不覆盖。 */ submitScore(req: SubmitScoreReq): Promise /** * 读取榜单,游客可读。返回前 `limit` 名,固定从高到低; * 同分时先达成者靠前,名次唯一、不并列。 */ getRankList(req?: RankListReq): Promise /** 查询我在指定榜单的排名。需用户已登录;是否上榜必须用 `ranked` 判断。 */ getMyRank(req?: MyRankReq): Promise /** * 申请摄像头,返回浏览器原生的实时 `MediaStream`(视频轨),可直接赋给 * `