# WebHome Devkit `webhome-devkit/` 是 WebHome 开发套件,统一放置完整开发文档、WebHome 首页示例、WebHome 扩展示例、起步模板和 AI skills。它不是 App 运行时的 `ext` 配置目录,也不等同于 CSP 站点字段里的 `ext`,因此放入主项目后使用 `webhome-devkit` 命名,避免和配置字段混淆。 本文主体是 WebHome 扩展脚本开发指南。 本文档面向两类读者: - 人工开发者:拿到一个真实网站 URL 后,开发 WebHome 注入脚本,让网页调用 App 原生播放、网盘、嗅探、网络和缓存能力。 - AI 开发助手:读取本文档和目标 URL 后,按第 16 章协议分析页面、生成脚本、给出配置方式和测试步骤。 使用方式:把本文档(必要时连同 `templates/extensions/` 目录)和目标网站 URL 一起交给 AI,即可直接产出可用的扩展脚本。本文档自包含扩展开发所需的全部 API 与规则;WebHome 单文件主页开发、网盘检测细节、性能改造长文见 `docs/应用完整开发文档.md`(下称"主文档"),本文按需交叉引用。若从主项目根目录访问,路径是 `webhome-devkit/docs/应用完整开发文档.md`。 WebHome 扩展脚本的主流场景不是把网站完全改写成 CSP 爬虫,而是在 App WebView 里加载真实网站,然后通过注入脚本增强这个网站。例如:拦截资源按钮、把网盘/磁力/直链交给 App 播放、给页面增加"App 播放"按钮、清理干扰 UI、嗅探页面运行时出现的媒体地址、按手机/电视形态重排版面。 ## 目录结构 - `examples/extensions//`:站点扩展示例,每个站点独立放 JS 和 manifest。 - `examples/homepages/`:单文件 WebHome 首页示例,例如 `nostr.html`。 - `templates/extensions/`:扩展脚本模板和调试辅助脚本。 - `templates/homepages/`:首页起步模板;完整首页范式以 `examples/homepages/` 和 skill demo 为准。 - `skills/`:Codex/Claude/OpenCode 等客户端可安装的 WebHome skills。 - `docs/`:长文档和跨主题说明。 ## 1. 能力模型与架构 WebHome = 一个真实网页 + App 注入的 `fm` SDK + 用户/配置注入的扩展脚本。 注入管线(对应 Native 实现 `HomeWebController` / `WebHomeExtensionRegistry` / `WebHomeExtension`): ```text 站点配置(homePage) ──► WebView 加载真实网页 │ 配置三层扩展来源 ──► Registry 解析/匹配/排序/依赖检查 ──► ready 列表 │ document-start: SDK + start 扩展(WebView 支持时提前注入) onPageFinished: SDK 注入 ──► document-end 扩展 ──► 600ms 后 document-idle 扩展 ``` 脚本可以做的事: - 读取和改写当前网页 DOM、注入样式(`GM_addStyle`)。 - 捕获用户点击,阻止网页默认跳转。 - 从 `href`、`data-url`、`onclick`、文本、剪贴板逻辑、网络请求里提取资源 URL。 - 调用 `fm.play()` 播放直链;`fm.vodInline()` 构造多集播放并按集回调解析。 - 调用 `fm.pan.play()` 把网盘、磁力、电驴、迅雷、荐片等推送到 App 既有解析链路。 - 调用 `fm.pan.check()` 检测支持的网盘分享是否有效。 - 调用 `fm.req()` 用 Native OkHttp 请求接口,绕开普通 WebView `fetch` 的 CORS 限制。 - 调用 `fm.res()` 把图片、视频、字幕等 DOM 资源转成本地资源网关地址。 - 调用 `fm.cache` / `GM_getValue` 保存脚本配置和轻量状态。 - 通过 `console.log()`、`GM_log()`、`fm.ext.log()` 和调试工作台排查行为。 - 按 `fongmiClient.isLeanback` 区分手机/TV,分别优化布局和遥控焦点。 脚本不适合做的事: - 不应在没有用户动作的情况下批量打开播放页。 - 不应把所有普通站内链接都拦截成播放;必须先判断是不是资源链接。 - 不应依赖过度脆弱的第 N 个子元素选择器,例如 `body > div:nth-child(4) > a:nth-child(2)`。 - 不应为了"万能"而破坏网站自己的搜索、筛选、登录、翻页和详情跳转。 - 不应把账号、Cookie、隐私数据输出到远程日志或第三方服务。 运行边界(由 Native 包装层强制): - 扩展只在**顶层 frame** 执行(包装层开头 `if(window.top!==window)return;`),iframe 内不会运行。 - 每个扩展独立包装在一个 IIFE 里,互相不共享局部变量;需要共享时挂到 `window` 并用 `depends` 声明顺序。 - 包装层自带 `try/catch`:运行期异常会被捕获、打到 console 和扩展日志,并派发 `fmexterror` 事件。但**语法错误无法被捕获**——整段脚本解析失败、静默不执行(见第 13 章)。 ## 2. 快速上手 10 分钟从零跑通第一个扩展: 1. 准备一个带 `homePage` 的站点并切换为主页站点(站点配置见主文档第 5、14 章)。 2. 打开 App:设置 → 增强功能 → WebHome 扩展。确认顶部总开关开启(默认开启)。 3. 点"新增",选择"代码",粘贴最小脚本: ```js GM_log("hello", location.href); GM_addStyle("body{outline:4px solid #0f766e;}"); window.addEventListener("fmsdk", function () { fm.ext.toast("扩展已生效"); }); ``` 4. 保存。回到 WebHome 页面(扩展管理保存后会触发刷新;也可手动下拉刷新或点扩展管理里的 Refresh)。 5. 看到页面出现绿色描边和 toast 即成功。点"Debug"打开调试工作台,Console 里应有 `[fm-ext] hello ...`。 6. 后续迭代:在扩展管理的代码编辑里改脚本 → 保存 → 自动刷新预览;定型后托管成远程 `.js` + manifest 分发。 ## 3. 扩展来源与加载机制 ### 3.1 三层来源与优先级 | 层 | 配置位置 | 作用域 | 默认启用 | 排序基数 | | --- | --- | --- | --- | --- | | 全局扩展 | 点播配置根级 `webHomeExtensions` | 不绑定站点,**必须**用 `cspKeyRegex` 限定 | **否**(需 `"enabled": true` 或用户在管理页手动启用) | 0 | | 站点扩展 | `sites[].extensions` | 天然绑定所属站点 | 是 | 10000 | | 用户扩展源 | App 内"WebHome 扩展"管理页 | 可绑定某个站点或全部站点 | 是 | 20000 | 加载顺序:全局 → 站点 → 用户。**按 `id` 去重,后加载的覆盖先加载的**,因此用户本地扩展可以用相同 `id` 覆盖配置下发的扩展(调试改版常用)。同层内按书写顺序保持稳定排序。 全局扩展默认禁用是有意设计:配置作者批量下发的脚本对用户而言是"可选增强",用户需在扩展管理页里逐个确认启用(启用远程脚本时会弹确认)。希望默认生效的站点增强应写进 `sites[].extensions`。 ### 3.2 总开关与启用状态 - 总开关:设置 → 增强功能 → WebHome 扩展,对应 `Setting.isWebHomeExtension()`,默认开启。关闭后所有扩展不加载、不注入。 - 单扩展开关:按扩展 `id` 持久化(preferences key `web_home_ext_enabled_`)。用户的手动开关**优先于** manifest 的 `enabled`/`disabled` 声明。 - manifest 里 `"disabled": true` 强制默认禁用;`"enabled": true/false` 显式设置默认值;都没写时用所在层的默认值(见 3.1 表)。 ### 3.3 远程脚本缓存与更新 - 远程 `.js` 和 manifest 下载后缓存在 App cache 目录 `webhome_ext/`(按 URL md5 命名)。 - 每次加载都是**网络优先**:先尝试重新下载,成功则更新缓存;失败(断网、源挂了)回退使用缓存副本。所以扩展离线可用,但源端更新会在下次成功联网加载时生效。 - 扩展管理页"Clear cache"清空缓存目录并重载;"Refresh"强制重新准备并刷新页面。 - 扩展配置变化触发的页面重载有 **5 秒节流**,避免连续保存导致反复刷新。 - `updateUrl` 字段会被解析保存(管理页可展示/跳转),当前不做自动版本比对升级;版本管理依靠源 URL 内容更新 + 网络优先策略。 ## 4. 配置格式 ### 4.1 扩展对象完整字段 ```json { "id": "pomo-native-router", "name": "Pomo native router", "version": "1.2.0", "runAt": "document-end", "cspKeyRegex": ["^pomo$"], "excludeCspKeyRegex": [], "js": ["https://example.com/webhome/pomo.mom.js"], "code": "", "depends": [], "updateUrl": "", "enabled": true } ``` | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `id` | string | 否 | 扩展唯一 ID。为空时自动生成 `md5((sourceUrl 或 siteKey) + ":" + 序号)`——**配置顺序变化会改变自动 id 并丢失用户开关状态**,正式分发必须固定 `id` | | `name` | string | 否 | 展示名称;为空时用 `id` | | `version` | string | 否 | 版本号,按 `.` 和 `-` 分段比较(数字段按数值、非数字段按字符串),可被 `depends` 约束 | | `runAt` | string | 否 | `document-start` / `document-end` / `document-idle`;非法值和缺省都按 `document-end` | | `cspKeyRegex` | string[] / string | 否 | 匹配站点 `key` 的**正则**(`find` 语义,非全匹配;精确匹配写 `^key$`)。全局扩展必填;站点/用户扩展填写后会收窄默认作用域。非法正则会被忽略并记日志 | | `excludeCspKeyRegex` | string[] / string | 否 | 排除站点 `key` 的正则,命中则不注入(优先级高于 include) | | `js` | string[] / string | 否 | 外部 JS 地址,支持相对地址(相对 manifest/配置 URL 解析)和 `file://`、`local://` | | `code` | string | 否 | 内联 JS 代码。`js` 和 `code` 至少一个,否则该扩展被丢弃;两者同时存在时 `code` 先执行,`js` 依次拼接其后 | | `depends` | string[] / string | 否 | 依赖其它扩展 `id`,支持版本约束 `id@>=1.0.0`(见 4.7) | | `updateUrl` | string | 否 | 更新地址,支持相对解析;当前仅记录展示 | | `enabled` / `disabled` | boolean | 否 | 默认启用状态声明(见 3.2) | | `manifestUrl` / `manifest` / `sourceUrl` / `url` | string | 否 | 指向远程 manifest 或 JS(见 4.5);对象自身没有 `js`/`code` 时才会按远程指针处理 | | `extensions` | array | 否 | 嵌套一组扩展(manifest 容器形态,见 4.5) | ### 4.2 URL 协议与相对解析 | 形态 | 解析方式 | | --- | --- | | `https://...` / `http://...` | 直接下载(网络优先 + 缓存回退) | | `./path.js`、`../x/y.json`、`path.js` | 相对所在配置/manifest 的 URL 解析 | | `file://` | 读取设备本地路径(适合本地调试,无缓存) | | `local://` | 读取 App files 目录下文件 | URL 是否被当作"脚本"判定规则:包含 `.js` 或以 `file://`、`local://` 开头按脚本处理;其余按 manifest JSON 下载解析。 ### 4.3 站点内联扩展 站点自己的 `extensions` 与当前站点一一对应,通常不需要写 `cspKeyRegex`。站点对象仍要保留普通 CSP 外壳(`type: 3`、`api: "csp_Builtin"`、`homePage`): ```json { "key": "pomo", "name": "Pomo", "type": 3, "api": "csp_Builtin", "homePage": "https://pomo.mom/", "extensions": [ { "id": "pomo-native-router", "name": "Pomo native router", "runAt": "document-end", "js": ["https://example.com/webhome/pomo.mom.js"] } ] } ``` 三种等价简写(适合只挂远程 JS): ```json "extensions": ["https://www.252035.xyz/dm.xueximeng.com.js"] ``` ```json "extensions": "https://www.252035.xyz/dm.xueximeng.com.js" ``` ```json "extensions": { "js": ["https://www.252035.xyz/dm.xueximeng.com.js"] } ``` 字符串 URL 指向 `.js` 时自动生成 `id`(`remote_`)/`name`(文件名),按默认 `document-end` 注入。简写无法设置 `version`、`runAt`、`depends`;需要时换完整对象。 站点注入顶部有"识别"按钮,可粘贴单个或多个松散站点 JSON 片段(包括 `{...}, {...}` 或结尾带逗号的对象),自动归类并追加到 UI 列表或 JSON 文本。站点注入表单里 Key 下方有"扩展"开关,打开后输入框显示为"扩展 URL / JSON"。最简单只填一个 JS URL,例如 `https://example.com/site.js`;也兼容 `extensions` 数组或单个扩展对象,保存时规范化。也可以点"文件"选择本地 `.js` / `.css` / `.json`,本地 JS 会生成内联 `code` 扩展对象,CSS 会生成 `GM_addStyle(...)`,JSON 会规范化为 `extensions` 数组。 ### 4.4 全局扩展 点播配置根级 `webHomeExtensions`,适合配置作者批量下发。必须用 `cspKeyRegex` 限定站点,且记住默认禁用(3.1): ```json { "webHomeExtensions": [ { "manifestUrl": "https://example.com/webhome/extensions.json", "cspKeyRegex": ["^pomo$", "^dm-xueximeng$"], "enabled": true } ] } ``` 外层对象上的字段(如 `cspKeyRegex`、`enabled`、`runAt`)会**合并覆盖**到远程 manifest 解析出的扩展上(远程指针字段本身除外),适合在不改源文件的情况下收窄作用域或改默认状态。 ### 4.5 远程 manifest 形态 `manifestUrl` 指向的 JSON 支持三种形态: ```json [ { "id": "a", "js": ["./a.js"] }, { "id": "b", "js": ["./b.js"] } ] ``` ```json { "extensions": [ { "id": "a", "js": ["./a.js"] } ] } ``` ```json { "id": "a", "name": "Single", "runAt": "document-end", "js": ["./a.js"] } ``` manifest 内的相对路径相对 manifest URL 解析。`extensions` 容器可以嵌套(容器上的 `enabled`/`disabled` 会下传为子项默认值)。 ### 4.6 用户扩展源(App 内) 入口:设置 → 增强功能 → WebHome 扩展。管理页提供总开关、新增、调试工作台(Debug)、刷新、清缓存,并分"User sources"与"Loaded extensions"两组展示。 | 新增方式 | 适合场景 | 说明 | | --- | --- | --- | | 文件 | 本地维护 `.js`、`.json` | 读取文件内容保存为本地代码源 | | 链接 | 扩展托管在 HTTP 服务、GitHub raw、CDN | 可填 `.js` 或 manifest JSON 地址,可附带名称/runAt/匹配范围 | | 代码 | 直接在 App 内写 JS | 开发调试主力;保存即刷新预览 | | 表单 | 只填 ID、名称、运行时机、匹配规则和 JS 地址 | 不想手写 manifest 的用户 | | 文本 JSON | 维护完整扩展源 JSON | 批量导入、复制、AI 输出 | 用户源匹配范围默认从当前点播配置里的 WebHome 站点弹窗多选,保存时写成精确 `cspKeyRegex`;也可以切换到正则模式手写 CSP key 正则。每个用户源有独立启用开关,源记录持久化在 preferences(`web_home_extension_user_sources`)。 ### 4.7 依赖 `depends` ```json { "id": "my-site", "depends": ["tv-focus-helper", "shared-lib@>=1.2.0"] } ``` - 约束运算符:`>=`、`>`、`<=`、`<`、`=`(缺省为 `=`)。版本按 `.`/`-` 分段,数字段数值比较。 - 依赖检查在"匹配 + 启用"筛选之后进行,不满足的扩展整组跳过(级联:A 依赖 B、B 被跳过则 A 也跳过),状态标记 `skipped`,原因可在管理页看到:缺少依赖 / 依赖未启用 / 依赖未匹配当前站点 / 依赖不可用 / 依赖版本不满足 / **依赖注入时机更晚**(依赖的 `runAt` 必须不晚于使用方:start ≤ end ≤ idle)/ 依赖存在循环。 - 注入顺序:先按 `runAt`(start → end → idle)再按配置顺序,最后做依赖拓扑排序,保证依赖先于使用方执行。 - 被依赖的库扩展把共享 API 挂在 `window` 上(如 `window.fmTvHelper = {...}`),使用方直接读取。 ## 5. 注入时机与生命周期 ### 5.1 runAt | `runAt` | 适用 | 实际注入点与注意 | | --- | --- | --- | | `document-start` | 提前 Hook `window.open`、`fetch`、`XMLHttpRequest`、历史路由,拦截站点最早注册的全局行为 | 依赖 androidx.webkit `DOCUMENT_START_SCRIPT` 特性(需较新系统 WebView)。支持时 SDK + start 扩展在文档创建时执行;**不支持时自动降级到 document-end 注入**(日志可见 `document-start downgraded`),脚本必须能容忍降级——Hook 写成幂等、DOM 逻辑不要假设 body 不存在 | | `document-end` | 大多数 DOM 增强、点击拦截、按钮注入 | 默认值。在 `onPageFinished` 注入 SDK 之后立即执行,主体 DOM 通常可读,但 SPA 内容可能仍在异步渲染——配合 `MutationObserver` | | `document-idle` | 非关键增强、批量扫描、网盘检测、性能开销大的任务 | document-end 批次之后约 600ms 注入,避免影响首屏和首次点击 | 经验规则: - 要拦截网站很早注册的全局行为,优先 `document-start`,同时写好降级逻辑。 - 要找按钮、资源区、标题、卡片,优先 `document-end`。 - 要做大范围扫描、可见区网盘检测、媒体性能条目扫描,优先 `document-idle` 或在 end 里延迟执行。 ### 5.2 包装层与生命周期事件 每个扩展被包装成独立 IIFE 注入,结构上等价于: ```js (function(){ if (window.top !== window) return; // 仅顶层 frame const __fmExt = { id, name, siteKey, source, runAt }; // 当前扩展元信息 // GM_addStyle / GM_log / GM_getValue / GM_setValue / GM_deleteValue / GM_xmlhttpRequest(见第 6 章) try { /* 你的脚本(code 先、js 依次拼接) */ window.dispatchEvent(new CustomEvent("fmextload", { detail: __fmExt })); } catch (e) { console.error("[fm-ext]", __fmExt.id, e && e.stack || e); // 同时写入扩展日志,并派发: window.dispatchEvent(new CustomEvent("fmexterror", { detail: { ...__fmExt, message } })); } })(); //# sourceURL=fm-ext-.js ``` 要点: - `//# sourceURL` 让 Console 报错能定位到 `fm-ext-.js`。 - 运行期异常被捕获并上报;**SyntaxError 不会**——整段 evaluate 失败,`fmextload`/`fmexterror` 都不触发,表现为"扩展像没装一样"。排查方法见第 13、14 章。 - 同一次页面加载中每个扩展只注入一次(按 id 去重);页面跳转/刷新后重新注入。 - 扩展配置变更(保存、开关、刷新)会重新准备并 reload 页面(5 秒节流)。 ### 5.3 扩展状态机 管理页和 `fm.ext.info()` 反映 Registry 状态,调试时按此判断卡在哪一步: | 状态 | 含义 | 常见原因 | | --- | --- | --- | | `unmatched` | 未匹配当前站点 | `cspKeyRegex` 写错(记住是对站点 key 的正则 find,不是对域名) | | `disabled` | 被禁用 | 全局扩展默认禁用;用户手动关闭 | | `matched` | 匹配且启用,待依赖检查 | - | | `skipped` | 依赖检查未通过 | 见 4.7 失败原因 | | `ready` | 进入注入列表 | - | | `injected` | 已注入当前页面 | reason 字段记录实际注入时机 | ## 6. 脚本运行环境与 GM API 包装层提供以下油猴风格 API。**与 Tampermonkey 的关键差异:存取值是异步的(返回 Promise)**。 | API | 签名 | 语义 | | --- | --- | --- | | `GM_addStyle(css)` | 同步,返回 `