bpi-rs 0.2.1

Bilibili API client library for Rust
Documentation
# 迁移到 bpi-rs 0.2

这份文档记录当前仓库的 0.2 迁移面。`bpi-rs` 正在从宽泛的扁平 API 包装,迁移到更符合 Rust 习惯的 SDK:独立模块客户端、显式登录态、类型化参数、直接返回业务 payload,以及由离线契约支撑的测试。

0.2 是有意设计的破坏性迁移。请优先把调用方改到新的模块客户端风格,不要继续包一层旧扁平方法名。

## 客户端构造

### 0.1 风格

旧示例通常把 `BpiClient::new()` 当作带副作用的构造函数,并默认本地账号状态会被隐式加载。

### 0.2 风格

`BpiClient::new()` 和 `BpiClient::builder().build()` 返回 `BpiResult`。构造过程是显式的,不会读取 `account.toml`,也不会安装全局 tracing subscriber。

```rust
use bpi_rs::{BpiClient, BpiResult};

fn anonymous_client() -> BpiResult<BpiClient> {
    BpiClient::builder().build()
}
```

需要自定义 HTTP/session 行为时使用 builder:

```rust
use std::time::Duration;

use bpi_rs::{BpiClient, BpiResult};

fn configured_client() -> BpiResult<BpiClient> {
    BpiClient::builder()
        .timeout(Duration::from_secs(15))
        .connect_timeout(Duration::from_secs(10))
        .user_agent("my-app/0.1")
        .referer("https://www.bilibili.com/")
        .origin("https://www.bilibili.com")
        .build()
}
```

## 登录态和凭据

凭据必须显式传入。不要依赖构造函数副作用。

### Cookie 字符串

```rust
use bpi_rs::{BpiClient, BpiResult};

fn logged_in_client(cookie: &str) -> BpiResult<BpiClient> {
    BpiClient::builder().cookie(cookie).build()
}
```

### Account 结构体

```rust
use bpi_rs::{Account, BpiClient, BpiResult};

fn client_from_account(account: Account) -> BpiResult<BpiClient> {
    BpiClient::builder().account(account).build()
}
```

### 更新已有客户端

```rust
client.set_account_from_cookie_str(
    "DedeUserID=123; SESSDATA=...; bili_jct=...; buvid3=...",
)?;
```

客户端需要回到游客状态时,调用 `client.clear_account()`。

## 从扁平方法迁移到模块客户端

0.2 推荐按领域分组调用接口:

```rust
let view = client.video().view(params).await?;
let nav = client.login().nav().await?;
let info = client.bangumi().info(params).await?;
```

示例:

```rust
use bpi_rs::ids::{Bvid, MediaId};
use bpi_rs::video::VideoViewParams;
use bpi_rs::bangumi::BangumiInfoParams;

let bvid: Bvid = "BV1xx411c7mD".parse()?;
let video = client.video().view(VideoViewParams::from_bvid(bvid)).await?;

let bangumi = client
    .bangumi()
    .info(BangumiInfoParams::new(MediaId::new(28_220_978)?))
    .await?;
```

当前模块客户端包括:

```text
activity, article, audio, bangumi, cheese, clientinfo, comment,
creativecenter, danmaku, dynamic, electric, fav, historytoview,
live, login, manga, message, misc, note, opus, search, user,
video, video_ranking, vip, wallet, web_widget
```

变更类和流程敏感接口仍然需要门控,或者有意不放进默认示例。

## 返回值处理

已迁移的模块客户端方法通常直接返回解码后的业务 payload:

```rust
let view = client.video().view(params).await?;
println!("{}", view.title);
```

公共结果别名是:

```rust
pub type BpiResult<T> = Result<T, BpiError>;
```

需要完整 B 站响应外壳时使用 `ApiEnvelope<T>`。迁移调用点时,优先选择直接返回 `BpiResult<T>` 的模块客户端方法。漫画模块里剩余的 envelope 别名只是 `ApiEnvelope<T>` 的兼容名称。

如果接口确实可能在成功时返回 `data: null`,对应模块客户端方法可以返回 `BpiResult<Option<T>>`。

## 错误处理

处理 `BpiError` 时,优先按类别或语义 helper 分支,不要只匹配原始消息。

```rust
match result {
    Ok(payload) => {
        println!("ok: {payload:?}");
    }
    Err(error) if error.requires_login() => {
        eprintln!("需要登录");
    }
    Err(error) if error.is_risk_control() => {
        eprintln!("请求被风控拦截");
    }
    Err(error) => return Err(error),
}
```

常用 helper:

```text
requires_login()
requires_vip()
is_permission_error()
is_risk_control()
semantic_error()
```

## 二维码登录

SDK 暴露二维码登录基础接口。应用侧负责二维码渲染、轮询策略、超时控制和 session 持久化。

```rust
use bpi_rs::login::LoginQrPollParams;

let generated = client.login().qr_generate().await?;
println!("scan url: {}", generated.url);

let status = client
    .login()
    .qr_poll(LoginQrPollParams::new(generated.qrcode_key)?)
    .await?;

if status.code == 0 {
    println!("login cookies: {:?}", status.cookies);
}
```

已提升的 `login.qr.flow` 契约是一个 Probe flow,会把 `qr_generate` 和带运行时 `qrcode_key` 的 `qr_poll` 组合起来;它不是单独的模块客户端方法。

## 自定义请求

尚未由模块客户端封装的接口,可以使用共享请求 helper。普通 JSON envelope 接口优先使用 payload 返回 helper。

```rust
use bpi_rs::{BilibiliRequest, BpiClient, BpiResult};
use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct Payload {
    value: serde_json::Value,
}

async fn custom_read(client: &BpiClient) -> BpiResult<Payload> {
    client
        .get("https://api.bilibili.com/x/example")
        .send_bpi_payload("custom.example")
        .await
}
```

观察到成功响应可能省略 payload 或返回 `null` 时,使用 `send_bpi_optional_payload`。

## 测试、探针和本地凭据

默认开发检查应保持离线:

```powershell
cargo fmt --check
cargo clippy --all-targets --all-features --locked -- -D warnings
cargo check --all-features
cargo test --all-features --lib
```

Live Probe 是独立工作流。原始 Probe 输出和草稿只保存在本地 `target/`:

```text
target/bpi-contract-drafts/...
target/bpi-probe-runs/...
target/bpi-probe-notes/...
```

可提交的接口证据放在:

```text
tests/contracts/<domain>/<endpoint>/contract.json
tests/contracts/<domain>/<endpoint>/responses/<case>.json
```

不要提交 `account.toml`、Cookie、`SESSDATA`、`bili_jct`、`buvid3`、原始 Probe 输出或账号相关响应数据。

新增或迁移接口前,先按 [API 风险分类](api-risk-classification.md) 判断属于 `public-read`、`authenticated-read`、`private-read`、`mutating`、`spending` 还是 `login-session`。`mutating` 和 `spending` 测试必须使用 `#[ignore]` 和显式环境变量门控。

## 迁移检查清单

- 把扁平调用替换为 `client.<domain>().<method>(...)`- 修改 `BpiClient::new()` 调用点,处理 `BpiResult`- 通过 builder、`Account``set_account_from_cookie_str` 显式注入凭据。
- 优先使用类型化 ID,例如 `Aid``Bvid``Cid``Mid``MediaId``SeasonId``EpisodeId`- 优先使用直接返回 payload 的 `BpiResult<T>` 方法,不继续依赖旧 envelope helper。
- 只有文档或真实响应证明字段可能缺失/为 `null` 时才使用 `Option<T>`- live、变更类和消费资产行为必须放在显式 opt-in 门控后。
- 发布改动前运行离线验证门禁。