# 应用完整开发文档 本文是当前项目的唯一完整说明文档,面向 App 使用者、配置维护者、WebHome 首页开发者、爬虫开发者和后续改造 AI。本文按当前代码行为整理,包含项目结构、配置、Spider、WebHome、本地 HTTP 服务、网盘检测、特殊协议、隐藏功能、打包和安全注意事项。 ## 1. 项目定位 这是一个基于 CatVod 生态的 Android 影音应用,包名为 `com.fongmi.android.tv`,同时支持手机端和电视端。 核心能力: - 点播:多站点分类、详情、搜索、换源、站点健康排序、收藏、最近观看。 - 直播:M3U、TXT、JSON、EPG、时移、收藏、分组。 - 播放:ExoPlayer/Media3、FFmpeg、DRM、字幕、弹幕、倍速、缩放、画中画。 - 扩展:Java/JAR Spider、JavaScript Spider、Python Spider、HTTP API 站点。 - WebHome:CSP 自定义网页首页,可调用 App Native SDK。 - Pan:`pan.check` 提供网盘有效性检测,`pan.play` 提供网盘播放语义入口并复用 `push_agent/pvideo` 链路。 - 增强功能:集中放置网盘检测、站点健康排序、观影记录同步、管理页面、远程托管、站点注入、WebHome 扩展、登录态学习、壳代理、一键同步、调试日志等能力。 - 本地服务:局域网 HTTP 服务、管理页面、远程托管、文件管理、登录态路径管理、远程推送、远程控制、多设备同步。 - 投屏:手机端可投屏,电视端可作为 DLNA Renderer 接收投屏。 ## 2. 目录结构 ```text TV/ ├── app/ Android 主应用 ├── catvod/ CatVod 抽象层、Spider 接口、OkHttp、代理工具 ├── quickjs/ JavaScript Spider 运行时 ├── chaquo/ Python Spider 运行时 ├── other/ 其它构建或依赖模块 └── webhome-devkit/ ├── docs/应用完整开发文档.md ├── examples/ ├── templates/ └── skills/ ``` 主要源码分层: ```text app/src/main/ 手机端和电视端共用业务逻辑 app/src/mobile/ 手机端 UI app/src/leanback/ 电视端 UI app/src/main/assets 内置局域网页面和解析页 ``` ## 3. Flavor、包和打包 当前主要 flavor: | Flavor | 场景 | | --- | --- | | `mobile` | 手机/平板端 | | `leanback` | Android TV/电视盒子端 | 常用手机端 arm64 release 打包: ```bash bash gradlew assembleMobileArm64_v8aRelease ``` 当前项目常见 APK 输出路径: ```text app/build/outputs/apk/mobileArm64_v8a/release/mobile-arm64_v8a.apk Release/apk/mobile-arm64_v8a.apk ``` 实际以 Gradle 本次构建输出为准。 版本更新行为: - App 启动时不会自动弹出版本更新弹窗。 - 用户仍可在设置页手动点击版本检查。 ## 4. 配置总览 配置是 App 的主要开放入口。Vod 配置通常是 JSON。当前代码识别的顶层字段如下: ```json { "spider": "./spider.jar", "sites": [], "parses": [], "lives": [], "doh": [], "proxy": [], "hosts": [], "headers": [], "rules": [], "ads": [], "flags": [], "wallpaper": "", "logo": "", "notice": "", "home": "", "parse": "", "urls": [], "msg": "" } ``` 顶层字段: | 字段 | 类型 | 说明 | | --- | --- | --- | | `spider` | string | 全局 JAR Spider 地址。站点或直播源未单独指定 `jar` 时使用 | | `sites` | array | 点播站点列表,元素结构见“点播站点配置” | | `parses` | array | 点播解析器列表,元素结构见“解析器配置” | | `lives` | array | 直播配置列表。点播配置内带 `lives` 时,会同步生成直播配置 | | `doh` | array | DNS over HTTPS 配置,元素结构见下方 `doh` 表;配置值会追加到内置 DoH 列表后生效 | | `proxy` | array | HTTP/HTTPS/SOCKS 代理规则,元素结构见下方 `proxy` 表 | | `hosts` | array | host 覆盖规则,字符串数组,格式为 `匹配规则=目标host或IP` | | `headers` | array | 按 host 注入请求 header,元素结构见下方 `headers` 表 | | `rules` | array | 嗅探规则,元素结构见下方 `rules` 表;点播和直播规则会合并进入 `RuleConfig` | | `ads` | array | 广告域名或正则字符串数组;命中后解析 WebView 会拦截该 host | | `flags` | array | 播放 flag 字符串数组,用于识别需要解析或特殊处理的播放来源 | | `wallpaper` | string | 壁纸配置 URL 或路径;存在时会同步生成壁纸配置 | | `logo` | string | 配置图标,保存到当前 Config | | `notice` | string | 配置公告文本,保存到当前 Config | | `home` | string | 默认站点 key;App 会优先选择 `sites[].key` 等于该值的站点 | | `parse` | string | 默认解析器名称;App 会优先选择 `parses[].name` 等于该值的解析器 | | `urls` | array | 配置仓库/多配置入口,元素结构为 `{ "name": "显示名", "url": "配置URL" }`;存在该字段时 App 按 depot 处理,不再按普通点播配置解析 | | `msg` | string | 错误消息。存在该字段时加载配置会直接抛出该消息 | `doh` 元素字段: | 字段 | 类型 | 说明 | | --- | --- | --- | | `name` | string | DoH 显示名 | | `url` | string | DoH endpoint URL,例如 `https://dns.google/dns-query` | | `ips` | array | bootstrap DNS IP 字符串数组;为空时使用系统 DNS 解析 DoH host | `proxy` 元素字段: | 字段 | 类型 | 说明 | | --- | --- | --- | | `name` | string | 代理规则名称 | | `hosts` | array | host 匹配字符串数组;匹配方式是 `text.contains(rule)` 或 `text.matches(rule)` | | `urls` | array | 代理地址数组。支持 `http://host:port`、`https://host:port`、`socks://host:port`、`socks5://host:port`;带账号密码时使用 `scheme://user:pass@host:port` | `proxy.hosts` 示例: ```json { "proxy": [ { "name": "app", "hosts": ["example.com", ".*\\.example\\.com", "*"], "urls": ["socks5://127.0.0.1:7897"] } ] } ``` `proxy.hosts` 只写目标 host 匹配规则,不写 `=` 映射。`*` 表示匹配所有非本机目标 host。 顶层 `hosts` 字符串规则: ```json { "hosts": [ "example.com=1.2.3.4", ".*\\.example\\.com=cdn.example.com" ] } ``` 顶层 `hosts` 的左侧是精确 host、包含匹配片段或 Java 正则,右侧是要交给 DNS 继续解析的目标 host/IP。不要把 `example.com=1.2.3.4` 这种 DNS 写法放到 `proxy.hosts` 里。 `headers` 元素字段: | 字段 | 类型 | 说明 | | --- | --- | --- | | `host` | string | host 匹配规则;匹配方式是 `text.contains(host)` 或 `text.matches(host)` | | `header` | object | 命中后注入的 header key/value;会覆盖同名请求 header | `rules` 元素字段: | 字段 | 类型 | 说明 | | --- | --- | --- | | `name` | string | 规则名;相同名称视为同一规则 | | `hosts` | array | 适用 host 匹配规则;匹配当前 URL host 和 `url` 查询参数里的 host | | `regex` | array | 视频 URL 识别规则;URL 包含规则字符串或匹配 Java 正则时视为可播放地址 | | `exclude` | array | 排除规则;URL 包含规则字符串或匹配 Java 正则时不会被当作视频地址 | | `script` | array | 解析 WebView 页面加载后顺序执行的 JS 脚本 | 配置中的相对路径以配置文件 URL 为基准解析。`spider`、站点 `api/ext/homePage`、直播 `api/ext`、解析器 `url` 等路径会经过 `UrlUtil.convert()` 或相对 URL 解析。 ## 5. 点播站点配置 站点字段示例: ```json { "key": "site_key", "name": "站点名", "type": 3, "api": "csp_MySpider", "jar": "./spider.jar", "ext": "./ext.json", "click": "document.querySelector('video').click()", "playUrl": "", "homePage": "./nostr.html", "chromeMode": "edge", "webHomeChrome": { "mode": "edge", "statusBarStyle": "light", "navigationBarStyle": "light" }, "hide": 0, "indexs": 0, "timeout": 30, "searchable": 1, "changeable": 1, "quickSearch": 1, "categories": ["电影", "剧集"], "header": { "User-Agent": "Mozilla/5.0" }, "style": { "type": "rect", "ratio": 1.33 } } ``` 字段说明: | 字段 | 类型 | 说明 | | --- | --- | --- | | `key` | string | 站点唯一标识。原生详情、播放、收藏、历史都会使用该值定位站点 | | `name` | string | 展示名 | | `type` | number | 站点类型,完整取值见下方站点 `type` 表;为空时按 `0` 处理 | | `api` | string | API 地址或 Spider 类名。`type=3` 时为 `csp_Xxx`、JS 文件 URL 或 Python 文件 URL;`type=0/1/2/4` 时为 HTTP 地址 | | `jar` | string | 当前站点独立 JAR,覆盖全局 `spider` | | `ext` | string/object | 传给 Spider `init` 的扩展参数;配置解析阶段会把 URL 或相对路径转成绝对地址,Spider 初始化前如果是远程 URL 会拉取文本 | | `click` | string | WebView 解析点击脚本,解析页面加载后会优先执行 | | `playUrl` | string | 站点级播放前缀或解析辅助。播放结果中 `playUrl` 也可用 `json:`、`parse:` 前缀指定解析器行为 | | `homePage` | string | 自定义 WebHome 首页 | | `home_page` | string | `homePage` 别名 | | `webHome` | string | `homePage` 别名 | | `web_home` | string | `homePage` 别名 | | `chromeMode` | string | WebHome 默认 chrome 模式。手机端支持 `normal` / `edge` / `immersive`;TV 端支持 `tv-normal` / `tv-toolbar-hidden` / `tv-overlay` / `tv-full`,跨端配置里的 `edge` 在 TV 端映射为 `tv-full` | | `webHomeChrome` | object/string | WebHome 默认 chrome 配置。对象可包含 `mode`、`statusBarStyle`、`navigationBarStyle`、`restoreAffordance`、`scrim`;字符串按 `mode` 处理 | | `hide` | number | `0` 显示,`1` 隐藏。为空时按 `0` 处理 | | `indexs` | number | `0` 普通站点,`1` 索引站点。索引站点条目点击进入聚合搜索 | | `timeout` | number | 播放超时,单位秒;为空时使用默认 15 秒,最小按 1 秒处理 | | `searchable` | number | `0` 永久禁用搜索,`1` 当前启用搜索,`2` 当前禁用搜索但允许 App 根据用户操作改回 `1`;为空时按 `1` 处理 | | `changeable` | number | `0` 永久禁用换源,`1` 当前允许换源,`2` 当前禁用换源但允许 App 根据用户操作改回 `1`;为空时按 `1` 处理 | | `quickSearch` | number | `0` 禁用快速搜索,`1` 启用快速搜索。为空时按 `1` 处理 | | `categories` | array | 字符串数组,限制分类列表;为空数组表示不限制 | | `header` | object | 当前站点请求 header,key/value 对象 | | `style` | object | 卡片样式,结构见“样式配置” | 站点 `type` 取值: | type | 名称 | 行为 | | --- | --- | --- | | `0` | XML API | 首页按 XML 解析;分类/详情请求带 `ac=videolist`;播放结果走站点 `playUrl` 或默认解析逻辑 | | `1` | JSON API | 首页按 JSON 解析;分类/详情请求带 `ac=detail`;分类筛选参数会以 `f={json}` 传给接口 | | `2` | JSON API 兼容类型 | 返回按 JSON 解析;分类/详情请求带 `ac=detail`;分类筛选不会自动追加 `f` 参数 | | `3` | Spider | 调用 Java/JAR、JS 或 Python Spider 标准方法 | | `4` | HTTP API + Base64 ext | 首页请求带 `filter=true`;分类请求带 `ext={base64(extend)}`;播放请求带 `play` 和 `flag` | `homePage` 配置后,切换到该站点主页时会加载 WebHome,而不是原生推荐页。新版手机端 WebHome 默认按 `edge` 预应用,背景可画到系统栏背后但系统栏仍显示;TV 端默认按 `tv-full` 使用全屏 WebView overlay,显式 `chromeMode: "normal"` / `tv-normal` 才保留顶部 toolbar。旧页面如果尚未适配安全区,可显式设 `chromeMode: "normal"` 先保持原生顶部/底部 UI。App 会记录上一次成功加载的 WebHome 首页 chrome 配置,下一次冷启动若当前配置 URL 和首页站点 key 仍匹配,会在 Activity 首帧前预应用该 chrome,减少先显示原生顶部/底部再切换到融合模式的闪烁;配置加载后仍以真实站点配置校正,非 WebHome 或加载失败会恢复 `normal`。 ## 6. 解析器配置 解析器字段示例: ```json { "name": "解析名", "type": 1, "url": "https://example.com/parse?url=", "ext": { "flag": ["qq", "iqiyi"], "header": { "User-Agent": "Mozilla/5.0" } }, "header": {}, "click": "" } ``` 字段说明: | 字段 | 说明 | | --- | --- | | `name` | 解析器名称,也是 UI 中展示和 `parse:` 前缀匹配的名称 | | `type` | 解析器类型。完整取值见下表;为空时按 `0` 处理 | | `url` | 解析地址。`type=0/1` 时通常是 HTTP 地址;`type=2/3` 时是 JAR parser key;`type=4` 由 App 内置生成 | | `ext.flag` | 字符串数组,适用播放 flag。为空数组表示不过滤 flag | | `ext.header` | 解析请求 header,key/value 对象 | | `header` | 简写 header,会合并到 `ext.header` | | `click` | WebView 点击脚本 | 解析器 `type` 取值: | type | 名称 | 行为 | | --- | --- | --- | | `0` | Web 解析 | 打开解析 WebView 嗅探真实播放地址 | | `1` | JSON 解析 | 请求 `url + webUrl`,读取返回 JSON 里的 `url` 或 `data.url` | | `2` | JAR Json 扩展解析 | 调用当前 JAR 中 `com.github.catvod.parser.Json{url}` 的 `parse(jxs, webUrl)` | | `3` | JAR Mix 扩展解析 | 调用当前 JAR 中 `com.github.catvod.parser.Mix{url}` 的 `parse(jxs, flag, parseName, webUrl)` | | `4` | 聚合解析 | App 内置“聚合”解析:并发尝试符合 flag 的 JSON 解析器和 Web 解析器 | 当解析 URL 已带 `?` 且 `ext` 非空时,App 会追加 `cat_ext={base64(ext)}`。 播放结果里的 `playUrl` 前缀: | 前缀 | 行为 | | --- | --- | | `json:{url}` | 临时使用 `type=1` JSON 解析器,解析地址为 `{url}` | | `parse:{name}` | 使用配置里名称等于 `{name}` 的解析器 | | 无前缀且非空 | 作为 `type=0` Web 解析地址 | ## 7. 直播配置 直播源支持传统文本、M3U 和 JSON。点播配置里的 `lives` 会同步生成直播配置,直播配置自身也可以独立加载。 JSON 直播源示例: ```json { "name": "直播源", "url": "https://example.com/live.m3u", "api": "csp_LiveSpider", "ext": "", "jar": "./spider.jar", "click": "", "logo": "https://example.com/logo/{name}.png", "epg": "https://example.com/epg.xml.gz", "ua": "Mozilla/5.0", "origin": "https://example.com", "referer": "https://example.com/", "timeZone": "Asia/Shanghai", "timeout": 30, "header": {}, "catchup": { "type": "append", "days": "7", "regex": "/PLTV/", "source": "?playseek=${(b)yyyyMMddHHmmss}-${(e)yyyyMMddHHmmss}", "replace": "/PLTV/,/TVOD/" }, "groups": [ { "name": "央视", "pass": "", "channel": [ { "name": "CCTV-1", "urls": ["https://example.com/live/cctv1.m3u8"], "logo": "https://example.com/logo/cctv1.png" } ] } ] } ``` 直播源类型: | 类型 | 识别方式 | 说明 | | --- | --- | --- | | M3U | 文本包含 `#EXTM3U` 且不是 `#genre#` 文本源 | 解析 `#EXTINF`、`group-title`、`tvg-*`、`catchup-*`、`#EXTVLCOPT`、`#KODIPROP` 等字段 | | TXT | 文本使用 `频道名,播放地址`,分组行包含 `#genre#` | 同一频道多地址可用 `#` 分隔;单条地址可用 `url|header参数` 携带 header | | JSON | 文本是 JSON array | 按 `groups[].channel[]` 结构解析 | | Spider 直播 | `api` 非空 | 调用当前直播 Spider 的 `liveContent(url)` 返回直播文本或 JSON | 直播根字段: | 字段 | 类型 | 说明 | | --- | --- | --- | | `name` | string | 直播源唯一名称 | | `type` | number | 直播源类型兼容字段;站点注入的常规直播列表 URL 使用 `0` | | `url` | string | 直播源 URL;`api` 为空时 App 直接请求该地址 | | `playerType` | number | 播放器类型兼容枚举;站点注入缺省写入 `2` | | `api` | string | 直播 Spider 类名或脚本地址;非空时调用 `liveContent(url)` | | `ext` | string/object | 传给直播 Spider 的扩展参数 | | `jar` | string | 当前直播源独立 JAR,覆盖全局 `spider` | | `click` | string | 直播解析 WebView 点击脚本 | | `logo` | string | 频道 logo 模板或默认 logo。支持 `{id}`、`{name}`、`{logo}` 替换 | | `epg` | string | EPG 地址,支持多个地址用英文逗号分隔;XMLTV 支持 `.xml` 和 `.gz` | | `ua` | string | 直播请求 User-Agent,会合并进频道请求 header | | `origin` | string | 直播请求 Origin,会合并进频道请求 header | | `referer` | string | 直播请求 Referer,会合并进频道请求 header | | `timeZone` | string | EPG 时区字符串,缺省使用系统时区 | | `keep` | string | App 保存的最近直播频道,配置作者通常不需要手写 | | `timeout` | number | 播放超时,单位秒;为空时使用默认 15 秒,最小按 1 秒处理 | | `header` | object | 直播源级 header,会和 `ua/origin/referer` 合并 | | `catchup` | object | 回看规则,字段见下方 `catchup` 表 | | `core` | object | TVBus/特殊内核配置,字段见下方 `core` 表 | | `groups` | array | JSON 直播分组,元素结构见下方 `group` 表 | | `boot` | boolean | App 内保存的直播启动状态,配置作者通常不需要手写 | | `pass` | boolean | 分组密码处理开关;为 `true` 时分组名里的 `_密码` 不会被拆成密码 | 直播枚举字段: | 字段 | 值 | 含义 | | --- | --- | --- | | `type` | `0` | 常规直播列表 URL。站点注入只推荐使用这个值;其它兼容值可通过完整 Live JSON 保留,但当前注入表单不把它们作为常用配置入口 | | `playerType` | `0` | 系统播放器兼容值 | | `playerType` | `1` | IJK 播放器兼容值 | | `playerType` | `2` | EXO/ExoPlayer 播放器兼容值;站点注入默认使用这个值 | `group` 字段: | 字段 | 类型 | 说明 | | --- | --- | --- | | `name` | string | 分组名称。TXT/M3U 分组名含 `_密码` 时,默认会拆出 `pass` | | `pass` | string | 分组密码;非空时该分组视为隐藏分组 | | `channel` | array | 频道列表,元素结构见下方 `channel` 表 | `channel` 字段: | 字段 | 类型 | 说明 | | --- | --- | --- | | `name` | string | 频道名称 | | `urls` | array | 播放地址数组;每项可以是 `url` 或 `url$线路名` | | `number` | string | 频道号;为空时 App 按顺序生成三位编号 | | `logo` | string | 频道 logo | | `epg` | string | 频道 EPG 名或地址;直播源 `epg` 模板可用 `{epg}` 替换该值 | | `ua` | string | 频道级 User-Agent | | `click` | string | 频道级 WebView 点击脚本 | | `format` | string | 媒体格式/MIME;M3U 的 `mpd`、`dash` 会转为 `application/dash+xml`,`hls` 会转为 `application/x-mpegURL` | | `origin` | string | 频道级 Origin | | `referer` | string | 频道级 Referer | | `tvgId` | string | EPG tvg-id;为空时使用 `tvgName` | | `tvgName` | string | EPG tvg-name;为空时使用频道 `name` | | `catchup` | object | 频道级回看规则,优先级高于直播源级 `catchup` | | `header` | object | 频道级 header,会和 `ua/origin/referer` 合并 | | `parse` | number | `0` 不强制 Web 解析,`1` 强制进入解析 WebView | | `drm` | object | DRM 配置,字段见下方 `drm` 表 | `catchup` 字段: | 字段 | 类型 | 说明 | | --- | --- | --- | | `type` | string | 只有 `default` 有特殊行为:直接使用 `source` 作为回看 URL;其它字符串按追加模式处理 | | `days` | string | 回看天数,当前代码保存该字段,回看 URL 生成逻辑不读取该值 | | `regex` | string | 频道 URL 匹配规则;为空时只要 `source` 非空就认为支持回看 | | `source` | string | 回看 URL 模板,支持 `${(b)格式}`、`${(e)格式}`、`${utc:}`、`${utcend:}` | | `replace` | string | 追加模式下的 URL 替换规则,格式 `正则,替换值` | `core` 字段用于 TVBus 等特殊内核: | 字段 | 类型 | 说明 | | --- | --- | --- | | `auth` | string | TVBus auth 地址;如果 `resp` 非空,App 会改用本地 `/tvbus` 返回 `resp` | | `name` | string | TVBus name,支持 URL 文本拉取 | | `pass` | string | TVBus pass,支持 URL 文本拉取 | | `broker` | string | TVBus broker 地址 | | `domain` | string | TVBus domain,支持 URL 文本拉取 | | `resp` | string | 本地 `/tvbus` 返回内容,支持 URL 文本拉取 | | `sign` | string | Hook 签名,和 `pkg` 同时存在时启用 Hook | | `pkg` | string | Hook 包名 | | `so` | string | TVBus so 地址或路径 | | `key` | string | 当前代码保留字段,运行时未读取 | | `option` | array | TVBus option 数组,元素为 `{ "key": "选项名", "values": ["值"] }` | `drm` 字段: | 字段 | 类型 | 说明 | | --- | --- | --- | | `key` | string | License URL 或 ClearKey JSON | | `type` | string | 包含 `widevine`、`playready`、`clearkey` 时分别映射对应 DRM UUID;其它值映射空 UUID | | `forceKey` | boolean | 是否强制使用默认 license URL | | `header` | object | DRM license 请求 header | 直播 `header` 会与 `ua`、`origin`、`referer` 合并后用于请求。频道级字段优先于直播源级字段。 ## 8. 样式配置 `style` 可用于站点、分类结果、单个 Vod 条目。 字段: ```json { "type": "rect", "ratio": 1.33, "land": 1, "circle": 0 } ``` 说明: | 字段 | 类型 | 取值和默认值 | 说明 | | --- | --- | --- | --- | | `type` | string | `rect`、`oval`、`list`;为空时按 `rect` | 展示类型。其它字符串不会命中特殊分支,最终按 `rect` 视图处理 | | `ratio` | number | `rect` 默认 `0.75`,`oval` 默认 `1.0`,最大 `4` | 图片宽高比例 | | `land` | number | `0` 或 `1` | 单条目快捷字段。`land=1` 会生成 `type=rect`,`ratio` 为空时使用 `1.33` | | `circle` | number | `0` 或 `1` | 单条目快捷字段。`circle=1` 会生成 `type=oval`,`ratio` 为空时使用 `1.0` | `land/circle/ratio` 是 Vod 条目上的快捷字段;如果同时提供 `style`,以 `style` 对象为准。 ## 9. Spider 开发 App 支持三类 Spider: - Java/JAR Spider。 - JavaScript Spider,运行于 QuickJS。 - Python Spider,运行于 Chaquopy。 Java/JAR Spider 标准方法: ```java void init(Context context, String extend) String homeContent(boolean filter) String homeVideoContent() String categoryContent(String tid, String pg, boolean filter, HashMap extend) String detailContent(List ids) String searchContent(String key, boolean quick) String searchContent(String key, boolean quick, String pg) String playerContent(String flag, String id, List vipFlags) String liveContent(String url) Object[] proxy(Map params) String action(String action) void destroy() ``` ### 9.1 返回结构 分类、搜索、详情返回 JSON;XML API 站点返回 RSS XML,App 会转成同等字段。 ```json { "class": [ { "type_id": "1", "type_name": "电影", "type_flag": "", "land": 0, "circle": 0, "ratio": 0 } ], "filters": { "1": [ { "key": "area", "name": "地区", "init": "全部", "value": [ { "n": "大陆", "v": "大陆" } ] } ] }, "list": [ { "vod_id": "123", "vod_name": "影片名", "vod_pic": "https://example.com/poster.jpg", "vod_remarks": "更新至 12 集" } ], "page": 1, "pagecount": 10, "total": 200 } ``` 顶层返回字段: | 字段 | 类型 | 说明 | | --- | --- | --- | | `class` | array | 分类列表,元素结构见下方 `class` 表 | | `filters` | object | 分类筛选表;key 为 `type_id`,value 为筛选数组 | | `list` | array | Vod 条目列表,元素结构见下方 `vod` 表 | | `page` | number/string | 当前页码,App 透传给 UI 或调用方 | | `pagecount` | number/string | 总页数;`Result` 内部字段名是 `pagecount` | | `total` | number/string | 总条数,App 透传给 UI 或调用方 | | `url` | string/array/object | 播放结果 URL 字段,结构见下方播放结果表 | | `header` | object | 播放或请求 header | | `msg` | string | 当 `code` 为 `0` 或为空时会作为 Toast 提示 | | `code` | number | 结果状态码;为空时按 `0` 处理 | `class` 元素字段: | 字段 | 类型 | 说明 | | --- | --- | --- | | `type_id` | string | 分类 ID;也可写别名 `id` | | `type_name` | string | 分类名称;也可写别名 `name` | | `type_flag` | string | 为 `1` 时分类按文件夹/子分类入口处理 | | `filters` | array | 当前分类内联筛选数组;结构同 `filters[type_id]` | | `land` | number | 分类卡片横图快捷样式,`1` 表示横图 | | `circle` | number | 分类卡片圆形快捷样式,`1` 表示圆形 | | `ratio` | number | 分类卡片图片宽高比 | 筛选字段: | 字段 | 类型 | 说明 | | --- | --- | --- | | `key` | string | 筛选参数名,会放入 `extend` 传给分类方法 | | `name` | string | 筛选显示名 | | `init` | string | 初始选中值 | | `value` | array | 筛选值数组,元素为 `{ "n": "显示名", "v": "提交值" }` | `vod` 元素字段: | 字段 | 类型 | 说明 | | --- | --- | --- | | `vod_id` | string | 详情 ID;XML 中对应 `` | | `vod_name` | string | 影片/剧集名称;XML 中对应 `` | | `type_name` | string | 类型名称;XML 中对应 `` | | `vod_pic` | string | 海报 URL;XML 中对应 `` | | `vod_remarks` | string | 备注/更新信息;XML 中对应 `` | | `vod_year` | string | 年份;XML 中对应 `` | | `vod_area` | string | 地区;XML 中对应 `` | | `vod_director` | string | 导演;XML 中对应 `` | | `vod_actor` | string | 演员;XML 中对应 `` | | `vod_content` | string | 简介;XML 中对应 `` | | `vod_play_from` | string | 播放线路名,多个线路用 `$$$` 分隔 | | `vod_play_url` | string | 播放列表,线路之间用 `$$$` 分隔;单线路内多集用 `#` 分隔;单集格式 `集名$播放地址` | | `vod_tag` | string | 为 `folder` 时条目作为文件夹/子分类入口 | | `action` | string | 点击条目时触发 Spider `action()` 或 HTTP API action,不进普通详情 | | `cate` | object | 子分类入口配置;存在时条目作为文件夹入口 | | `style` | object | 单条目覆盖样式,结构见“样式配置” | | `land` | number | 单条目横图快捷样式,`1` 表示横图 | | `circle` | number | 单条目圆形快捷样式,`1` 表示圆形 | | `ratio` | number | 单条目图片宽高比 | | `vodFlags` / XML `dl/dd` | array/XML | XML 播放线路结构;JSON 通常直接使用 `vod_play_from` 和 `vod_play_url` | 详情返回通常是 `{ "list": [vod] }`。搜索返回的 `vod` 会由 App 自动补上站点信息,用于聚合搜索和换源显示。 播放返回示例: ```json { "parse": 0, "jx": 0, "url": "https://example.com/video.m3u8", "playUrl": "", "header": { "Referer": "https://example.com/" }, "format": "application/x-mpegURL", "subs": [ { "url": "https://example.com/sub.srt", "name": "中文", "lang": "zh", "format": "application/x-subrip", "flag": 1 } ], "danmaku": [ { "name": "弹幕", "url": "https://example.com/danmaku.xml" } ], "drm": null, "position": 0, "artwork": "https://example.com/art.jpg", "flag": "线路名", "click": "", "msg": "" } ``` 播放结果字段: | 字段 | 类型 | 说明 | | --- | --- | --- | | `parse` | number | `0` 不强制 Web 解析,`1` 强制进入解析 WebView;为空时按 `0` 处理 | | `jx` | number | `1` 时等同需要解析;为空时按 `0` 处理 | | `url` | string/array/object | 播放地址。字符串表示单地址;数组按 `[显示名1, 地址1, 显示名2, 地址2]` 解析;对象结构为 `{ "values": [{ "n": "显示名", "v": "地址" }], "position": 0 }` | | `playUrl` | string | 播放前缀。支持 `json:{url}`、`parse:{name}` 或普通 Web 解析地址 | | `header` | object | 播放请求 header;Cookie 会被保存到 CookieStore | | `format` | string | 媒体 MIME/格式,传给播放器 MediaSource | | `subs` | array | 字幕数组,元素字段见下方 `subs` 表 | | `danmaku` | array | 弹幕数组,元素字段见下方 `danmaku` 表 | | `drm` | object/null | DRM 配置,字段见直播 `drm` 表 | | `position` | number | 起播位置,毫秒 | | `artwork` | string | 播放器封面 URL | | `flag` | string | 当前播放线路名;为空时 App 会使用调用播放器时传入的 flag | | `click` | string | 解析 WebView 点击脚本 | | `msg` | string | Toast 提示;仅 `code` 为 `0` 或为空时显示 | | `code` | number | 播放结果状态码,空值按 `0` | | `jxFrom` | string | 解析来源标记 | | `desc` | string | 播放描述文本 | | `key` | string | 播放站点 key;App 内部会在部分链路写入 | `subs` 元素字段: | 字段 | 类型 | 说明 | | --- | --- | --- | | `url` | string | 字幕 URL 或路径 | | `name` | string | 字幕名称 | | `lang` | string | 字幕语言 | | `format` | string | 字幕 MIME;为空时部分本地注入链路会按扩展名推断 | | `flag` | number | Media3 subtitle selection flag;为空时按默认字幕处理 | `danmaku` 元素字段: | 字段 | 类型 | 说明 | | --- | --- | --- | | `name` | string | 弹幕名称;为空时显示 `url` | | `url` | string | 弹幕 URL 或路径 | ### 9.2 Vod 条目特殊字段 | 字段 | 说明 | | --- | --- | | `action` | 点击条目时触发 Spider `action()` 或 HTTP API action,不进普通详情 | | `vod_tag: "folder"` | 条目作为文件夹/子分类入口 | | `cate` | 条目作为子分类入口 | | `style` | 单条目覆盖样式 | | `land` / `circle` / `ratio` | 单条目样式快捷字段 | ### 9.3 proxy 返回 Java/JAR Spider 代理返回: ```java new Object[]{200, "video/mp2t", inputStream, headers} ``` `Object[]` 含义: | 下标 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `0` | number 或 `NanoHTTPD.Response` | 是 | HTTP 状态码;如果直接返回 `Response`,App 原样返回 | | `1` | string | 是 | Content-Type | | `2` | InputStream | 是 | 响应流 | | `3` | Map | 否 | 响应 header | JS Spider 普通模式 `proxy(params)` 返回数组: ```js [200, "text/plain; charset=utf-8", "content", { "Cache-Control": "no-store" }, 0] ``` JS/Python proxy 数组字段: | 下标 | 类型 | 说明 | | --- | --- | --- | | `0` | number | HTTP 状态码 | | `1` | string | Content-Type | | `2` | string/bytes | 响应内容。字符串默认按 UTF-8 输出 | | `3` | object | 响应 header,可为空 | | `4` | number | `1` 表示第 2 项是 Base64;内容含 `base64,` 前缀时 App 会自动截掉前缀再解码 | CatVod 兼容 JS proxy 模式下,`proxy(segments, headers)` 返回 JSON 字符串,运行时会按 `Res` 结构转换为本地代理响应。 ## 10. JS Spider 运行时 JS Spider 运行在 QuickJS。模块可以通过 `export default` 导出对象或工厂函数,也可以导出 CatVod 兼容的 `__jsEvalReturn()`。 当前运行时实际调用的导出方法: | 方法 | 参数 | 返回 | 说明 | | --- | --- | --- | --- | | `init(ext)` | `ext` | 任意 | 初始化。普通 JS 模式下 `ext` 为字符串或对象;CatVod 兼容模式下为 `{ stype: 3, skey, ext }` | | `home(filter)` | `filter: boolean` | string | 返回首页分类 JSON | | `homeVod()` | 无 | string | 返回首页推荐 JSON | | `category(tid, pg, filter, extend)` | `tid: string`,`pg: string`,`filter: boolean`,`extend: object` | string | 返回分类页 JSON | | `detail(id)` | `id: string` | string | 返回详情 JSON。注意 JS 运行时只传 `ids[0]`,不是数组 | | `search(key, quick)` | `key: string`,`quick: boolean` | string | 旧搜索签名 | | `search(key, quick, pg)` | `key: string`,`quick: boolean`,`pg: string` | string | 分页搜索签名 | | `play(flag, id, flags)` | `flag: string`,`id: string`,`flags: array` | string | 返回播放 JSON | | `live(url)` | `url: string` | string | 返回直播内容 | | `sniffer()` | 无 | boolean | 是否手动嗅探,映射 Java `manualVideoCheck()` | | `isVideo(url)` | `url: string` | boolean | 判断 URL 是否为视频格式 | | `proxy(params)` | 普通模式:`params: object`;CatVod 兼容模式:`segments: array, headers: object` | array 或 string | 本地代理回调,详见 proxy 返回 | | `action(action)` | `action: string` | string | 处理 `Vod.action` 或外部 action | | `destroy()` | 无 | 任意 | 释放资源 | JS 全局工具: | 工具 | 签名 | 返回 | 说明 | | --- | --- | --- | --- | | `req` | `req(url, options)` | object | 同步 HTTP。实际等价于 `http(url, { ...options, async: false })` | | `http` | `http(url, options)` | Promise 或 object | 默认返回 Promise;`options.async === false` 时同步返回 object | | `getPort` | `getPort()` | number | 当前本地 HTTP 服务端口 | | `getProxy` | `getProxy(local)` | string | 当前 JS Spider 代理 URL;`local=true` 返回 `127.0.0.1`,`false` 返回局域网 IP,自动带 `?do=js` | | `js2Proxy` | `js2Proxy(dynamic, siteType, siteKey, url, headers)` | string | CatVod 兼容代理 URL。`dynamic=true` 时使用局域网 IP,`false` 使用 `127.0.0.1` | | `joinUrl` | `joinUrl(parent, child)` | string | 按 URL 规则解析相对路径 | | `s2t` | `s2t(text)` | string | 简转繁 | | `t2s` | `t2s(text)` | string | 繁转简 | | `md5X` | `md5X(text)` | string | MD5 并写日志 | | `aesX` | `aesX(mode, encrypt, input, inBase64, key, iv, outBase64)` | string | AES 工具并写日志 | | `rsaX` | `rsaX(mode, pub, encrypt, input, inBase64, key, outBase64)` | string | RSA 工具并写日志 | | `setTimeout` | `setTimeout(fn, delay)` | null | QuickJS 定时执行 | | `local.get` | `local.get(rule, key)` | string | 读取 Native `Prefers` 字符串 | | `local.set` | `local.set(rule, key, value)` | void | 写入 Native `Prefers` 字符串 | | `local.delete` | `local.delete(rule, key)` | void | 删除 Native `Prefers` 字符串 | JS `req/http` options: | 字段 | 类型 | 取值和默认值 | 说明 | | --- | --- | --- | --- | | `async` | boolean | `http` 默认异步;`req` 固定 `false` | `false` 时同步返回 object,其它情况 Promise 返回 object | | `method` | string | `get`、`post`、`header`;默认 `get` | `post` 发送 POST;`header` 发送 HEAD;其它值按 GET | | `headers` | object | 默认 `{}` | 请求 headers | | `data` | object | 默认 `undefined` | POST 数据,配合 `postType` 使用 | | `body` | string | 默认 `undefined` | 原始请求体。仅当 `data` 为空且 header 含 `Content-Type` 时使用 | | `postType` | string | `json`、`form`、`form-data`;默认 `json` | `json` 发 JSON body;`form` 发 `application/x-www-form-urlencoded`;`form-data` 发 multipart 表单 | | `timeout` | number | 默认 `10000` | 毫秒 | | `redirect` | number | `1` 跟随重定向,`0` 不跟随;默认 `1` | 传给 OkHttp client | | `buffer` | number | `0`、`1`、`2`、`3`;默认 `0` | `0` 返回文本;`1` 返回 byte 数组形式的 JSArray;`2` 返回 Base64 字符串;`3` 返回原始 byte[] | JS `req/http` 返回 object: | 字段 | 类型 | 说明 | | --- | --- | --- | | `code` | number/string | HTTP 状态码;异常时为空字符串 | | `headers` | object | 响应头;同名多值为数组 | | `content` | string/array/bytes | 响应内容,由 `buffer` 决定;异常时为空字符串 | ## 11. Python Spider 运行时 Python Spider 运行在 Chaquopy。Spider 类继承 `base.spider.Spider` 时,当前运行时会调用下列方法: | 方法 | 参数 | 返回 | 说明 | | --- | --- | --- | --- | | `init(self, extend="")` | `extend: string` | 任意 | 初始化。运行时会先把 `getDependence()` 返回的依赖 py 文件下载到缓存目录,再调用本方法 | | `homeContent(self, filter)` | `filter: bool` | string | 返回首页分类 JSON | | `homeVideoContent(self)` | 无 | string | 返回首页推荐 JSON | | `categoryContent(self, tid, pg, filter, extend)` | `tid: string`,`pg: string`,`filter: bool`,`extend: dict` | string | 返回分类页 JSON | | `detailContent(self, ids)` | `ids: list` | string | 返回详情 JSON | | `searchContent(self, key, quick, pg="1")` | `key: string`,`quick: bool`,`pg: string` | string | 返回搜索 JSON | | `playerContent(self, flag, id, vipFlags)` | `flag: string`,`id: string`,`vipFlags: list` | string | 返回播放 JSON | | `liveContent(self, url)` | `url: string` | string | 返回直播内容 | | `localProxy(self, param)` | `param: dict` | list | 本地代理回调,结构见 proxy 返回 | | `isVideoFormat(self, url)` | `url: string` | bool | 判断 URL 是否为视频格式 | | `manualVideoCheck(self)` | 无 | bool | 是否手动嗅探 | | `action(self, action)` | `action: string` | string | 处理 `Vod.action` 或外部 action | | `destroy(self)` | 无 | 任意 | 释放资源 | | `getName(self)` | 无 | string | 返回 Spider 名称;基类默认 `None` | | `getDependence(self)` | 无 | list | 返回依赖 py 模块名列表,不带 `.py` 后缀 | Python 基类工具: | 工具 | 签名 | 说明 | | --- | --- | --- | | `loadSpider` | `self.loadSpider(name)` | 从缓存目录加载 `{name}.py` 并返回其中的 `Spider()` | | `loadModule` | `self.loadModule(name)` | 从缓存目录加载 `{name}.py` 模块 | | `regStr` | `self.regStr(reg, src, group=1)` | 正则提取字符串 | | `removeHtmlTags` | `self.removeHtmlTags(src)` | 移除 HTML 标签 | | `cleanText` | `self.cleanText(src)` | 移除 emoji 范围字符 | | `fetch` | `self.fetch(url, params=None, cookies=None, headers=None, timeout=5, verify=True, stream=False, allow_redirects=True)` | `requests.get` 包装,响应编码设为 UTF-8 | | `post` | `self.post(url, params=None, data=None, json=None, cookies=None, headers=None, timeout=5, verify=True, stream=False, allow_redirects=True)` | `requests.post` 包装,响应编码设为 UTF-8 | | `html` | `self.html(content)` | `lxml.etree.HTML(content)` | | `str2json` | `Spider.str2json(text)` | `json.loads(text)` | | `json2str` | `Spider.json2str(value)` | `json.dumps(value, ensure_ascii=False)` | | `getProxyUrl` | `self.getProxyUrl(local=True)` | 返回 Python Spider 代理 URL,自动带 `?do=py` | | `log` | `self.log(msg)` | 打印日志;dict/list 会转 JSON | | `getCache` | `self.getCache(key)` | 通过本地 `/cache` 读取字符串或 JSON;如果 JSON 对象含 `expiresAt` 且已过期会自动删除 | | `setCache` | `self.setCache(key, value)` | 通过本地 `/cache` 写入字符串、数字、dict 或 list | | `delCache` | `self.delCache(key)` | 通过本地 `/cache` 删除 key | ## 12. 播放和特殊协议 App 播放输入可以来自配置、Spider、WebHome、外部 Intent、本地 HTTP 推送。 支持或识别的协议/格式: | 协议/格式 | 说明 | | --- | --- | | `http://` / `https://` | 普通网络媒体 | | `rtsp://` / `rtmp://` / `smb://` | 外部打开和部分播放链路支持 | | `push://真实地址` | 让当前播放页再次打开一个新播放地址 | | `assets://path` | 读取 APK assets,经本地服务转换 | | `file://path` | 读取 App 本地路径,经本地服务转换 | | `proxy://query` | 转到 Spider 本地代理 | | `.strm` | 读取第一行作为真实播放地址 | | YouTube URL | 使用 NewPipe 提取播放地址;播放列表可展开成多集 | | `magnet:` | 迅雷内核解析 BT | | `.torrent` | 迅雷内核解析种子 | | `ed2k:` | 迅雷内核解析 | | `thunder:` | 详情解析阶段可展开或转换 | | `jianpian:` / `tvbox-xg:` / `ftp:` | 荐片/XG P2P 解析 | `UrlUtil.convert()` 会把下面伪协议转换成本地服务地址: ```text assets://path -> http://127.0.0.1:{port}/path file://path -> http://127.0.0.1:{port}/file/path proxy://query -> http://127.0.0.1:{port}/proxy?query ``` ## 13. 本地 HTTP 服务 App 启动后会启动 NanoHTTPD 服务,端口从 `9978` 到 `9998` 依次尝试,取第一个可用端口。 地址: ```text http://127.0.0.1:{port} http://{局域网IP}:{port} ``` `/device` 返回局域网地址、设备类型和时间。 ```json { "uuid": "android-id", "name": "device-name", "ip": "http://192.168.1.23:9978", "type": 1, "time": 1710000000000 } ``` 设备类型:`0` 电视端,`1` 手机端,`2` DLNA 设备记录。 ### 13.1 端点总览 | 路径 | 方法 | 能力 | | --- | --- | --- | | `/` | GET | 内置局域网基础管理网页 | | `/m` | GET | 增强管理页面,入口来自“设置 -> 增强功能 -> 管理页面” | | `/device` | GET/POST | 设备信息 | | `/media` | GET/POST | 当前播放状态 | | `/api/playback/current` | GET/POST/OPTIONS | 当前播放记录安全快照,供 JS/Python/PHP/Jar 等爬虫调用 | | `/api/playback/progress` | POST/OPTIONS | 写入单条观影进度,受“观影记录同步 -> 允许本机 API 修改”开关控制 | | `/api/playback/progress/batch` | POST/OPTIONS | 批量写入观影进度,支持 `{ "items": [...] }`、`records/data/list` 等常见批量外壳 | | `/api/playback/progress/delete` | POST/OPTIONS | 清理本地观影进度,支持按 `historyKey`、`siteKey+vodId`、站点或当前配置清理 | | `/action` | GET/POST | 播放控制、搜索、推送、刷新、同步、设置、投放 | | `/cache` | GET/POST | 字符串缓存 | | `/file/{path}` | GET | 浏览或下载 App 本地文件 | | `/upload` | POST | 上传文件;zip 自动解压 | | `/newFolder` | POST | 新建文件夹 | | `/delFolder` | POST | 删除文件夹 | | `/delFile` | POST | 删除文件 | | `/parse` | GET/POST | 内置解析 HTML | | `/proxy` | GET/POST | Spider 本地代理 | | `/webResource` | GET/POST/PUT/PATCH/DELETE/HEAD/OPTIONS | WebHome 资源网关 | | `/pan/check` | POST/OPTIONS | 网盘检测 | | `/debug/logs` | GET | 调试日志网页 | | `/debug/logs.txt` | GET | 下载当前调试日志 | | `/debug/clear` | GET | 清空调试日志 | | `/debug/enable` / `/debug/disable` | GET | 开启或关闭调试日志 | | `/debug/stream` | GET | 调试日志增量轮询接口,返回版本、行数、字节数和日志文本 | | `/manage/session` | GET/POST | 管理页面会话状态、保活、后台访问提示 | | `/manage/background/settings` | GET/POST | 尝试打开后台耗电/后台运行相关系统设置页 | | `/manage/devices` | GET/POST | 发现并列出同网段 App 设备 | | `/manage/remote/ping` | GET/POST | 检测远端 App 设备连通性 | | `/manage/action` | GET/POST | 转发 `/action` 到远端 App 设备 | | `/manage/remote/file` | GET/POST | 代理读取或下载远端 App 文件 | | `/manage/remote/archive` | GET/POST | 代理打包下载远端 App 文件或目录 | | `/manage/remote/upload` | POST | 代理上传文件到远端 App | | `/manage/remote/newFolder` | POST | 代理在远端 App 新建目录 | | `/manage/remote/delFolder` | POST | 代理删除远端 App 目录 | | `/manage/remote/delFile` | POST | 代理删除远端 App 文件 | | `/manage/sync/paths` | GET/POST | 读取或保存一键同步目录 | | `/manage/sync/tree` | GET/POST | 浏览外置存储目录树,用于勾选同步目录 | | `/manage/sync/detect` | GET/POST | 自动识别已配置接口里的本地包目录并加入同步目录 | | `/manage/sync/start` | POST | 从管理页面发起一键同步 | | `/manage/login-state` | GET/POST | 读取登录态学习状态、已确认路径、待确认路径和最近发现项 | | `/manage/login-state/learn` | GET/POST | 开始或完成登录态学习,参数 `action=begin/finish` | | `/manage/login-state/paths` | GET/POST | 保存已确认登录态路径 | | `/manage/login-state/tree` | GET/POST | 浏览登录态可选目录树,包含 App 私有目录和共享存储 | | `/manage/login-state/file` | GET/POST | 读取或写入已授权路径下的登录态文件内容 | | `/manage/file/archive` | GET/POST | 把一个或多个本地目录/文件打包下载 | | `/manage/proxy` | GET/POST | 读取或保存壳代理开关、默认代理和规则 | | `/manage/csp` | GET/POST | 读取或保存站点注入配置 | | `/manage/csp/page` | POST | 保存站点注入 WebHome 代码或链接 | | `/tvbus` | GET/POST | TVBus auth response | ### 13.1.1 管理页面 `/m` “管理页面”是增强功能里的浏览器管理入口,不是 App 内嵌 WebView 页面。点击“设置 -> 增强功能 -> 管理页面”后,App 会启动前台管理服务并给出本机地址和局域网地址: ```text http://127.0.0.1:{port}/m http://设备IP:{port}/m ``` 页面能力: - 本机管理:操作当前手机或电视 App 的文件、登录态、同步目录、站点注入、接口和壳代理。 - 远端管理:先发现并选择同网段 App 设备,再操作远端设备的文件、登录态、同步、搜索和推送。 - 文件管理:浏览外置存储目录、上传文件、新建目录、删除、下载单文件,勾选多个文件或目录后可打包下载。 - 一键同步:发现设备、选择推送或拉取、勾选同步内容、通过目录树选择 Jar/脚本保存数据目录,并可同步已确认登录态。 - 登录态学习:开始/完成学习,查看已确认路径、待确认项和最近发现项,管理 App 私有目录和共享存储里的登录态文件。 - 站点注入:启用/禁用全局注入和单条条目,新增 WebHome、通用 CSP 或直播,支持文件、代码、链接和 JSON 配置。 - 壳代理 Proxy:启用/禁用代理,编辑默认代理地址,支持表单规则、JSON/raw 文本规则和自动识别导入。 - 接口管理:查看本机或远端设备的配置入口,配合远端管理做浏览器侧维护。 可用性策略: - 管理页面开启后会启动前台服务,持有局部 WakeLock 和 Wi-Fi lock,并定期保持本地 HTTP 服务运行。 - 页面或接口访问会刷新会话时间;超过约 10 分钟没有访问会自动停止服务。 - 手机进入后台后,如果系统厂商限制后台网络,页面会提示打开后台耗电或后台运行设置。不同品牌设置页不完全一致,无法保证直接跳到最后一级,只能按已知页面逐级降级。 - 当前没有鉴权,只应在可信局域网内临时开启,用完后在通知或弹窗里停止管理页面。 ### 13.2 `/action` `/action` 支持 `GET` 和 `POST`。返回固定为 `200 OK`,大部分动作异步投递到 App 内部事件总线;参数缺失时通常直接忽略。 `do` 取值: | do | 必要参数 | 可选参数 | 行为 | | --- | --- | --- | --- | | `search` | `word` | 无 | 触发 App 搜索 | | `push` | `url` | 无 | 推送播放地址 | | `setting` | `text` | `name` | 写入配置文本或配置 URL;`name` 为显示名称 | | `refresh` | `type` | `path`、`json` | 刷新指定模块或注入资源,`type` 取值见下表 | | `control` | `type` | 无 | 播放控制,`type` 取值见下表 | | `sync` | `type`、`mode` | `force`、`device`、`config`、`targets`、`configs` | 多设备同步,`type` 和 `mode` 取值见下表 | | `cast` | `config`、`device`、`history` | 无 | 投放到另一台 App | | `file` | `path` | 无 | 使用 App 本地文件:`.apk` 打开安装,`.srt/.ssa/.ass` 注入字幕,其它路径按配置加载 | 搜索、推送、设置: ```text GET/POST /action?do=search&word=关键词 GET/POST /action?do=push&url=播放地址 GET/POST /action?do=setting&text=配置内容或配置URL&name=显示名称 ``` `refresh.type` 取值: | type | 额外参数 | 行为 | | --- | --- | --- | | `home` | 无 | 刷新点播首页 | | `live` | 无 | 刷新直播 | | `detail` | 无 | 刷新当前详情页 | | `player` | 无 | 刷新当前播放器 | | `category` | 无 | 刷新当前分类页 | | `subtitle` | `path` | 注入字幕 URL 或路径 | | `danmaku` | `path` | 注入弹幕 URL 或路径 | | `vod` | `json` | 用 `Vod` JSON 更新当前播放页关联条目 | 播放控制 `control.type` 取值: | type | 行为 | | --- | --- | | `play` | 播放 | | `pause` | 暂停 | | `stop` | 停止播放服务 | | `prev` | 上一集/上一项 | | `next` | 下一集/下一项 | | `loop` | 切换循环行为 | | `replay` | 重播或刷新当前播放 | 多设备同步: ```text POST /action?do=sync&type=history&mode=0&force=false POST /action?do=sync&type=keep&mode=0&force=false POST /action?do=sync&type=backup&mode=1&force=false ``` `sync.type` 取值: | type | 说明 | | --- | --- | | `history` | 同步观看历史 | | `keep` | 同步收藏 | | `backup` | 按同步选项同步接口、Jar/脚本保存数据、登录态、WebHome 缓存、搜索记录、观看历史、收藏和设置 | `sync.mode` 取值: | mode | 说明 | | --- | --- | | `0` | 双向同步 | | `1` | 从远端同步到本机 | | `2` | 从本机发送到远端 | `sync.force` 取值: | force | 说明 | | --- | --- | | `false` | 合并同步 | | `true` | 同步前先清空本机对应数据再写入 | `backup` 请求使用表单字段: | 字段 | 类型 | 说明 | | --- | --- | --- | | `options` | JSON string | 同步选项。字段为 `config`、`spider`、`loginState`、`search`、`history`、`keep`、`webHome`、`settings`、`paths` | | `backup` | JSON string | `mode=1` 时由发送端携带的数据库和 SharedPreferences 备份数据 | | `device` | JSON string | `mode=2` 时由请求端携带的本机设备信息,远端会回传备份数据 | | `syncFiles` | file | 可选。`multipart/form-data` 文件字段,内容为同步目录压缩包;仅 `spider=true` 时使用 | | `loginStateFiles` | file | 可选。`multipart/form-data` 文件字段,内容为登录态学习已确认路径压缩包;仅 `loginState=true` 且有可同步路径时使用 | 同步选项说明: | 选项 | 默认 | 说明 | | --- | --- | --- | | `config` | `true` | 接口订阅、点播/直播/壁纸配置和当前接口指针 | | `spider` | `true` | Jar/脚本保存数据。包含默认 SharedPreferences 中非 App 设置项,也会同步 `paths` 指定的外置存储目录 | | `loginState` | `true` | 登录态学习中已确认的 Cookie、Token、SharedPreferences、cache 或接口 Jar 登录状态文件 | | `search` | `true` | 搜索关键词和热词缓存 | | `history` | `true` | 观看历史 | | `keep` | `true` | 收藏 | | `webHome` | `true` | WebHome 通过 `fm.cache` 保存的数据 | | `settings` | `false` | App 通用设置、播放设置、弹幕设置、增强功能开关等 | | `paths` | `"TV\nTVBox\nTVData"` | 同步目录列表,每行一个外置存储相对路径;App 端和管理页面都支持用目录树勾选 | 目录同步规则: | 项目 | 说明 | | --- | --- | | 默认目录 | `/sdcard/TV`、`/sdcard/TVBox`、`/sdcard/TVData` | | 自定义目录 | 在一键同步界面点击“同步目录”后用目录树勾选;管理页面“一键同步 -> 同步目录”也可勾选目录树。内部保存为外置存储相对路径,例如 `TVBox`、`TVData/cookie` | | 本地包识别 | 管理页面支持“自动识别本地包”。它会扫描 App 已保存的点播、直播、壁纸配置 URL;只处理 `file:`、绝对路径或相对路径形式的本地包,忽略 `http(s)`、`proxy://`、`assets://` 等在线或内置地址。若目录已被 `TV`、`TVBox`、`TVData` 等现有同步目录覆盖,不会重复添加 | | 传输格式 | 发送端把同步目录打成 zip,通过 `multipart/form-data` 的 `syncFiles` 字段发送;登录态路径打成单独 zip,通过 `loginStateFiles` 字段发送;接收端按各自根路径恢复 | | 进度显示 | 发送端在压缩阶段显示已处理文件数、原始大小和压缩速度;上传阶段显示文件数量、原始大小、压缩后大小、上传百分比、已传/总量和实时速度;同步过程中可取消 | | 超时策略 | 使用长传输超时,正常持续上传/接收不会按普通短请求超时处理 | | 恢复行为 | 文件包先恢复,再恢复 `backup` 数据;恢复后会清理 Jar/spider 加载缓存并重新加载当前接口,让新 cookie/本地包尽快生效 | | 当前边界 | 当前是单 zip 流式传输,不是分片并发和断点续传;后续如要支持断点,需要增加文件清单、分片 hash、临时目录、续传协商和合并校验协议 | 很多接口 Jar 会把夸克、UC、阿里云盘、迅雷、115、天翼、123、PikPak、WebDAV 等账号 Cookie 或授权状态保存到 `/sdcard/TV`、`/sdcard/TVBox`、`/sdcard/TVData` 或用户自定义目录中,因此需要勾选“Jar/脚本保存数据”才能随一键同步迁移。若登录态写入 App 私有目录,例如 `shared_prefs`、`files` 或 `cache`,应先用“登录态学习”确认路径,再勾选“一键同步 -> 登录态”迁移。 投放到另一台 App: ```text POST /action?do=cast ``` `cast` 参数包含 `config`、`device`、`history`,值均为 JSON 字符串。 ### 13.3 `/media` 返回当前播放器状态: ```json { "state": 3, "speed": 1.0, "duration": 3600000, "position": 600000, "url": "https://example.com/video.m3u8", "title": "标题", "artist": "", "artwork": "" } ``` `state`:`1` 其它,`2` ready,`3` playing,`6` buffering。 ### 13.4 观影记录同步 观影记录同步用于把 App 本地播放进度和用户自有服务、爬虫、多端 App 之间打通。它包含四条通道: - 当前播放只读 API:爬虫读取当前播放条目的安全快照。 - 本机修改 API:爬虫或局域网工具写入、批量写入或清理本地播放进度。 - 远端同步:App 主动从用户配置的远端 API 拉取观影记录并合并到 `History`。 - Webhook 上报:App 在播放过程中主动把进度 POST 到用户服务器。 增强功能入口为“观影记录同步”。顶层只显示总开关、本机 API 修改开关、远端同步摘要和 Webhook 摘要;远端同步源列表与 Webhook 端点列表点击后进入二级界面,新增/编辑均在独立弹窗中完成。 #### 13.4.1 字段模型 播放记录协议版本固定为 `webhtv.playback.v1`。时间字段均为 Unix epoch milliseconds,进度单位为毫秒。 | 字段 | 类型 | 出现位置 | 说明 | | --- | --- | --- | --- | | `schema` | string | 读取、Webhook | 固定为 `webhtv.playback.v1` | | `event` | string | Webhook | 事件名,目前固定使用 `playback.progress` 和 `playback.ended` | | `eventId` | string | Webhook | 单次事件 UUID,同一次重试保持不变 | | `timestamp` | number | 读取、Webhook | App 生成记录的时间 | | `sessionId` | string | 读取、Webhook | 本次播放会话 ID,切换播放条目后更新 | | `dedupeKey` | string | 读取、Webhook | 播放条目去重 key,由接口、站点、影片、剧集等字段计算,不包含 token | | `cid` | number | 读取、写入、Webhook | 本机点播配置 id,只用于本机落库空间,跨设备不稳定 | | `configKey` | string | 读取、写入、远端同步、Webhook | 点播接口稳定身份,当前为点播接口 URL 的 SHA256;服务端应按 `token + configKey` 分组 | | `configName` | string | 读取、写入、远端同步、Webhook | 点播接口显示名,仅用于展示和排查 | | `historyKey` | string | 读取、写入、清理、Webhook | 本地历史主键,通常为 `siteKey@@@vodId` 或 `siteKey@@@vodId@@@cid` | | `siteKey` | string | 读取、写入、远端同步、Webhook | 站点 key | | `siteName` | string | 读取、Webhook | 站点显示名,取不到时回退为 `siteKey` | | `vodId` | string | 读取、写入、远端同步、Webhook | 影片 id | | `vodName` | string | 读取、写入、远端同步、Webhook | 影片名称 | | `vodPic` | string | 读取、写入、远端同步、Webhook | 封面 URL,可为空 | | `flag` | string | 读取、写入、远端同步、Webhook | 播放线路或来源名称 | | `episodeName` | string | 读取、写入、远端同步、Webhook | 当前集名称,对应 `History.vodRemarks` | | `episodeUrl` | string | 写入、远端同步、完整 Webhook | 当前集原始地址,用于更精确匹配剧集 | | `state` | string | 读取、Webhook | `idle`、`buffering`、`playing`、`paused`、`ended` | | `positionMs` | number | 读取、写入、远端同步、Webhook | 当前播放进度 | | `durationMs` | number | 读取、写入、远端同步、Webhook | 视频总时长 | | `progress` | number | 读取、写入、Webhook | `positionMs / durationMs`,写入时可作为 `positionMs` 的辅助计算值 | | `speed` | number | 读取、写入、Webhook | 播放倍速,缺省按 `1.0` 处理 | | `completed` | boolean | 读取、写入、Webhook | 是否完播;写入时为辅助字段,最终仍以进度和阈值校验 | | `updatedAt` | number | 写入、远端同步 | 远端记录最后更新时间;缺省时 App 使用当前时间 | | `client` | string | 标准/完整 Webhook | 客户端 flavor,例如 mobile、leanback | | `appVersion` | string | 标准/完整 Webhook | App 版本名 | | `clientKey` | string | 完整 Webhook、写入 | 设备或爬虫侧标识;基础/标准预设不默认发送 | `cid` 不能作为跨端接口身份。同一远端 URL 和 token 下,服务端仍必须按 `configKey` 区分不同点播接口;否则 A 客户端的 b 接口记录可能覆盖 B 客户端的 a 接口记录。 #### 13.4.2 当前播放只读 API 该接口供 JS/Python/PHP/Jar 等爬虫通过 HTTP 读取当前播放记录。接口只返回当前播放条目,不返回全局历史。调用方必须传入当前站点 `siteKey`,App 会校验它和当前播放记录一致。 ```text GET/POST /api/playback/current?siteKey=站点Key GET/POST /playback/current?siteKey=站点Key ``` 成功返回安全字段,不包含真实播放 URL、headers、cookies、配置地址、Jar/ext、设备原始标识或其它站点历史: ```json { "schema": "webhtv.playback.v1", "timestamp": 1781170000000, "sessionId": "2a9d6c9f-...", "dedupeKey": "8d1f...", "cid": 1, "configKey": "sha256(config-url)", "configName": "接口名称", "historyKey": "site_key@@@vod_id@@@1", "siteKey": "site_key", "siteName": "站点名称", "vodId": "vod_id", "vodName": "影片名", "vodPic": "https://example.com/poster.jpg", "flag": "线路1", "episodeName": "第1集", "state": "playing", "positionMs": 123456, "durationMs": 456789, "progress": 0.2702, "speed": 1.0, "completed": false } ``` 错误返回 JSON:`400` 缺少 `siteKey`,`404` 当前无可读记录或站点不匹配,`405` 方法不支持。 #### 13.4.3 本机写入与清理 API 本机修改 API 用于让爬虫或局域网工具把远端进度写回 App 本地历史,或清理误同步、已失效和测试写入的记录。调用前需要用户在“观影记录同步”中开启总开关和“允许本机 API 修改”;关闭修改开关时,只读接口仍可用。 ```text POST /api/playback/progress POST /api/playback/progress/batch POST /api/playback/progress/delete POST /playback/progress POST /playback/progress/batch POST /playback/progress/delete ``` 请求体必须使用 JSON,建议 `Content-Type: application/json; charset=utf-8`。接口直接按 UTF-8 读取原始 body,适合 JS、Python、PHP、Jar、curl 等通用 REST 调用。本机修改 API 不定义独立 token;爬虫访问用户远端服务时应使用远端服务提供的 token,写回 App 本地历史时不需要再向 App 配置额外凭证。 单条写入示例: ```json { "configKey": "sha256(config-url)", "configName": "接口名称", "siteKey": "site_key", "vodId": "vod_id", "vodName": "影片名", "vodPic": "https://example.com/poster.jpg", "flag": "线路1", "episodeName": "第1集", "episodeUrl": "https://example.com/play/1.m3u8", "positionMs": 123456, "durationMs": 456789, "speed": 1.0, "completed": false, "updatedAt": 1781170000000, "clientKey": "crawler-or-device-id" } ``` 最小必填字段: | 字段 | 说明 | | --- | --- | | `siteKey` | 目标站点 key | | `vodId` | 目标影片 id | | `vodName` | 影片名称 | | `episodeName` | 当前集名称 | | `positionMs` | 当前进度,必须大于 0 | | `durationMs` | 总时长,必须大于 0 | 可选字段: | 字段 | 说明 | | --- | --- | | `configKey` | 跨端点播接口身份。提供后 App 会映射到本机对应 `cid`;映射不到则跳过 | | `configUrl` | 可替代 `configKey`,App 会计算其 SHA256 | | `configName` | 接口显示名,仅用于响应和调试 | | `historyKey` | 已知本地历史 key。带 `configKey` 时仍会以本机映射后的 `cid` 重新生成 key,避免跨设备 `cid` 混用 | | `vodPic` | 封面 URL | | `flag` | 播放线路或来源 | | `episodeUrl` | 当前集 URL,用于优先匹配剧集 | | `progress` | 无 `positionMs` 时可用 `durationMs * progress` 推导 | | `speed` | 播放倍速,缺省为 `1.0` | | `completed` | 为 true 且有 `durationMs` 时,进度会按总时长处理 | | `updatedAt` | 远端记录更新时间,缺省为当前时间 | | `cid` | 本机配置 id。只在没有 `configKey` 时用于同一设备内指定落库空间 | 批量写入支持直接数组或常见外壳: ```json { "items": [ { "siteKey": "site_key", "vodId": "vod_id", "vodName": "影片名", "episodeName": "第1集", "positionMs": 123456, "durationMs": 456789, "updatedAt": 1781170000000 } ] } ``` `items` 也可写作 `records`、`data` 或 `list`。上面的 JSON 可直接保存为 mock 文件,用于批量写入和远端同步联调。 合并规则: | 规则 | 说明 | | --- | --- | | 配置空间 | 优先用 `configKey` 映射本机点播接口并确定 `cid`;没有 `configKey` 时使用请求 `cid` 或当前接口 | | 主定位 | 使用 `siteKey + vodId` 定位影片 | | 剧集匹配 | 优先比较 `episodeUrl`,其次比较 `flag + episodeName`,最后比较 `episodeName` | | 冲突处理 | 远端 `updatedAt` 大于本地 `History.createTime` 时才覆盖;不新于本地时返回 `skipped` | | 当前播放 | 写入只更新 `History`,不强行 seek 正在播放的视频 | 清理请求支持单对象、直接数组或 `{ "items": [...] }`: ```json { "historyKey": "site_key@@@vod_id@@@1" } ``` 清理定位规则: | 字段 | 说明 | | --- | --- | | `historyKey` | 精确清理一条本地历史,优先使用 | | `siteKey` + `vodId` | 清理同一站点同一影片下的本地历史 | | `siteKey` + `scope=site` | 清理同一站点下的本地历史;也可用 `confirm=true` 明确确认 | | `configKey` | 可选;优先用它定位本机点播接口空间 | | `scope=all` + `confirm=true` | 清理当前接口下的全部观影历史;可传 `configKey` 或本机 `cid` 指定配置空间 | 响应字段: | 字段 | 说明 | | --- | --- | | `success` | 单条是否处理成功 | | `action` | `created`、`updated`、`deleted`、`skipped`、`failed` | | `message` | 跳过或失败原因 | | `historyKey` | 写入、匹配或清理的本地历史 key | | `configKey/siteKey/vodId/episodeName` | 调用方提交的核心定位字段 | | `affected` | 写入为 1;清理时为实际删除条数 | | `localUpdatedAt` | 本地记录更新时间 | | `remoteUpdatedAt` | 远端记录更新时间 | 批量响应包含 `total/applied/created/updated/deleted/skipped/failed/items`。单条失败不影响其它条目。 #### 13.4.4 远端同步源 远端同步源是用户在 App 中配置的 HTTP API。App 按用户填写的固定 URL 发起 `GET` 请求,不额外拼接 query;返回值使用本机写入 API 的同一字段模型。 ```http GET <用户填写的远端 API> Accept: application/json X-WebHTV-Token: <服务端提供的 token> X-WebHTV-Config-Key: <当前点播接口 configKey> X-WebHTV-Config-Name: <当前点播接口显示名> ``` `X-WebHTV-Token` 可选,由用户服务端统一提供,用于鉴权和用户空间分组。App 不生成 token,也不把 token 写入本地历史主键或 `dedupeKey`。同一个远端 URL 下,同一 token 且同一 `configKey` 才表示同一套观影记录;同一 token 下不同 `configKey` 必须分别存储。 远端响应示例: ```json { "items": [ { "configKey": "sha256(config-url)", "configName": "接口名称", "siteKey": "site_key", "vodId": "vod_id", "vodName": "影片名", "vodPic": "https://example.com/poster.jpg", "flag": "线路1", "episodeName": "第1集", "episodeUrl": "https://example.com/play/1.m3u8", "positionMs": 123456, "durationMs": 456789, "speed": 1.0, "completed": false, "updatedAt": 1781170000000 } ], "nextSince": 1781170000000 } ``` 响应项带 `configKey` 时,App 会先映射本机已配置的点播接口;映射不到则跳过。响应项不带 `configKey` 时按当前点播接口写入,用于兼容旧服务端。 远端同步源配置项: | 字段 | 说明 | | --- | --- | | `name` | 可选;为空时 UI 使用 API URL 的 host | | `url` | 必填;远端 API 地址,必须以 `http` 开头 | | `token` | 可选;通过 `X-WebHTV-Token` 请求头发送 | | `siteKeys` | 可选;限制只合并指定站点 key | | `syncOnStartup` | App 启动后自动同步 | | `intervalMinutes` | 周期同步间隔,0 表示不周期同步 | | `maxItems` | 单次最多处理条数,上限 1000 | #### 13.4.5 Webhook 上报 Webhook 上报由 App 在播放过程中主动 POST 到用户配置的端点。上报时机固定为周期进度、暂停/退出/切集前最终进度和自然完播,不在 UI 中提供多事件选择。 ```http POST <用户配置的 Webhook URL> Content-Type: application/json X-WebHTV-Timestamp: 1781170000 X-WebHTV-Token: <服务端提供的可选 token> X-WebHTV-Webhook-Id: X-WebHTV-Dedupe-Key: X-WebHTV-Config-Key: X-WebHTV-Config-Name: Idempotency-Key: ``` 请求头说明: | 请求头 | 必填 | 说明 | | --- | --- | --- | | `Content-Type` | 是 | 固定为 `application/json` | | `X-WebHTV-Timestamp` | 是 | 发送请求时的秒级时间戳 | | `X-WebHTV-Token` | 否 | 用户服务器提供的 token,用于鉴权和用户空间分组 | | `X-WebHTV-Webhook-Id` | 是 | 与 payload 的 `eventId` 一致 | | `X-WebHTV-Dedupe-Key` | 是 | 与 payload 的 `dedupeKey` 一致 | | `X-WebHTV-Config-Key` | 是 | 与 payload 的 `configKey` 一致 | | `X-WebHTV-Config-Name` | 否 | 与 payload 的 `configName` 一致 | | `Idempotency-Key` | 是 | 与 `eventId` 一致,兼容常见 Webhook 幂等处理 | 事件类型: | 事件 | 说明 | | --- | --- | | `playback.progress` | 周期进度;暂停、退出、切集、切线路前会补发最终进度 | | `playback.ended` | 自然完播 | 字段预设: | 预设 | 字段 | | --- | --- | | 基础 | `schema/event/eventId/timestamp/sessionId/dedupeKey/cid/configKey/configName/historyKey/siteKey/siteName/vodId/vodName/vodPic/flag/episodeName/state/positionMs/durationMs/progress/speed/completed` | | 标准 | 基础 + `appVersion/client` | | 完整 | 标准 + `episodeUrl/episodeIndex/clientKey` | | 自定义 | 协议字段固定发送,其余字段由 UI 多选;每个字段在 UI 中显示简短说明 | 服务端建议先按 token 确定用户空间,再按 `configKey` 区分点播接口,最后用 `dedupeKey` 合并同一播放条目,用 `eventId` 或 `Idempotency-Key` 去重同一次事件重试。 ### 13.5 `/cache` ```text GET/POST /cache?do=get&rule=命名空间&key=键 GET/POST /cache?do=set&rule=命名空间&key=键&value=值 GET/POST /cache?do=del&rule=命名空间&key=键 ``` 实际存储 key: ```text cache_ + (rule ? rule + "_" : "") + key ``` ### 13.6 `/file` 和文件管理 `/file/{path}`: - path 是 App 本地根目录下路径。 - 如果是目录,返回目录 JSON。 - 如果是文件,流式返回文件内容。 - 支持 Range、ETag、Accept-Ranges。 目录返回: ```json { "parent": "", "files": [ { "name": "subtitles", "path": "/subtitles", "time": "2026-05-22 12:00:00", "dir": 1 } ] } ``` 上传: ```text POST /upload ``` 上传 zip 会自动解压到目标目录。 ## 14. WebHome 自定义主页 WebHome 是 App 为 CSP 站点扩展的自定义网页首页能力。站点配置里声明 `homePage` 后,切换到该站点主页时优先加载网页。 WebHome 用户脚本扩展的详细开发流程、加载方式、调试流程、模板和示例见第 25 章。 示例: ```json { "key": "nostr_home", "name": "Nostr 推荐", "type": 3, "api": "csp_Builtin", "homePage": "./nostr.html" } ``` 如果配置文件是在线 URL,`./nostr.html` 会相对配置文件 URL 解析。例如配置来自: ```text https://example.com/config.json ``` 则 `./nostr.html` 解析为: ```text https://example.com/nostr.html ``` WebHome 不需要配置 `bridge: "full"`,当前默认注入完整 SDK。 加载 WebHome 时会应用站点 `header`: - `User-Agent` 会覆盖 WebView 默认 UA;未配置时恢复 WebView 默认 UA。 - `Cookie` 会写入 WebView CookieManager。 - 其余 header 附加到首个页面请求;页面内的子资源请求不会自动携带,需要时用 `fm.res()`。 ## 15. WebHome 运行环境 WebView 设置: | 能力 | 状态 | | --- | --- | | JavaScript | 开启 | | DOM Storage | 开启 | | Database | 开启 | | Cache | `LOAD_DEFAULT`;`fm.reload()` 和顶部刷新会先 `clearCache` 再加载 | | Mixed Content | 允许 | | Media Playback Requires User Gesture | 不强制 | | Cookie | 开启 | | Third-party Cookie | 开启 | | 背景 | Native WebView 透明 | | 渲染 | 硬件加速 layer,渲染进程高优先级,`offscreenPreRaster`(Android 6+) | | 视口 | `useWideViewPort` + `loadWithOverviewMode`,`textZoom` 固定 100,禁用缩放 | | 滚动 | 关闭 over-scroll 效果,滚动条 overlay 样式 | App 注入: ```js window.fongmi window.fm ``` `window.fongmi` 是完整命名空间,`window.fm` 是短别名。 SDK 注入后会触发: ```js window.dispatchEvent(new CustomEvent("fmsdk")); ``` 注意:SDK 默认由 Native 在页面加载完成(`onPageFinished`)后注入,页面最早执行的内联脚本可能先于 `window.fm` 运行。例外:站点配置了 `runAt: "document-start"` 的 WebHome 扩展且当前 WebView 支持 `DOCUMENT_START_SCRIPT` 特性时,SDK 会随 document-start 脚本提前注入。WebHome 如果有账号、配置、身份、同步状态等关键持久化数据,应在 `fmsdk` 后读写 `fm.cache`;如果检测到 `window.fongmiBridge` 已存在但 `window.fm` 尚未就绪,可以短暂等待 `fmsdk`,不要立刻把关键数据写入浏览器 fallback 存储。 App 从后台恢复 WebHome 时会触发: ```js window.dispatchEvent(new CustomEvent("fmresume", { detail: { time, pausedMs } })); ``` WebHome 进入后台暂停时会触发: ```js window.dispatchEvent(new CustomEvent("fmpause", { detail: { time } })); ``` WebView 布局尺寸、安全区、系统手势区或 chrome mode 变化时,Native 会更新 `--fm-web-*`、`--fm-safe-*`、`--fm-gesture-*` 等根元素 CSS 变量,并触发: ```js window.dispatchEvent(new CustomEvent("fmviewport", { detail })); ``` `detail` 字段见第 16 章 `fm.ui.getViewport()` 说明。页面高度建议使用 `var(--fm-web-height, 100vh)` 而不是裸 `100vh`,可以规避旧 WebView 视口高度不含工具栏的偏差。 SDK 还会 hook `history.pushState` / `history.replaceState` / `popstate`,路由变化时触发: ```js window.dispatchEvent(new CustomEvent("fmurlchange", { detail: { url } })); ``` 页面可以监听这些事件重新读取配置、恢复状态、保存 UI 快照或补偿播放记录。 ### 15.1 WebView 版本基线 App `minSdk` 为 24(Android 7.0)。Android 7.0 出厂的系统 WebView 约为 Chromium 51;系统 WebView 理论上可单独升级,但电视和盒子经常没有应用商店、无法升级,长期停留在出厂版本。实际开发应按以下基线准备: - 把 Chromium 50-70 当成必须兼容的“旧内核”,按本章红线开发。 - Chromium 80+ 视为现代内核,`?.`、`??` 等语法只有在确认目标设备 ≥ 80 时才能使用。 - 开启“调试日志”后,日志 tag `webview` 会输出当前设备 WebView provider 包名和版本号,真机验收前先确认目标设备内核版本。 - 电脑浏览器预览不等于兼容验证,Chrome DevTools 无法模拟旧内核的语法支持。 ### 15.2 JS 语法红线 语法级不兼容是最危险的一类:旧内核解析到不认识的语法会直接抛 `SyntaxError`,**整个 `