实现进度与验证范围
应用版本 0.0.1,当前为 插件分发/浏览/点播/直播/图文/本地片库/账号表单/评论互动开发版,运行时协议报告 1.0.0-dev。协议草案描述完整目标;
本页记录实际代码范围,不能把 TypeScript 中存在的接口视为当前客户端已经支持。
本阶段以 Windows 为主,Android 已验证单文件导入与单部真实 MP4 播放;未完成五端验收,尚未冻结 HPP 1.0。支持范围与缺口见下文。
这是保留既有 HGhub 前端风格的全新插件播放器。不读取、不迁移旧账号、收藏或历史;
应用使用独立标识 com.hghub.player,首次启动空片库。旧站点 API、线路、登录与媒体解密代码已移除。
已落地
| 层 | 实现 |
|---|---|
| 独立宿主 | src-tauri/crates/hpp,不依赖原短剧平台 API、凭据、媒体解密逻辑 |
| 插件管理 | 原生多文件选择、粘贴、URL 和本地/远程 manifest 勾选导入;简介与来源列表名称展示;元信息及版本校验,降级显式选择,启停、重试、移除、索引恢复 |
| 插件更新 | 手动检查和勾选安装;updateUrl 优先、数值版本比较、来源记录持久化;预览不替换现有插件,修订变化拒绝过期更新;失败后恢复上一可用文件,保留设置与存储 |
| 初始化 | 工厂与能力发现;首次以空 endpoint 获取线路,选定默认线路后重建;设置和站点线路变更重建 |
| 执行 | 每插件独立 QuickJS runtime、线程、64 MiB 堆;8 个并发调用、40 个总排队槽位、调用预算与同步中断 |
| 生命周期 | Promise/await 上下文归属、调用取消、遗漏 signal 的 HTTP 取消、定时器清理、空闲卸载、连续故障停止及显式重试 |
| 数据桥 | 全量带标签的可传递值;支持 Uint8Array,不与普通对象冲突;深度、字节预算与结果校验 |
| 持久化 | 插件 ID 隔离的键值存储,二进制、TTL、原子落盘、容量上限;Cookie 独立罐与 domain/path 匹配 |
| HTTP | DNS/重定向检查,复用应用现有网络策略,取消、请求/响应上限、字符集、限速、Retry-After 冷却;GET/HEAD 重试和有界匿名 GET 响应缓存 |
| 原生工具 | URL、URLSearchParams、编解码、摘要/HMAC、AES CBC/ECB/CTR/GCM、随机数,原生 HTML/CSS 选择器 |
| 媒体 | 随机端口 loopback HTTP 服务,progressive 流式代理与真实 Range/HEAD/206/416,HLS 主/子清单及 URI 属性改写 |
| 资源归属 | 解析器匹配/选择;解析器独立 Cookie、请求头和变换;源与解析器字幕保留各自提供者;同一解析动作共享预算 |
| 字节变换 | 有界图片/清单/分片/密钥的原生 AES-CBC、plugin transformResource;对输出计算局部范围 |
| 会话 | 图片批次和播放会话;释放取消传输,依赖插件停用/移除/更新/设置变更时释放;修订校验防止旧会话使用新环境 |
| Windows 页面 | 经典沉浸播放器外壳;首页直接播放,片源范围在设置选择;推荐列表保留来源/分区排序与显隐;分区完整列表导航;分类树成功结果按插件修订缓存、筛选与排序;推荐/分类/搜索/分区滚动分页、失败保留内容并重试原游标、ID 去重与重复游标终止;单源/多源/综合搜索、来源选择;插件管理、设置、线路、日志 |
| 站点线路 | 手动刷新、可达性探测与切换;刷新后保留仍存在的线路,否则选择第一项并重建;探测不携带插件默认头或 Cookie,不自动切换 |
| 本地片库 | Rust 原子落盘,收藏/追更/历史按插件与作品隔离;进度另含播放列表与真实分集 ID;离开/暂停/结束保存、重新选集续播;插件移除后记录保留 |
| 详情与播放 | 按季分组,显式 season/episodeNumber 唯一时定位候选,不用 index 猜测;分集分页、刷新、HLS/普通视频播放;同版本覆盖修订使页面与会话失效 |
| 跨来源换源 | 详情页与播放器共用换源入口;并行搜索其他可用来源,标题/年份/类型排序、独立失败重试;候选详情、分集分页与用户确认;显式季集唯一匹配才预选,沿用旧秒数默认关闭,新来源进度优先;来源关联原子保存,“我的”可查看关联来源,旧收藏/追更/历史独立保留 |
| 直播 | 纯直播源可只实现 live;首页入口显示频道分组、频道分页、频道图标、节目单按需加载;直播解析复用 HLS/普通流、线路/流选择和媒体会话,调用只传 channelId |
| 沉浸式信息流 | 分类页按具体方法显示 feed 入口;首页共用沉浸播放;单条竖屏播放、上下条按钮与方向键、游标分页及 ID 去重;直接使用 entryEpisodeId,缺失时取详情及必要的首批分集;本地收藏、评论与云端互动;按真实分集保存进度,自动下一条遵循偏好;覆盖页面或离开后释放播放 |
| 图文 | articles.list/getDetail 独立列表与详情;正文按允许标签重建、图片走所属插件媒体会话、外链使用外部浏览器;支持文内视频、关联点播作品、评论与互动 |
| 我的 | 插件账号资料、云端收藏/点赞/历史分页与本页导入本地;手动批量追更检查每批 ≤50,忽略非本批条目 |
| 云端目录与关注 | 云端收藏目录按原始 folderId 请求,关注列表独立游标分页、头像走所属插件媒体代理;仅实现 interact.follow 时显示取消关注,成功后移除对应作者 |
| 评论与回复 | 详情页按需打开作品/当前真实分集评论,回复使用 replies/commentId;独立分页与 ID 去重、纯文本、头像代理、评论点赞;发送返回评论时插入,无返回则刷新;失败保留草稿,离开页面取消未完成调用;不支持的目标/操作独立隐藏 |
| 云端互动 | 详情页按具体方法显示云端点赞/收藏/作者关注,成功后更新状态;本地收藏单独保留;认证失效清除互动状态并沿用所属插件的登录引导 |
| 上游进度回报 | 实际 playing/pause/ended 产生 start/pause/end;单次播放消费者按单调时钟每 30 秒至多一个 heartbeat;刷新/切流不重复 start,带真实 episodeId 和有限秒数;失败静默,认证错误只清除账号缓存不弹出播放提示 |
| 插件表单 | auth/ui 必需方法检查、返回状态/字段/资料校验;插件设置与日志面板内展示登录、账号、退出与自定义操作;八种字段、多步值合并、隐藏挑战更新、必填校验、验证码/头像媒体代理 |
| 登录状态 | 插件自行持久化会话;宿主输入仅在当前表单内存中保留;AUTH_INVALID 保留填写值,AUTH_REQUIRED/EXPIRED 清除页面账号缓存并引导插件登录;退出失败后重新查询插件资料,不清空插件存储 |
| 播放恢复 | 页面按到期时间/致命播放错误尝试一轮重新解析,携带 reason:refresh,保留当前播放位置;支持手动刷新 |
| 播放选择 | 源/解析器各自的候选流和播放线路控件;按阶段与插件 ID 保存原始 serverId;手动切换保留当前秒数,刷新保留选择;解码失败刷新一次后按设置有限切换候选流 |
| 解析器站点偏好 | 播放页主机由宿主提取;点播与共用播放器提供解析器选择及默认关闭的“记住”;成功选择后原子保存,跨站点不复用手动选择;流指定解析器优先,停用/移除/不匹配的记忆不参与自动路由;设置可逐项忘记或清空 |
| 开发资料 | 完整草案类型 src/plugins/protocol.ts;可注入传输/加密/HTML 适配器的内存假宿主 src/plugins/mockHost.ts;纯虚构源和真实宿主 stdio 测试适配器 |
当前发现并调度的方法为:
media.getDetail、media.resolvePlayback、media.listEpisodes、media.matchUrlresolver.resolvesearch.search、search.getFilters、search.suggest、search.trendinghome.getSections、home.listSection、catalog.getCategories、catalog.getFilters、catalog.listlibrary.listMedia、library.listFolders、library.listFollowing、progress.report、updates.checkcomments.list、comments.send、comments.like、interact.like、interact.favorite、interact.followfeed.list、endpoints.list、transformResourcelive.getGroups、live.listChannels、live.resolve、live.getProgrammesarticles.list、articles.getDetailauth.getMethods、auth.submit、auth.getProfile、auth.logoutui.getActions、ui.submit
当前启用检查要求 source 实现 media 或 live 的全部必需方法,resolver 实现 resolver.resolve。
纯直播源不得声明返回点播 MediaItem 的 home/catalog/search/feed/library 能力而不实现 media。
声明 auth 时必须实现全部四个方法,声明 ui 时必须实现两个方法;不完整命名空间初始化失败。
上述方法已接入点播详情、播放器、信息流和“我的”的对应用户流程。
未实现的可选特性仍未宣称支持。
表单管理复用 hpp_manage IPC:form.actions 的 target 为 auth 或 ui;
form.submit 的 target 为 { kind: "auth" | "ui", id },values 为字符串键值对象,
两者都使用界面预生成的 requestId,进入与普通能力相同的取消/截止时间作用域。
表单内单个非法字段使该表单无效;动作/登录方式列表中的坏条目独立丢弃。
校验日志只含字段路径和原因,不含输入值。网页视图仍未实现,未声明 webview 特性;
无 form 的登录方式可直接提交,但不能据此认为开发版已支持网页登录。
播放解析链上的认证错误由宿主标注实际提供者,界面据此进入数据源或解析器的账号区块;
不会信任插件 details 中自报的其他插件身份,也不把解析器登录态与源登录态混用。
仍需完成
- 分发平台完善:订阅、启动自动检查、自动更新偏好、24 小时冷却及播放期间延后安装已实现并有 Rust 回归。Android content URI 单文件读取已修复实测;本地列表相对兄弟文件仍需目录授权。
- 其他能力:云端收藏目前使用上游默认目录;详情互动中的目录选择、分集点赞和更完整账号状态联动后续完善。信息流复用经典播放器的手势与控制器,仍需更多设备验收。
- 播放完善:设备能力与默认清晰度偏好、过期候选索引重新定位、服务器失败切换;完整字幕格式处理。当前候选流有限切换只处理播放器致命错误,解析阶段的认证/锁定错误停止并引导用户处理。
- 宿主刷新调度:下载和后台消费者共享的 single-flight 刷新、资源映射原子替换、可恢复错误分类;当前自动刷新由播放页执行。
- 媒体预算与缓存:播放/图片/下载分队列,按主机共享并发,资源缓存与淘汰,长时间直播及低延迟 HLS 验证。
- 字节区间变换:带字节变换的 HLS BYTERANGE 当前明确返回 UNSUPPORTED;不对截断密文直接解密。
- API 与可选功能:RSA 是协议必需 API,目前缺失,不能归为可选;网页交互、DASH、下载为可选且未实现。静态 hg.env.media 尚未由实际播放路径探测。
- 应用功能:插件自检、数据清理和占用、应用更新。此项目不开发旧平台数据迁移。
- 平台验收:Windows 原生窗口和五端完整媒体回归仍未完成;Android 已定位并修复文档选择器 URI 导入失败,粘贴与文件导入新 APK 实测成功。
缓存当前只接纳显式 credentials:"omit" 且无 Authorization/Cookie 的 GET;
对默认 include 但当时恰无 Cookie 的请求采取保守的缓存未命中,不改变响应语义。
假宿主不模拟 QuickJS 隔离、完整配额或异步调用上下文;这些必须使用 Rust 宿主测试。
分发请求使用独立、匿名的宿主下载环境;远程列表重定向后按最终 URL 解析相对路径, 只下载勾选条目,文件身份与列表不一致时仅该条失败。下载、检查和等待安装锁期间支持取消; 文件替换已开始的单个安装完成后再停止后续条目,不将取消解释为回滚上游操作或存储写入。 离开页面后返回的预览会立即释放。原平台专用调试脚本仅保留在被忽略的本地目录, 公共测试入口只依赖虚构源,不依赖私有插件。
播放选择的可执行契约
范围与触发
对应 05 §7、06 §2/3、08 §7 与 09 §2/5。此处记录宿主 IPC,插件的 PlaybackParams 不增加解析器字段。
签名
hpp_manage("media.prepare", { id, requestId, args, streamIndex?, resolverId?, resolverStreamIndex?, resolverServers? })
请求、响应与环境
args.serverId仅传入源的media.resolvePlayback;resolverServers为解析器插件 ID 到原始线路 ID 的字符串对象,只有选中解析器的条目会传给其resolver.resolve。- 两个
*streamIndex为 0–15 的整数,省略使用 0;选择范围由该阶段本次返回的流决定。 - 返回
PreparedMedia.choices.source及可选choices.resolver:pluginId、servers、serverId、streamIndex、streams;流描述仅含 index、label、height、bitrate、codec、kind、format、serverId。 - 选择描述不含资源 URL、请求头或变换密钥。会话地址仍由媒体代理提供。无需新增环境变量。
- 已得到源候选但首选流格式不支持、解析器不可用或解析失败时,错误附带宿主生成的
playbackChoices,界面保留手动选择其他候选的入口;该字段不接受插件自报。 - localStorage
hpp.playback.servers使用 JSON 数组["source" | "resolver", pluginId]的序列化值作为键,值为原始 serverId;只记录成功解析后实际选中的线路。
校验与错误
| 条件 | 结果 |
|---|---|
| 索引为负数、字符串或大于 15 | INVALID_ARGUMENT,不执行插件 |
| 索引超出本次候选列表 | 明确报错,保留手动选择控件 |
| 流声明 resolverId,同时存在旧 UI 选择 | 按流声明路由;声明的解析器不可用则明确报错 |
| resolverServers 包含其他解析器条目 | 忽略非当前提供者条目,不转交给源 |
| 源/解析器返回非法线路条目 | 独立丢弃;重复 ID 保留首条;移除未知字段 |
| AUTH_REQUIRED、AUTH_EXPIRED、CONTENT_LOCKED | 停止解析,不自动轮询候选 |
正常、基线与异常案例
- 正常:源第 2 条 page 候选 → 指定解析器第 2 条直接流;源和解析器使用不同线路 ID,并分别展示。
- 基线:只有一条源流且没有解析器,隐藏额外控件;不发送解析器 serverId。
- 异常:解码致命错误先刷新一次,开启“播放失败后尝试其他播放流”时每个剩余候选本轮至多尝试一次,耗尽后提示手动操作;用户重试才开启新一轮。
回归断言
Rust tests/playback_selection.rs 检查两阶段参数归属、指定解析器优先、解析信息合并、选择信息不泄露资源配置、非法索引和坏条目处理。
Edge 集成测试检查真实流/线路切换保留秒数、两阶段选择刷新后保留、跨集与重载持久化、一次刷新与有限候选重试、首选格式不支持后手动播放备用候选。
错误与正确示例
错误:把解析器线路放入 args.serverId,导致源收到其他插件的线路 ID。
正确:args.serverId = "source-backup",resolverServers = { "fixture.resolver": "resolver-backup" },各阶段只收到自己的原始 ID。
直播与图文的调用约定
适用范围
对应 05 §9、06 §8/9、08 首页与详情界面、09 媒体归属。首页按具体方法显示来源的直播/图文入口,停用来源不显示。
调用签名
hpp_manage("live.prepare", {id,requestId,args:{channelId,serverId?,reason,codecs},streamIndex?,resolverId?,resolverServers?,resolverStreamIndex?})hpp_manage("article.prepare", {id,requestId,args:{articleId,reason,codecs},streamIndex?,resolverId?,resolverServers?,resolverStreamIndex?})- 两者返回与点播相同的
PreparedMedia,释放仍使用media.release。
参数与资源契约
直播只向 live.resolve 传原始 channelId、播放参数,不构造 mediaId/episodeId。节目单使用 Unix 毫秒窗口,用户打开后才请求未来 24 小时。
图文播放重新请求 articles.getDetail({id:articleId}),仅取其中 streams 进入共享媒体链;正文/作者等字段不进入播放配置。
图文评论与互动分别使用 {kind:"article",articleId},不复用 media 目标。两类播放沿用来源/解析器隔离、取消、修订和字幕归属,无新增环境变量。
校验与失败处理
| 条件 | 结果 |
|---|---|
| source 只有完整 live | 正常启用,不要求空壳 media |
| live/articles 命名空间缺少必需方法 | 初始化失败,保留可查看的安装记录 |
| 频道非法 ID/number,节目时间缺失或倒置 | 丢弃独立坏条目,记录路径 |
| 图文正文不是文本 | 结果校验失败 |
| 正文超过 512 KiB | 按 UTF-8 字符边界截断并警告 |
| streams 为 [] | 正文正常显示,不显示视频入口 |
| 节目单失败 | 不影响频道播放,提供独立重试 |
| 播放致命错误 | 尝试一次 reason:refresh,失败后允许手动切换/重试 |
正常与异常用例
- 正常:纯直播源 → 频道分页 → 点播频道 → 原生代理 HLS → 原始 signal ID 切换 → 手动刷新。
- 基线:只有纯文本的文章,按纯文本渲染,无需视频或评论能力。
- 异常:文章含 script/style/iframe/form、事件属性或 javascript 链接,整个危险节点或属性不参与可见节点构造;正文图片必须先登记代理 URL,不能直接填入远程 src。
必须验证的行为
tests/extra_content.rs 覆盖纯直播初始化、缺失方法、非法独立条目、正文预算、空 streams、媒体会话与公开选择描述。
scripts/test-plugin-extra-content.mjs 覆盖生产 App 的首页入口、频道/文章分页、HLS/普通视频、节目单按需请求、原始参数归属、正文清洗及外链命令。
列表封面集中登记图片批次,避免每行占用一个媒体会话;超出 200 个图片的集合分批登记并在页面销毁时释放全部会话。
实现示例对照
错误:直播插件伪造 VOD episodeId;文章 HTML 原样放入界面并保留上游 img src。
正确:频道仅传 channelId;文章使用惰性模板解析,再从允许的节点/属性构建 React 元素,图片统一替换为 assets.prepare 返回的本地地址。
跨来源确认与片库关联
对应 06 §17.2、08 §6/7,不改变插件 search/media 的公开签名。用户打开换源面板后按当前标题并行查询其他已启用、可见且实现 search/media 的来源;年份与类型用于宿主排序,不增加插件未约定的搜索参数。
关联命令
hpp_manage("library.sources.get", {pluginId,mediaId}) -> LibraryEntry[]:返回关联组内其他条目,包含来源不可用的历史条目。界面按当前可用来源与成人内容偏好决定能否显示和打开。hpp_manage("library.sources.link", {from:{pluginId,item:MediaItem},to:{pluginId,item:MediaItem}}) -> null:仅在用户确认后调用,校验两侧再一次原子写入;已有两个关联组会合并,重复关联幂等。library.json.associations为条目键到关联组条目键集合的映射;键仍使用 JSON 序列化的[pluginId,mediaId],不拼接分隔符。原有 favorite、tracking、lastProgress 和逐集 progress 均不转移、不清除。- 无新增环境变量;不需要、也不执行旧 HGhub 数据迁移。
确认后播放
候选详情所有已返回播放列表中,明确的 season + episodeNumber 仅匹配一集且无未加载分页时可预选;双方都缺少 season 时允许按明确 episodeNumber 预选。index 相同、身份缺失、多个匹配或分集尚未全部返回时不自动选集。
用户可手动选新来源分集、加载其后续页,或只打开新来源详情。锁定分集不可选。 跨页选择只保存真实 playlistId/episodeId;新页重新取详情并沿原始游标查找该分集,重复游标终止,分集变化时停止并要求重选。
目标分集已有本地进度时优先使用目标记录。没有时,“沿用原来源播放位置”默认不勾选;用户勾选才复制确认时有限、非负的秒数,否则从 0 开始。 确认前先保存原来源当前进度,再建立关联并替换当前详情路由;取消、搜索、查看候选均不建立关联,也不触发新来源解析。
校验与失败边界
| 情况 | 行为 |
|---|---|
| 关联任一条目标识或 MediaItem 非法 | INVALID_ARGUMENT,两侧记录与磁盘均不变 |
| 关联到自身 | INVALID_ARGUMENT |
| 一个来源搜索失败 | 本来源独立错误与重试,其他候选可继续选择 |
| 候选详情或分集页失败 | 留在当前来源,显示重试,不解析候选媒体 |
| 新来源进度读取失败 | 停止确认播放,避免在未知优先记录时覆盖进度 |
| 关联保存失败 | 保留原详情与播放,显示失败 |
| 新来源分集已移除、锁定或重复游标后仍找不到 | 不自动播放,提示用户重新选集 |
| 插件移除 | 关联与旧片库保留,界面标记来源不可用 |
正常案例:A 的 S2E1 → B 的明确 S2E1(即使 index 为 41),用户确认后以 B 的真实 ID 播放。基线案例:不勾选沿用位置且 B 无进度,则从 0 播放。异常案例:B 中有两条 S2E1,要求手动选集;同 index 不视为匹配。
Rust content_library 检查关联组对称/合并/幂等、重启恢复、非法写入原子性,以及各来源收藏/追更/进度独立。scripts/test-plugin-source-switch.mjs 使用真实 QuickJS 与生产界面检查排序、失败隔离、取消、季集匹配、进度优先级、显式沿用秒数、跨页选集及“我的”关联入口。
错误示例:根据旧 index:1 把新来源第一个条目视为同集,并直接覆盖旧记录。
正确示例:只用明确季集身份提出候选,由用户确认新来源及分集,各来源继续按自己的真实 ID 记录。
信息流的调用约定
对应 05 §3、06 §5、08 §7 与 09 媒体会话约定,不新增插件能力或字段。
feed.list({cursor?,limit:20})的条目按 ID 去重;下一页失败保留当前播放条目,重试使用原始游标。重复游标停止继续加载;仅有重复条目的页不将索引推进到空条目。- 存在
entryEpisodeId时直接调用media.prepare,args为真实mediaId、episodeId和通用播放参数,省略未知的playlistId;不会为播放而额外调用media.getDetail。 - 缺少直接入口时请求
media.getDetail({id}),取第一播放列表的首集。列表首批为空且有next时按原始playlistId与游标调用一次media.listEpisodes;锁定或仍无分集时显示错误与详情入口,不用index伪造分集 ID。 - 无播放列表身份的直接入口进度使用
[pluginId,mediaId,null,episodeId]为键,与包含真实playlistId的进度分别保存;不要求迁移旧平台数据。 - 播放开始后按周期、暂停、结束及离开保存本地进度。上游报告沿用实际播放事件和 30 秒心跳约定,刷新不重复 start;报告失败静默。
- 每次只挂载一个播放消费者;切换、覆盖或离开释放媒体会话,返回时从已保存位置继续。自动下一条受应用偏好控制;末页不循环。
- 回归使用
tests/fixtures/feed.js和scripts/test-plugin-feed.mjs,通过真实 QuickJS、媒体代理和生产 React 页面验证。浏览器入口启用与生产一致的 React StrictMode,分类元数据不缓存已取消的请求 Promise。
首页编排与滚动分页
对应 08 §3–5 与 03 §3,不新增插件方法。
hpp.preferences记住来源顺序order、首页隐藏来源hiddenHomeIds、分区顺序sectionOrder和分区显隐hiddenSections;首页显隐不改变搜索范围。多个来源模式仍保留选择顺序,拖动来源时同步该模式的已选顺序。- 推荐列表的“调整”提供原生 HTML 拖动把手与可访问的上移/下移按钮;当前首页直接播放,推荐列表由分类页入口进入。分类沿用来源顺序;“全部参与”只恢复该来源分区,不修改其他来源。
- 推荐分区、分类和分区列表共用
usePagedPlugin,搜索保留各来源独立的请求锁和游标链。按原始 ID 去重,已消费游标不能再次请求;来源返回重复游标时停止并显示提示。 - 底部哨兵以当前滚动容器为根预加载,同时保留按钮。下一页失败不会清空已有内容,也不会自动无限重试;重试使用失败时的原始游标。首页刷新失败保留现有推荐。
- 查询、筛选、排序、插件修订或搜索范围变化时取消旧请求,旧响应不能覆盖新结果。分类树仅缓存成功的结果,键含插件修订;不缓存或跨页面共享未完成的请求 Promise。
scripts/test-plugin-browsing.mjs使用真实拖动、滚动与生产页面验证,夹具在tests/fixtures/browsing.js(HPP crate 内);全程使用不透明游标,不将其转换为页码。
按播放站点记住解析器
对应 06 §3、08 §7/8,不更改插件的 PlaybackParams 或解析器参数。
宿主命令与公开选择描述
resolver.preferences.get({}) -> Record<string,string>:返回精确主机名到解析器插件 ID 的映射。resolver.preferences.set({host,pluginId:string|null}) -> null:只接受当前可用的解析器;null 忘记该主机。拒绝 URL、端口、路径、通配符与非法类型;主机规范化为小写/IDNA,去除 DNS 末尾点。resolver.preferences.clear({}) -> null:清空全部记忆。resolver-preferences.json独立于插件数据,先原子写入成功再替换内存;写入失败保留原值。没有新增环境变量。PreparedMedia.choices.resolverRoute与宿主生成的playbackChoices.resolverRoute包含host、forced、selectedId?、resolvers:[{id,name}]。界面只取得主机和可用选项,不取得播放页路径、查询参数、请求头或密钥。media/live/article.prepare的可选resolverHost将当前手动resolverId限于对应主机。源在刷新或换集后改用其他播放站点时,旧的界面选择不参与新站点路由;这两个字段不会传给源或解析器插件。
优先级与持久化
- 流声明的
resolverId优先,指定插件不可用时明确失败,不自动改用其他解析器。 - 本次对当前主机的手动选择优先于记忆。
- 未手动指定时,从可用且
matches命中的候选里取该精确主机的记忆;停用、移除或不匹配的记忆忽略。唯一匹配直接使用,多个匹配且无可用记忆则要求选择。 - 没有匹配时列出已安装可用解析器供手动选择;不将匹配其他主机的历史偏好自动套用。
- “记住这个站点的选择”默认关闭,仅本次解析成功、实际提供者与所选项一致且主机未变化时保存;失败或取消不覆盖旧记忆。插件指定解析器时不提供覆盖记忆入口。
点播详情与直播/图文/信息流共用该选择流程;设置提供逐项忘记与全部清空。
tests/resolver_preferences.rs 验证精确主机隔离、跨宿主重启、指定优先、失效偏好和落盘失败;
scripts/test-plugin-resolver-preferences.mjs 验证实际点播/直播、默认不记住、跨站点切集、失败不覆盖、页面重载与设置清理。
验证与复现
在仓库根目录执行:
cargo test --manifest-path src-tauri/crates/hpp/Cargo.toml
cargo check --manifest-path src-tauri/Cargo.toml
cargo build --manifest-path src-tauri/Cargo.toml
npm.cmd run build
npm.cmd run test:plugins
依赖已缓存时可以为 Cargo 添加 --offline。浏览器测试默认使用 Windows Edge;
其他安装位置可设置 BROWSER_EXECUTABLE。测试数据目录临时创建,不读取正式插件、登录态或上游。
exFAT 工作目录不支持硬链接;如 MSVC 报增量缓存调试信息损坏,可设置 $env:CARGO_INCREMENTAL = '0'
后重新构建。资源紧张时为 Cargo 添加 -j 2。
test:plugins 运行真实 Rust/QuickJS 宿主和生产 App 与 React 页面,只将 Tauri IPC 换成 stdio 传输。
浏览器本地录制合成 H.264 画面,再经实际媒体代理验证普通视频拖动、HLS 初始化段/分片播放及刷新。
新增回归覆盖新导航、首页、分类筛选/排序、多来源搜索合并与单源失败隔离、本地收藏/追更、实际分集续播。
同时使用 src-tauri/crates/hpp/tests/fixtures/forms.js 验证账号/操作表单、多步登录、登录态恢复与失效、
退出接口失败后的本地会话清理,以及离开面板取消提交。注册入口只验证通用外链命令,不访问外部站点。
src-tauri/crates/hpp/tests/fixtures/social.js 与 scripts/test-plugin-social.mjs 验证评论/回复、云端互动、目录与关注分页、取消评论,以及实际播放事件回报;心跳使用可注入的单调时钟验证 30 秒边界。
直播与图文回归使用 src-tauri/crates/hpp/tests/fixtures/extra-content.js;纯直播源不声明点播 media,以防测试空壳掩盖初始化约束。
本地私有开发基线独立验证,不纳入客户端、公共测试或文档站产物。
产物位于忽略提交的 test-results/plugins/<时间>/,包含测试报告、截图和本地请求记录。
这项测试不等同于实际 Tauri WebView2 窗口/打包验证。
历史验证(2026-11-13,早期界面)
- HPP Rust 测试 46 项全部通过,含无 playlist 的信息流进度隔离与持久化、关联来源合并/幂等/原子性、解析器站点偏好的持久化/优先级/失效回退/写入失败保留;评论/互动、直播/图文结果过滤、纯直播能力发现、媒体会话、安装、账号、播放及网络回归通过。
- Tauri 应用 Rust 日志测试 3 项通过。
- 前端类型检查、生产构建与 Windows debug 可执行文件构建通过。
- Edge 实际播放与生产 App 严格模式回归通过,报告:
test-results/plugins/2026-11-13T05-32-01-958Z/report.json,包含 25 组通过结果。 - 覆盖首页/分类、多来源合并与来源选择、单源失败隔离、收藏/追更/分集续播、账号表单、 云端列表导入、追更结果归属过滤、移除插件保留不可用来源记录、分区完整列表导航、线路刷新与探测。
- 分发覆盖文件选择传输、本地相对列表、重定向后的无扩展名远程列表、简介及来源名称、只下载勾选项、 身份不符条目隔离、仅安装、更新检查不替换文件、失败更新恢复和勾选安装更高版本。
- 播放覆盖源/解析器流和线路切换、保留秒数、刷新与跨集保留选择、重载持久化、一次刷新后的有限候选流切换, 以及首选格式不支持后手动播放备用流;新增设置可关闭自动候选流切换。
- 评论覆盖作品/分集/回复目标、独立分页、纯文本显示、未登录保留草稿、返回评论插入与无返回刷新、离开页面取消提交,UNSUPPORTED 分别隐藏目标、发送或点赞入口。
- 云端互动覆盖点赞/收藏/作者关注与取消关注,收藏目录原始 ID、关注列表分页;进度覆盖实际播放 start/pause/end、认证失败静默、30 秒心跳边界与重复事件抑制。
- 直播/图文覆盖纯直播源、频道分组/分页、按需节目单、实时线路切换、图文分页、正文 allow-list、图片媒体代理、外链打开、文内视频与文章评论互动。
- 信息流覆盖直接入口不取详情、真实播放列表/分集分页回退、失败重试保留条目、去重和重复游标终止、收藏与暂停/离开续播、刷新不重复 start、自动下一条开关、覆盖页面释放播放,以及导航始终位于窗口内;截图为同目录
feed-player.png。 - 修复分类页在 React StrictMode 重挂载时复用已取消请求而无法加载的问题;生产入口与浏览器测试使用相同严格模式。
- 换源覆盖精确/相似候选排序、单源搜索失败隔离、取消不建立关联、明确季集匹配(不同 index)、目标进度优先、默认不沿用旧秒数、显式沿用、含糊集号手动选择、真实跨页分集重定位,以及“我的”关联入口和卸载后保留关系;截图
source-switch-confirmation.png。 - 浏览覆盖真实来源/分区拖动、按钮移动、显隐/全部参与、重载持久化与分类来源顺序;滚动触发分页、下一页失败保留内容、原游标重试、ID 去重、重复游标终止、筛选/排序/查询取消、多源独立失败和首页刷新失败保留内容。分类树跨导航复用成功缓存,同版本替换修订后重新获取;截图
home-arrangement.png、browsing-pagination.png。 - 解析器偏好覆盖默认不记住、勾选且成功才保存、失败不覆盖旧偏好、跨播放主机切集隔离、指定解析器优先、页面重载自动路由、点播/直播共用入口,以及设置逐项忘记/清空和清空后的重载;截图
resolver-site-preference.png、resolver-preference-settings.png。 - 测试只访问本机虚构服务,没有外部内容站点请求。浏览器 stdio 桥回归不等同于实际 WebView2 窗口验收。
2026-11-15 审计与经典界面回归
当前桌面首页与详情入口共用经典沉浸播放器,选集、评论、详情通过抽屉打开;手机沿用文字底栏与播放器手势。信息流 entryEpisodeId 直接解析,打开详情/选集抽屉时才请求详情,不人为添加 playlistId。错误提示不遮挡收藏和选集。
本轮 Rust 全套(含 runtime_contract 与 public_example)通过;经典 UI 的社交、直播/图文、信息流、换源和解析器专项回归通过。Android content URI 文件导入修复后已实测;最终 APK 粘贴/文件导入及单部真实 MP4(1920×804)播放通过。完整前端 25 组、Rust 52 项通过。剩余缺口见本文限制说明,不把历史界面测试当作当前验收。
引擎补丁
rquickjs-sys 0.12.2 的普通 Promise reaction/await 未提供完整 Before/After 钩子。
本地 vendor 补丁为每条 continuation 捕获注册上下文,避免并发调用或共享 Promise 时串用 requestId。
详见 src-tauri/vendor/rquickjs-sys/HGHUB-PATCH.md;升级引擎时必须保留相应回归测试。