目标不是下载器

我在 ShayuAgent 里做 B 站相关工具时,更关心的是把公开视频的元信息和可用流信息整理给 Agent,而不是把它做成一个无边界下载器。这里的原则很简单:尊重平台规则,只处理自己有权访问的内容,不绕过登录、付费或版权限制。

这篇笔记只记录工程上的链路设计,不提供规避风控的做法。

一条典型链路

视频工具通常先解析用户输入。输入可能是 BV 号、AV 号、普通链接或分享链接。第一步要把它们归一化成稳定参数,比如视频 ID、分 P、清晰度偏好和是否需要 DASH 信息。

接着请求播放信息接口。B 站部分接口需要 WBI 签名,工程上可以把签名逻辑独立成一个 Signer,把密钥获取独立成一个 KeyProvider,请求客户端只关心「参数进来,签名后的 URL 出去」。

为什么要拆 KeyProvider

签名密钥并不是业务逻辑的一部分。把它单独拆出去,有几个好处:可以缓存、可以过期刷新、可以在错误时触发一次重试,也更容易测试。

当接口返回业务错误码时,工具层不应该直接把原始响应丢给前端。更好的方式是转成稳定错误:比如 invalid_inputwbi_key_unavailableplayurl_deniedplatform_changed。Agent 更容易理解这些错误,也能决定是否换一种策略。

Shark-Tools 里其他解析器也有类似模式:先把业务参数规范化,再加时间戳、nonce 和签名。下面是脱敏后的通用写法,不包含真实密钥和目标接口:

import hashlib
import hmac
import json
import secrets
import time

def build_signed_payload(params: dict, secret: str) -> dict:
    payload = {
        **params,
        "timestamp": int(time.time() * 1000),
        "nonce": secrets.token_hex(16),
    }
    canonical = json.dumps(payload, separators=(",", ":"), ensure_ascii=False)
    signature = hmac.new(
        secret.encode("utf-8"),
        canonical.encode("utf-8"),
        hashlib.sha256,
    ).hexdigest()
    return {"payload": payload, "signature": signature}

WBI、HMAC、MD5 参数签名这些细节各不相同,但工程上都应该抽成一个小而清晰的 Signer。上层只需要知道“给我一组已签名参数”,不要到处散落字符串拼接。

输出要面向上层

视频解析工具最后返回的内容,最好不是某个平台接口的原始 JSON,而是上层能直接使用的结构:标题、作者、封面、时长、分辨率、音视频轨道、字幕或错误原因。

这样前端可以直接渲染,Agent 也可以继续总结、转写或生成选题。工具越靠近底层,输出越应该稳定;否则平台一改字段,上层所有逻辑都会跟着抖。

我喜欢在边界层就做一次规范化:

def normalize_streams(raw: dict) -> dict:
    streams = []
    for item in raw.get("dash", {}).get("video", []):
        streams.append({
            "quality": item.get("id"),
            "codec": item.get("codecs"),
            "width": item.get("width"),
            "height": item.get("height"),
            "url": item.get("baseUrl") or item.get("base_url"),
        })
    return {
        "title": raw.get("title", ""),
        "cover": raw.get("pic", ""),
        "duration": raw.get("duration", 0),
        "streams": [item for item in streams if item["url"]],
    }

规范化不是为了让代码显得“干净”,而是为了隔离变化。平台返回字段属于外部世界,自己的 streams 结构才是内部契约。

维护策略

这类解析链路必须接受一个事实:平台会变化。我的做法是让代码里保留清楚的错误边界和少量注释,把签名、请求、解析、规范化输出拆开。坏的时候,先看是哪一层坏了,而不是一上来整段重写。

逆向不是一次性灵感,更多是长期维护。写得克制一点,后面自己会感谢自己。