HGhub 开发者HPP 1.0 草案

04 · 宿主 API

← 总览

宿主 API 是插件与外界交互的唯一途径。它以对象 hg 的形式传给工厂函数(见 02 §3)。 所有插件看到的是同一套 hg。

1. 模块一览

模块 用途 同步/异步
hg.env 运行环境信息 同步只读
hg.endpoint 当前站点线路 同步只读
hg.http 网络请求 异步
hg.cookies Cookie 罐 异步
hg.storage 持久键值存储 异步
hg.crypto 密码学原语 同步
hg.codec 编码转换 同步
hg.html HTML 解析与查询 同步
hg.settings 读取用户设置 同步只读
hg.log 日志 同步
hg.webview 请用户在网页中完成操作 异步
hg.PluginError 错误类型 —

通用规则:

2. hg.http

hg.http.fetch(request: HttpRequest): Promise<HttpResponse>

2.1 请求

interface HttpRequest {
  url: string;                       // 绝对 https 地址
  method?: "GET" | "HEAD" | "POST" | "PUT" | "PATCH" | "DELETE";   // 默认 GET
  headers?: Record<string, string>;
  query?: Record<string, string | number | boolean | undefined>;   // 追加到 url
  body?: string | Uint8Array | { json: Json } | { form: Record<string, string> };
  responseType?: "text" | "json" | "bytes";                         // 默认 text
  charset?: string;                  // text/json 的解码字符集
  timeoutMs?: number;                // 默认 15000,上限 30000
  redirect?: "follow" | "manual";    // 默认 follow
  maxBytes?: number;                 // 响应体上限,默认 2 MiB,最大 8 MiB
  cache?: { ttlSec: number; staleIfErrorSec?: number };
  retry?: 0 | 1 | 2;                 // 默认 0
  credentials?: "include" | "omit";  // 是否使用 Cookie 罐,默认 include
  signal?: AbortSignal;
}
字段 规则
url 长度 ≤ 8 KiB;目标受 03 §2 的网络规则约束,被拦截时抛 NETWORK
headers 总大小 ≤ 16 KiB;由传输层决定的头会被丢弃(03 §2)。未指定 User-Agent 时为 HGhub/<版本>
query 值为 undefined 的键被忽略;按表单规则编码后追加
body ≤ 1 MiB。{ json } 自动设置 Content-Type: application/json;{ form } 自动设置 application/x-www-form-urlencoded;显式给出的 Content-Type 优先。GET、HEAD 不得带请求体
charset 省略时取响应头中的字符集,回退 UTF-8;其他字符集需特性 charset.legacy
maxBytes 响应体超出即中止并抛 UPSTREAM_ERROR

2.2 响应

interface HttpResponse<T> {
  status: number;
  ok: boolean;                       // 200–299
  url: string;                       // 最终地址
  headers: Record<string, string>;   // 键小写;多值以 ", " 连接
  setCookies: string[];              // 原始 Set-Cookie,逐条
  data: T;                           // 按 responseType:string / 解析后的 JSON / Uint8Array
  fromCache: boolean;
}

2.3 重定向

请求 行为
redirect: "manual" 不跟随;3xx 响应原样返回,headers.location 可读
带请求体(POST 等) 恒按 manual 处理,避免重复提交
其他 跟随,至多 5 跳;每跳重新过网络规则

重定向跨 origin 时移除显式 Cookie 与 Authorization,不重新向跨源目标附加原始凭据头; 自动 Cookie 每跳按目标 URL 重新匹配所属插件的 Cookie 罐。

2.4 缓存

cache 让宿主代为缓存响应,减少对上游的重复请求。

2.5 重试

retry 只对 GET、HEAD 生效,在 NETWORK、TIMEOUT 或 5xx 时自动重试,间隔 400 毫秒、1200 毫秒。 重试同样消耗限速配额;处于冷却期的主机不重试。

2.6 可能抛出的错误

错误码 情形
NETWORK DNS、连接、TLS 失败,或目标被网络规则拦截
RATE_LIMITED 排队超时,或主机处于冷却期(带 retryAfterSec)
TIMEOUT 超过 timeoutMs 或调用的截止时间
CANCELLED signal 被触发
UPSTREAM_ERROR 响应体超过 maxBytes
UPSTREAM_CHANGED responseType: "json" 解析失败
INVALID_ARGUMENT 地址不合法、请求体过大、GET 带请求体等

3. hg.env

interface HostEnv {
  protocol: string;                    // 宿主实现的协议版本,如 "1.0.0"
  app: { name: string; version: string };
  plugin: { id: string; version: string };
  platform: "windows" | "macos" | "linux" | "android" | "ios";
  locale: string;                      // 界面语言,BCP 47
  features: readonly string[];         // 宿主支持的可选特性
  media: {
    codecs: readonly ("h264" | "hevc" | "av1")[];   // 当前播放路径实际可用,按偏好排序
    formats: readonly ("hls" | "dash" | "progressive")[];
  };
}

3.1 可选特性

特性名 含义
webview 支持 hg.webview(§12)
charset.legacy TextDecoder、hg.codec.decodeText、hg.http 的 charset 支持 GBK / GB18030 / Big5 / Shift_JIS / EUC-KR
transform.segment 支持对视频分片做资源变换(见 05)
format.dash 当前播放器与宿主 MPD/分段代理均支持 MPEG-DASH(见 09)

插件在 meta.features(数组)中声明用到的特性,仅供界面提示;运行时应当自行探测并降级。

4. hg.endpoint

interface EndpointApi {
  readonly current: {
    id: string;
    label?: string;
    url: string;           // 纯 origin,如 "https://api.example.com"
  } | null;
  url(path: string): string;   // 把以单个 "/" 开头的路径拼到当前线路上;拒绝 "//host" 等跨 origin 形式
}

5. hg.cookies

hg.cookies.get(params: { url: string }): Promise<Cookie[]>
hg.cookies.set(params: { url: string; cookie: Cookie }): Promise<void>
hg.cookies.clear(params?: { url?: string }): Promise<void>

interface Cookie {
  name: string; value: string;
  domain?: string; path?: string;
  hostOnly?: boolean;      // get 返回匹配属性;set 省略 domain 时为 true
  expiresAt?: number;      // Unix 毫秒;省略为会话 Cookie
  secure?: boolean; httpOnly?: boolean;
}

6. hg.storage

hg.storage.get<T>(key: string): Promise<T | undefined>
hg.storage.set(key: string, value: TransferValue, options?: { ttlSec?: number }): Promise<void>
hg.storage.delete(key: string): Promise<void>
hg.storage.keys(prefix?: string): Promise<string[]>
hg.storage.clear(): Promise<void>

7. hg.crypto

原生实现的同步原语。字符串入参按 UTF-8 编码。

type Hash = "md5" | "sha1" | "sha256" | "sha512";

hg.crypto.digest(algorithm: Hash, data: Uint8Array | string): Uint8Array
hg.crypto.hmac(algorithm: Hash, key: Uint8Array | string, data: Uint8Array | string): Uint8Array
hg.crypto.aesEncrypt(params: AesParams, data: Uint8Array): Uint8Array
hg.crypto.aesDecrypt(params: AesParams, data: Uint8Array): Uint8Array
hg.crypto.rsaEncrypt(params: RsaParams, data: Uint8Array): Uint8Array
hg.crypto.randomBytes(length: number): Uint8Array      // ≤ 65536
hg.crypto.randomUUID(): string

interface AesParams {
  mode: "cbc" | "ecb" | "ctr" | "gcm";
  key: Uint8Array;                  // 16 / 24 / 32 字节
  iv?: Uint8Array;                  // cbc、ctr:16 字节;gcm:12 字节;ecb:省略
  padding?: "pkcs7" | "none";       // 仅 cbc、ecb,默认 pkcs7
  aad?: Uint8Array;                 // 仅 gcm
}

interface RsaParams {
  publicKey: string;                // PEM(SPKI 或 PKCS#1)
  padding: "pkcs1" | "oaep-sha1" | "oaep-sha256";
}

8. hg.codec

hg.codec.base64Encode(data: Uint8Array | string, options?: { urlSafe?: boolean; pad?: boolean }): string
hg.codec.base64Decode(text: string): Uint8Array      // 同时接受标准与 URL 安全字母表
hg.codec.hexEncode(data: Uint8Array | string): string
hg.codec.hexDecode(text: string): Uint8Array
hg.codec.utf8Encode(text: string): Uint8Array
hg.codec.utf8Decode(data: Uint8Array): string
hg.codec.decodeText(data: Uint8Array, charset: string): string

输入不合法时抛 INVALID_ARGUMENT。decodeText 对 UTF-8 之外的字符集需特性 charset.legacy。

9. hg.html

不少上游只有页面可解析。宿主提供原生的 HTML 解析与 CSS 选择器查询, 避免每个插件各自打包体积庞大的解析库。

hg.html.parse(html: string): HtmlNode

interface HtmlNode {
  readonly tag: string;                       // 小写标签名;文档根为 "#document"
  select(selector: string): HtmlNode[];
  first(selector: string): HtmlNode | null;
  text(): string;                             // 后代文本拼接并折叠空白
  ownText(): string;                          // 仅直接子文本节点
  attr(name: string): string | null;
  attrs(): Record<string, string>;
  html(): string;                             // 内部 HTML
  outerHtml(): string;
  parent(): HtmlNode | null;
  children(): HtmlNode[];
}

10. hg.settings

hg.settings.get<T>(key: string): T | undefined
hg.settings.getAll(): Record<string, Json>

11. hg.log

hg.log.debug(...args: unknown[]): void
hg.log.info(...args: unknown[]): void
hg.log.warn(...args: unknown[]): void
hg.log.error(...args: unknown[]): void

参数被格式化为一行文本;对象以 JSON 序列化,Uint8Array 只记录长度。 限额与可见性见 02 §10。超出速率的日志被丢弃并计数。

12. hg.webview(可选)

需要特性 webview。用于上游只能通过网页完成的登录或人机验证。

hg.webview.open(params: {
  url: string;                 // 起始页
  title?: string;              // 外框标题,≤ 60 字符
  doneWhen: {
    urlPrefix?: string;        // 导航到以此为前缀的地址时视为完成
    cookieNames?: string[];    // 这些 Cookie 全部出现时视为完成
  };
  timeoutSec?: number;         // 默认 300,上限 600
}): Promise<{ finalUrl: string }>

典型用法(需要验证时由插件发起):

try {
  return await api.fetchJson(path, ctx);
} catch (err) {
  if (err.code === "INTERACTION_REQUIRED" && hg.env.features.includes("webview")) {
    await hg.webview.open({ url: hg.endpoint.url("/login"), title: "完成验证",
      doneWhen: { urlPrefix: hg.endpoint.url("/account"), cookieNames: ["session"] } });
    return await api.fetchJson(path, ctx);   // 带着新 Cookie 重试一次
  }
  throw err;
}

13. hg.PluginError

new hg.PluginError(code: ErrorCode, message?: string, options?: {
  retryAfterSec?: number;
  retryable?: boolean;
  details?: Json;
  cause?: unknown;
})

hg.PluginError 是 Error 的子类,实例带有 code、retryable、retryAfterSec、details。 语义与错误码见 02 §8。