02 · 运行时
本章定义插件代码的执行环境:能用什么、何时被加载、如何被调用、出错时如何表达。
1. 引擎
协议不绑定具体引擎,只规定环境契约。宿主必须满足:
- 语言级别:完整支持 ES2022(含模块、
async/await、类字段、BigInt、可选链)。 - 每个插件运行在独立的引擎运行时与上下文中:不与其他插件、不与界面共享任何对象或全局状态。 内存预算、执行中断与调度必须独立;仅在同一个 runtime 中创建多个 context 不满足要求。
- 上下文之外没有环境能力:没有文件系统、没有原生网络、没有进程、没有到宿主内部接口的通道。
- 能对单个上下文设置内存上限与栈上限,并能从外部中断其执行(防止死循环卡住应用)。
实现建议(非规范):选用解释执行、不依赖 JIT 的可嵌入引擎(QuickJS 一系)。 它在五个平台上行为一致,可在 iOS 上使用,并原生支持内存上限与执行中断。 在 WebView 的 iframe 或 Worker 中运行插件不满足上述第 2、4 条。
独立调度保证一个插件的同步执行不占用界面线程或阻塞其他插件;可以使用专用线程或等效机制, 不要求每插件一个操作系统进程。本协议隔离脚本级死循环、超额分配与可恢复引擎错误, 不承诺在同进程内隔离原生引擎崩溃、进程被系统终止等故障。
2. 全局对象
2.1 保证存在
除 ES2022 标准内建对象外,宿主必须提供:
| 全局对象 | 说明 |
|---|---|
console |
debug / log / info / warn / error,等价于 hg.log.* |
setTimeout、clearTimeout |
归属于创建它的调用,随该调用结束清理;无 setInterval |
queueMicrotask |
— |
TextEncoder、TextDecoder |
保证 UTF-8;其他字符集需特性 charset.legacy |
URL、URLSearchParams |
WHATWG URL 语义 |
atob、btoa |
— |
AbortController、AbortSignal |
— |
structuredClone |
仅支持可传递值 |
crypto.getRandomValues、crypto.randomUUID |
密码学安全的随机源 |
2.2 保证不存在
fetch、XMLHttpRequest、WebSocket、DOM(window、document、navigator)、
localStorage / sessionStorage / indexedDB、WebAssembly、Worker、setInterval、
Node.js 的 require / process / Buffer。
联网用 hg.http,持久化用 hg.storage。
3. 入口
export const meta = { id: "demo", name: "示例源", version: "1.0.0", protocol: 1 };
export default function (hg) { /* 返回插件对象 */ }
- 默认导出是工厂函数:接收宿主 API 对象
hg(见 04), 返回插件对象或解析为插件对象的 Promise。 - 插件对象由若干能力命名空间组成,命名空间是否存在即表示是否实现该能力(见 06)。
- 工厂应当轻量:只做对象构造与读取设置。工厂执行期间仅可使用 04 §1 指定的模块。
- 工厂的时间预算为 5 秒,超时按加载失败处理。
- 把
hg作为参数注入(而不是全局变量)的好处是:作者可以在 Node.js 里传入一个假的hg对插件做单元测试,无需启动宿主。
4. 可传递值
宿主与插件之间传递的参数与返回值必须是可传递值:
null、布尔、有限数字、字符串- 上述值组成的数组与普通对象(无环、无自定义原型方法)
Uint8Array(可出现在任意层级)
规则:
- 值为
undefined的对象属性在传递时被丢弃。 Date、Map、Set、类实例、函数、Symbol、BigInt不可传递。- 对象嵌套深度 ≤ 32;单次调用的参数或返回值 ≤ 8 MiB。
- 传递是按值复制:任何一方之后修改对象都不影响另一方。
以上限制适用于能力方法的业务参数、业务返回值与持久数据。以下为运行时绑定对象,不走可传递值序列化:
hg、ctx(含 AbortSignal)、HttpRequest.signal、HtmlNode、错误对象与定时器回调。
它们不得嵌入业务返回值或存储。void 方法允许以 undefined 完成;数组元素不得为 undefined。
大小计算为:将每个 Uint8Array 替换成 null、丢弃 undefined 对象属性后,对紧凑 JSON 的 UTF-8 字节数,
再加全部二进制原始字节数;不能只计算 JS 字符数。这个定义用于预算,不要求桥接采用 JSON 编码。
5. 调用模型
5.1 方法形状
插件的能力方法与资源变换都是同一形状(卸载钩子 dispose 的例外见 06 §16):
method(params: object, ctx: CallContext): T | Promise<T>
params恒为单个对象。后续协议版本只会向其中增加可选字段,插件不得对未知字段报错。ctx是本次调用的上下文:
| 字段 | 说明 |
|---|---|
requestId |
调用标识,出现在宿主日志中 |
deadline |
动态只读的截止时间(Unix 毫秒);网页视图暂停调用预算期间为 null |
signal |
取消信号:用户离开页面、调用被取代或到达截止时间时触发 |
宿主必须为每次调用建立任务作用域。调用内产生的 HTTP、网页视图、定时器及异步宿主操作
默认归属于该作用域,跨 await 与定时器回调仍保持归属,不能借同插件的另一次调用续命。
插件可透传 ctx.signal 或提供更早的取消信号;显式信号与调用取消取“任一触发即取消”,不能替代调用作用域。
方法完成、失败或被取消时,宿主停止其剩余任务,丢弃迟到结果,拒绝迟到回调继续发起宿主操作。 已经发给上游的写操作与已经完成的原子存储写入不会撤销;宿主不自动重试写操作。
5.2 截止时间
| 调用类别 | 默认预算 | 上限 |
|---|---|---|
| 列表、详情、搜索等读取 | 15 秒 | 30 秒 |
| 播放解析(含解析器) | 20 秒 | 60 秒 |
| 登录提交 | 30 秒 | 60 秒 |
资源变换(transformResource) |
5 秒 | 10 秒 |
| 工厂函数 | 5 秒 | 5 秒(不可调) |
插件可以在 meta.timeouts(键为 read、playback、login、transform,单位秒)里调整预算,
用于上游偏慢的场景。宿主必须把声明值夹在 5 秒与上表的上限之间:放宽可以,突破上限不行。
超时后宿主放弃结果、触发 signal,并向界面返回 TIMEOUT。正常异步超时不算运行时故障;
同步执行超出预算被强制中断时,触发调用同样返回 TIMEOUT,但运行时须重建并累计故障次数。
排队时间计入调用预算;连续等待多步网络请求不得重置总预算。
网页视图暂停预算的例外见 04 §12:ctx.deadline 在暂停期间为 null,恢复后反映更新的截止时间。
5.3 并发
- 同一插件的多次调用在
await点交错执行(单线程事件循环语义)。插件不得假设调用是串行的。 - HGhub 默认同一插件同时在途的调用 ≤ 8,排队 ≤ 32;超出时宿主直接返回
RATE_LIMITED,不进入插件。 - 宿主可以合并参数完全相同的在途只读调用。
- 不同插件之间真正并行,互不阻塞。
- 一个异步调用取消通常不影响同插件其他调用;若同步执行被中断、内存超限或引擎无法继续,
宿主销毁整个插件运行时:触发调用返回对应错误,同运行时其他在途调用以
INTERNAL结束并记录原因, 队列中的未执行调用可在重新初始化后继续,但不得重放已进入插件的方法。
5.4 无状态约定
插件随时可能被卸载并重新加载:
- 模块级变量与闭包中的状态只能当作缓存,丢失后必须能重建。
- 需要跨调用保留的数据写入
hg.storage。 - 分页状态必须编码在游标里,不得保存在内存中。
6. 生命周期
已导入/停用 ──启用──▶ 初始化 ──能力与线路就绪──▶ 就绪
│ │
│ 初始化失败/超时 │ 空闲超时 · 应用挂起 · 用户改动设置
▼ ▼
初始化失败 卸载(下次调用再加载)
就绪 ──同步执行中断/内存超限/可恢复引擎错误──▶ 运行时重建 ──连续 3 次──▶ 已停用(提示用户,可手动恢复)
- 启用时初始化,空闲后惰性恢复:导入的元信息检查不执行工厂;首次启用执行以下流程,
初始化成功后才向界面发布可用能力:
- 检查
requires,以已保存线路(没有则为null)执行工厂;工厂不能要求首次执行时已有线路。 - 按
kind校验能力及其必需方法,缓存有效的方法清单。 - 若实现
endpoints,调用endpoints.list({ refresh: false });方法必须允许 current 为null, 此时不得依赖hg.endpoint.url(),可使用插件内的引导地址获取列表。 - 保存选择仍在列表中时沿用,否则选择第一项;列表为空则初始化失败。无 endpoints 时 current 为
null。 - 当前线路变化时重建上下文、重新校验能力,再开放业务调用。初始化中的业务调用暂不执行。 已就绪插件空闲卸载后,下次调用用已保存线路重建;无需每次联网列举线路。
- 检查
- 空闲卸载:插件无在途调用时,宿主可以随时卸载它;空闲满 5 分钟应当卸载。
- 用户改动设置、切换站点线路后,宿主必须销毁并重建上下文。
- 设置改变后重新执行完整初始化;线路切换使用已取得的列表与所选线路恢复,不递归重新选择默认线路。 重建前取消在途调用;若有活跃播放,会话按 09 停止后由用户重新播放。
- 应用挂起且没有活跃播放或下载时,宿主应当卸载全部插件。活跃媒体会话可按需唤醒插件执行刷新、 变换和进度回报;这是宿主调度的调用,不授予插件自由后台执行能力。平台挂起限制见 09 §7。
- 一次业务方法成功完成后重置连续运行时故障计数;普通业务错误、网络错误和正常异步超时不累计。
7. 限额
宿主必须执行资源预算。下表区分协议固定上限与 HGhub 默认调度策略;默认策略可以调优, 不能作为插件推断设备性能的依据。改变协议固定上限按兼容规则处理。
| 资源 | 预算 | 性质 |
|---|---|---|
| 堆内存 | 64 MiB / 插件 | HGhub 默认;包含绑定的 HTML 文档等宿主资源占用 |
| 调用栈 | 1 MiB | HGhub 默认 |
| 单段同步执行(两次让出事件循环之间) | 1 秒 | HGhub 默认,中断后重建运行时 |
| 在途调用 | 8,排队 32 | HGhub 默认(§5.3) |
| 单次调用的参数或返回值 | 8 MiB | 协议固定上限,含整个对象而非仅二进制字段 |
| 定时器 | 64 个,单个延迟 ≤ 60 秒 | HGhub 默认,生命周期受调用预算约束 |
hg.storage |
5 MiB / 插件;键 ≤ 256 字节;单值 ≤ 256 KiB | 协议固定上限,不与 HTTP/媒体缓存共享 |
| 日志 | 每分钟 240 行;单行 ≤ 2 KiB;每插件保留最近 2000 行 | HGhub 默认 |
8. 错误模型
8.1 表达方式
插件通过抛出 hg.PluginError 表达可预期的失败:
throw new hg.PluginError("AUTH_REQUIRED", "请先登录");
throw new hg.PluginError("RATE_LIMITED", "请求过于频繁", { retryAfterSec: 30 });
message会展示给用户:应当简短、可读、使用界面语言。details(可选)只进入插件日志。- 抛出其他任何值(普通
Error、字符串等)一律视为INTERNAL,其消息不展示给用户,只写入日志。
8.2 错误码
| 错误码 | 含义 | 宿主的默认处理 | 可重试 |
|---|---|---|---|
AUTH_REQUIRED |
需要登录 | 引导登录 | 登录后 |
AUTH_INVALID |
凭据错误 | 在登录表单内提示 | 否 |
AUTH_EXPIRED |
会话失效 | 清除登录态并引导重新登录 | 登录后 |
NOT_FOUND |
内容不存在 | 提示;片库条目标记失效 | 否 |
CONTENT_LOCKED |
内容存在但当前不可播放 | 展示原因;可尝试换播放线路或换源 | 否 |
RATE_LIMITED |
被限流 | 在 retryAfterSec 内禁止重试并提示 |
等待后 |
UPSTREAM_ERROR |
上游返回错误 | 提示;可重试 | 是 |
UPSTREAM_CHANGED |
上游结构与插件预期不符 | 提示"插件需要更新",提供检查更新入口 | 否 |
INTERACTION_REQUIRED |
需要用户完成验证等操作 | 提示进入插件页;宿主不从错误消息或 details 推断并自动打开网页 | 用户操作后 |
NETWORK |
网络不可达、DNS、TLS 失败 | 提示检查网络 | 是 |
TIMEOUT |
超过截止时间 | 提示;可重试 | 是 |
CANCELLED |
调用被取消 | 静默 | — |
INVALID_ARGUMENT |
参数不合法 | 写入日志 | 否 |
STORAGE_FULL |
插件持久存储配额不足 | 提示清理该插件数据;不得自动清除登录态 | 清理后 |
UNSUPPORTED |
能力或参数组合不被支持 | 静默或提示 | 否 |
INTERNAL |
插件缺陷或未归类错误 | 提示"插件出错",可查看日志 | 是 |
PluginError的retryable省略时按上表推断。hg.*接口失败时抛出的也是PluginError(如hg.http.fetch抛NETWORK、TIMEOUT、CANCELLED)。 插件不捕获时原样上抛即可。- 宿主遇到未知错误码必须按
INTERNAL处理。
8.3 UPSTREAM_ERROR 与 UPSTREAM_CHANGED
这个区别决定用户该做什么,插件应当认真区分:
- 上游明确表示失败(HTTP 5xx、业务错误码)→
UPSTREAM_ERROR:稍后重试可能恢复。 - 响应成功但结构不认识(字段缺失、类型不符、选择器取不到节点)→
UPSTREAM_CHANGED: 重试无用,需要更新插件。
9. 返回值校验
宿主把插件返回的数据交给界面之前必须校验:
- 结构:必填字段缺失或类型不符 → 本次调用以
INTERNAL失败,日志记录出错字段的路径。 - 列表容错:列表中单个条目不合法时丢弃该条目并记录警告,不使整页失败。
- 未知字段:丢弃,不传给界面。
- 字符串:展示文本超过字段长度上限时截断(上限见 05); ID、地址、游标及密钥等机器处理字段不截断,按所属类型报错或丢弃条目。 文本按纯文本渲染;唯一例外是明确标注为富文本的字段,它们必经清洗后才会进入页面。
- 数字:必须有限;计数类字段必须是非负整数。
- 地址:按字段用途校验;宿主请求的资源只接受绝对 HTTPS 地址(声明
allowInsecureHttp时允许 HTTP);external流的例外见 05 §7.3。地址长度 ≤ 8 KiB,超长丢弃或报错,不截断。
10. 日志
hg.log.*与console.*写入该插件的日志环形缓冲,用户可在插件详情页查看与导出。- 日志只保存在本机,宿主不得自动上传。