ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

WebToApp 模块市场发布指南:从 modules/ 目录结构、CI 校验到 PR 合并上线的完整流程

WebToApp 模块市场发布指南:从 modules/ 目录结构、CI 校验到 PR 合并上线的完整流程 WebToApp 模块市场发布指南从 modules/ 目录结构、CI 校验到 PR 合并上线的完整流程【免费下载链接】web-to-appThe most full featured web-to-app toolkit on Android, a complete APK workshop that runs entirely on your phone项目地址: https://gitcode.com/GitHub_Trending/web/web-to-app本文基于 WebToAppweb-to-app仓库的模块市场发布文档完整讲解社区 JS/CSS 扩展模块的提交流程modules/目录布局、registry.json与module.json的双文件约定、CI 校验器的全部检查规则、submissions.json的自动生成机制以及客户端拉取与镜像回退的实现细节。读完本文你可以独立完成一个可被 CI 校验通过、合并后即刻对全量客户端可见的市场模块。市场架构GitHub 仓库就是后端模块市场Module Market是一个由 GitHub 支撑的社区 JS/CSS 扩展模块目录。没有后端——应用直接从本仓库拉取目录文件因此贡献就是一个普通的 pull request 流程PR 一旦合并到main下一刻就上线。权威提交规则、字段 schema、审核清单和 CI 校验规范位于 modules/README.md本文覆盖其快速入门要点并深入校验器与客户端源码。目录布局modules/ ├── registry.json # 面向应用的目录 ├── submissions.json # CI 生成的 PR / 贡献者元数据 ├── README.md # 贡献者指南 └── module-folder/ # 每个模块应用同时拉取registry.json和submissions.json并且只显示两者中都存在的模块使应用内目录与实际合并的 PR 保持一致。这不是宣传语而是写死在客户端代码里的过滤逻辑ModuleMarketRepository.kt 中用submissions[entry.id] ?: returnmapNotNull null对每个 registry 条目做交集过滤——submissions.json里没有的条目直接从列表中消失客户端不存在任何可绕过的本地白名单或过滤器。客户端拉取、缓存与镜像从 ModuleMarketRepository.kt 可以看到客户端的完整拉取配置仓库固定为shiaho777/web-to-app的main分支modules/目录源优先级依次为raw.githubusercontent.com与cdn.jsdelivr.net/gh/jsDelivr后者作为 CDN 回退目录文件和模块图标先经由全局镜像以raw.githubusercontent.com和 jsDelivr 作为自动回退使商店在各地包括中国大陆都能快速加载默认客户端缓存为一小时REGISTRY_TTL_MS 60 * 60 * 1000L缓存文件落在应用私有目录的module_market/下刷新按钮强制刷新force可绕开缓存。因此合并的模块无需应用更新即可传播PR 合入main后所有客户端在下次刷新或缓存过期后即可看到新模块。点击安装时才进一步下载module-path/module.json和main.js外加style.css若hasCss为true然后交给本地扩展管理器。添加一个模块第一步创建 kebab-case 模块文件夹在modules/下创建一个kebab-case文件夹命名正则见校验器 validate_modules.py 中的^[a-z0-9](?:-[a-z0-9])*$modules/my-module/ ├── module.json # 必需 ├── main.js # 必需 ├── style.css # 可选 └── icon.png # 可选,≤256KB文件夹名就是registry.json里的path字段。除运行时实际下载的module.json/main.js/style.css与icon.*图标文件外校验器允许存在的额外文件只有README.md、CHANGELOG.md、LICENSE/LICENSE.md、.gitkeep见 validate_modules.py 的ALLOWED_EXTRA_FILES多余的子目录和文件会收到 warning——运行时根本不会下载它们只浪费仓库空间。第二步编写 module.json 清单module.json是安装时真正被解析的清单manifest。以仓库自带的示例模块 modules/hello-world/module.json 为模板{ id: globally-unique-id, name: Display Name, description: Paragraph shown on the install page., icon: material-icon-name, category: OTHER, tags: [tag1, tag2], version: { code: 1, name: 1.0.0, changelog: Initial release }, author: { name: Your Name, url: https://github.com/your-handle, email: optionalexample.com }, runAt: DOCUMENT_END, urlMatches: [ { pattern: *, isRegex: false, exclude: false } ], permissions: [DOM_ACCESS], configItems: [ { key: greeting, name: Greeting text, description: Shown in the floating banner., type: TEXT, defaultValue: Hello, WebToApp!, required: false } ] }注意version在这里是对象code单调递增整数校验器要求 1、namesemver 字符串、changelog。允许的枚举值与 Kotlin 端core/extension/ExtensionModule.kt中的枚举一一对应校验器里也硬编码了同一份集合category未知值回退为OTHER不会破坏安装只是让模块从分类过滤 chip 中隐藏CONTENT_FILTER、CONTENT_ENHANCE、STYLE_MODIFIER、THEME、FUNCTION_ENHANCE、AUTOMATION、NAVIGATION、DATA_EXTRACT、DATA_SAVE、INTERACTION、ACCESSIBILITY、MEDIA、VIDEO、IMAGE、AUDIO、SECURITY、ANTI_TRACKING、SOCIAL、SHOPPING、READING、TRANSLATE、DEVELOPER、OTHERrunAt省略时默认DOCUMENT_ENDDOCUMENT_START、DOCUMENT_END、DOCUMENT_IDLE、CONTEXT_MENU、BEFORE_UNLOADpermissions在安装页面上是信息性展示运行时并不据此做沙箱隔离而是供审核者识别危险能力。共 31 项DOM_ACCESS、DOM_OBSERVE、CSS_INJECT、STORAGE、COOKIE、INDEXED_DB、CACHE、NETWORK、WEBSOCKET、FETCH_INTERCEPT、CLIPBOARD、NOTIFICATION、ALERT、KEYBOARD、MOUSE、TOUCH、LOCATION、CAMERA、MICROPHONE、DEVICE_INFO、MEDIA、FULLSCREEN、PICTURE_IN_PICTURE、SCREEN_CAPTURE、DOWNLOAD、FILE_ACCESS、EVAL、IFRAME、WINDOW_OPEN、HISTORY、NAVIGATION其中危险项COOKIE、INDEXED_DB、NETWORK、WEBSOCKET、FETCH_INTERCEPT、CLIPBOARD、LOCATION、CAMERA、MICROPHONE、SCREEN_CAPTURE、FILE_ACCESS、EVAL、IFRAME在评审时会接受额外审查。configItems[].typeTEXT、TEXTAREA、NUMBER、BOOLEAN、SELECT、MULTI_SELECT、RADIO、CHECKBOX、COLOR、URL、EMAIL、PASSWORD、REGEX、CSS_SELECTOR、JAVASCRIPT、JSON、RANGE、DATE、TIME、DATETIME、FILE、IMAGE其中SELECT/MULTI_SELECT/RADIO必须提供非空的options字符串数组——校验器会明确报错因为 App 端 Gson 将ModuleConfigItem.options反序列化为ListString对象形式如[{value,label}]会导致反序列化失败。urlMatches两种模式isRegex: false推荐Chrome 扩展风格 glob。*匹配任意数量字符*://...展开为(https?|ftp|file)://all_urls和*都表示匹配所有 URLisRegex: trueJava 正则每次 URL 匹配带 200ms 超时。设exclude: true使一条规则从匹配集合中扣除如果只有 exclude 规则模块匹配除此之外的所有 URL。第三步向 registry.json 添加条目registry.json是应用首先拉取的索引。每个条目镜像模块清单外加path文件夹名和hasCss两个字段{ id: my-module, path: my-module, name: My Module, description: 它做什么, icon: star, category: CONTENT_ENHANCE, tags: [demo], version: 1.0.0, minAppVersion: 33, author: { name: You }, runAt: DOCUMENT_END, permissions: [DOM_ACCESS], urlMatches: [{ pattern: *://example.com/* }], hasCss: false, iconUrl: icon.png }两个关键点version形状不同在registry.json中它是一个semver 字符串不同于module.json里的对象取值必须是module.json::version.name的同一数字。两个 registry 专属字段pathkebab-case 文件夹名和hasCss布尔必须与磁盘上style.css的存在与否一致。其他可选字段minAppVersion整数声明模块依赖某个versionCode才有的 API——更旧的客户端会直接隐藏该条目。当前versionCode为66v2.6.4只有真正依赖较新构建时才需要设置。仓库现有条目如 registry.json 中的wta-hello-world统一使用minAppVersion: 33。iconUrl可选。相对路径icon.png、icon.svg、icon.webp、icon.jpg、icon.jpeg之一须与main.js同目录且≤ 256 KB或绝对https://URL指向仓库外托管。不设置时App 显示模块名首字母的圆形头像。iconMaterial Icons 名称如auto_awesome、dark_mode与iconUrl是两回事。第四步保持双文件一致提交 PR保持registry.json与module.json一致——id、name、version、runAt和permissions必须相符CI 会强制这一点详见下文校验规则。然后开一个 pull requestCI 会自动运行校验器维护者按审核清单审查后合并。合并后所有客户端在下次刷新默认 1 小时缓存即可看到你的模块。没有开发者账号、API key 或投稿门户。main.js 注入合约运行时把你的代码包在 IIFE 里注入 WebView注入前可用的全局全局值__MODULE_INFO__{ id, name, icon, version, uiConfig, runMode }__MODULE_CONFIG__用户保存的{ key: value }配置对象__MODULE_UI_CONFIG__UI 面板配置镜像__MODULE_INFO__.uiConfig__MODULE_RUN_MODE__INTERACTIVE或AUTOgetConfig(key, defaultValue)__MODULE_CONFIG__的便捷访问器代码外层还有一层try/catch——未捕获的异常会以模块名为前缀写入console.error不会中断页面其他注入。所以不必自己再包try/catch但要意识到错误是静默吞掉的。如果带style.css同时把registry.json里hasCss设为true它会以idext-module-module-id的style标签在你的代码运行之前注入。一个最小 hello-world可对照 modules/hello-world/main.js 与 modules/reading-mode/main.js(function () { var greeting getConfig(greeting, Hello!); var banner document.createElement(div); banner.textContent greeting; banner.style.cssText position:fixed;top:24px;left:50%; transform:translateX(-50%);padding:10px 16px;background:#111; color:#fff;border-radius:12px;z-index:2147483647;; document.body.appendChild(banner); setTimeout(function () { banner.remove(); }, 3000); })();可选注册面板按钮。若模块带交互式 UI向悬浮面板注册入口__WTA_MODULE_UI__.register({ id: __MODULE_INFO__.id, name: __MODULE_INFO__.name, icon: __MODULE_INFO__.icon, uiConfig: __MODULE_UI_CONFIG__, runMode: __MODULE_RUN_MODE__, onClick: function () { // open your UI } });如果跳过这一步而模块又声明了configItems运行时会自动注册一个默认入口保证用户至少能打开模块设置。版本与升级发新版时同时升module.json的version.code/version.name和registry.json的version。客户端用 semver 比较本地已安装版本并提示一键升级——对应 ModuleMarketRepository.kt 中compareSemver(entry.version, local.version.name)的比较逻辑大于 0 即标记为UpdateAvailable。用户配置在更新中保留新清单configItems里仍存在的 key 保留其值被移除的 key 会被自动清理。因此重命名 config key 等于重置它——升版本并在version.changelog中说明。CI 校验什么校验器是 validate_modules.py纯 Python 标准库无需pip install本地自查python3 .github/scripts/ci/validate_modules.py退出码0表示目录合法1表示发现至少一个 errorCI 失败。它由 modules-check.yml 工作流在任何触及modules/**的 PR 与推送到main时自动运行也允许workflow_dispatch手动触发一个绿的 CI 是维护者合并的前置条件。校验覆盖JSON 合法性与必填字段registry 侧id/path/namemanifest 侧id/name/version允许的枚举值category、runAt、permissions、configItems[].typeregistry.json↔module.json一致性id、name、versionregistry 字符串 vs manifestversion.name、runAt必须相等且 registry 的permissions必须覆盖 manifest 声明的权限registry 是列表面允许是超集kebab-case 文件夹名与id无孤儿目录有文件夹无 registry 条目或鬼条目有条目无文件夹无重复id/path必需文件存在module.json、main.jshasCss与style.css的存在一致双向检查声称true但文件缺失、文件存在但false都会报错iconUrl大小256 KB 上限、扩展名白名单、路径遍历拒绝..或绝对路径直接报错相对图标文件必须真实存在未被iconUrl引用的图标文件给 warningmain.js无顶层return在 IIFE 包裹里会变成语法错误校验器按行首零缩进的return启发式检测非空且为 UTF-8超过 512 KB 给 warninggetConfig(...)调用与声明的configItems对应用了getConfig却没声明configItems会收到 warning——用户将静默拿到 undefined 默认值。submissions.json决定谁出现在市场submissions.json由 modules-publish.ymlModule Market Publish工作流在每次触及modules/的main推送时自动生成每条对应一个真正落地main的模块带 PR 编号、合并时间merged_at、PR 作者的 GitHub 身份以及该模块目录下所有其他提交者的 GitHub 身份contributors列表。App 端市场把原始提交者与贡献者渲染为叠加头像并聚合出贡献者榜单。App 端市场只展示出现在submissions.json中的模块——这是只展示已合并 PR的全部机制。由于main分支受保护要求 PR check 状态且默认GITHUB_TOKEN不能直接推送工作流不会直接回推main它把重新生成的文件提交到固定工作分支ci/regenerate-submissions开或复用更新一个 PR然后排队 auto-merge若配置了PAT_TOKEN则等待 check 通过后自动 squash 合并否则留给人工合并。生成逻辑本身generate_submissions.py遍历所有模块目录用git log --diff-filterA找出每个目录的引入提交调GET /repos/{owner}/{repo}/commits/{sha}/pulls判断该提交是否属于某个已合并 PR是则记录 PR 编号、URL、merged_at、PR 作者的 login 头像否则按直推处理记录提交作者解析出的 GitHub login——任何真实作者都会被记录模块只能经评审或维护者推送进入main只有 bot 身份被排除遍历每个模块完整提交历史解析每位作者的 GitHub login把除原始提交者及 bot之外的人记入contributors提交重新生成的文件。作为贡献者你不用做任何事——PR 合并后等几秒工作流跑完即可。审核清单维护者视角以下是 modules/README.md 中定义的合并前检查项提 PR 前可以自行对照目录名和id唯一且为 kebab-casemodule.json和registry.json的id/name/version/runAt/permissions/urlMatches一致main.js可读没有特殊说明的话不接受混淆/压缩代码没有无条件调用第三方网络没有未在permissions中声明就读取document.cookie或鉴权 tokenurlMatches范围合理侵入性强的模块不要无脑写*代码能在 IIFE 包裹下正常执行不要写顶层returnrunAt与代码预期一致DOCUMENT_START不能假设document.body已存在version.name变了的话version.code也要随之 1hasCss当且仅当目录里有style.css时才为true如果设置了iconUrl对应文件存在、不超过 256 KB扩展名是png/svg/webp/jpg/jpeg之一不放未被iconUrl引用的图标文件或其他运行时用不到的冗余文件边界说明浏览器扩展不同社区市场只收录 JS/CSS 模块。MV3 浏览器扩展不是社区目录——浏览器扩展标签页改为实时搜索 Chrome 网上应用店见 Chrome MV3 扩展。registry 中sourceType: CHROME_EXTENSION的条目需带 32 位 Chrome Web Store ID走的是另一条安装路径不参与社区模块目录的常规校验约束。小结发布一个 WebToApp 市场模块的完整链路是创建 kebab-case 目录module.jsonmain.js可选style.css/icon.*→ 在registry.json添加对应条目注意version字符串与path/hasCss两个专属字段→ 本地跑python3 .github/scripts/ci/validate_modules.py自查 → 提 PR 等 CI 与人工审核 → 合并后由 Publish 工作流重新生成submissions.json→ 全量客户端在缓存周期内自动看到新模块。整套机制没有任何私有后端仓库本身即是发布基础设施。【免费下载链接】web-to-appThe most full featured web-to-app toolkit on Android, a complete APK workshop that runs entirely on your phone项目地址: https://gitcode.com/GitHub_Trending/web/web-to-app创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进