油猴脚本双通道发布与自动化更新机制设计¶
本文系统梳理开源用户脚本(Userscript)在面对高频迭代与版本稳定性双重诉求时,如何设计兼顾尝鲜与可复现性的双发布通道(Rolling Channel 与 Stable Channel);深度剖析各主流脚本管理器(Tampermonkey 与 Violentmonkey)在更新机制上的协议差异与实战避坑,并给出基于 GitHub Actions 的全自动化持续交付与 Tag 驱动发版工程方案。
一、 背景与核心矛盾¶
对于内容汉化、站点功能增强、反跟踪或体验优化类的开源油猴脚本,通常存在两类截然不同的诉求:
- 高频同步诉求(Rolling):
- 依赖上游词库、规则集或站点前端反爬改版的脚本,往往需要借助 GitHub Actions 进行高频增量抓取(如每 6 小时同步一次)。
- 普通用户希望第一时间自动获得最新的翻译词条与适配补丁,越快越好。
- 版本稳定性与可追溯诉求(Stable):
- 团队环境、二次开发者或对稳定性要求极高的重度用户,希望将脚本锁定在某个经过充分验证的里程碑版本(如特定 Release 或 Tag)。
- 一旦上游意外破坏布局或产生 Bug,用户需要能够明确回滚,而不是被后台静默更新强行拉到最新的主干分支。
如果仅维护单一发布通道并将元数据头硬编码指向 main 分支(例如 @updateURL .../main/script.user.js),虽然方便了高频更新,却剥夺了用户锁定版本的权利;更严重的是,若发布设计不当,用户即使从 Release 页面手动下载了某个 Tag 的脚本,脚本管理器也极易在后台自动将其覆盖回 main 分支。
二、 主流脚本管理器的更新判定机制与差异¶
要实现可靠的通道隔离,必须彻底理解客户端脚本管理器(Tampermonkey、Violentmonkey、ScriptCat)的更新逻辑。
1. Tampermonkey 的更新协议¶
根据 Tampermonkey 官方规范,其更新检测流程包含两层判定:
- @version 标签:更新检查的前提。版本号由若干点分段组成(如 1.3.3),管理器基于语义版本算法比对版本高低。若目标源的 @version 不大于当前已安装版本,绝不触发更新。
- @updateURL:定义更新检查清单(Check URL)。Tampermonkey 会定期向该 URL 发起轻量请求(或通过元数据块)提取 @version。
- @downloadURL:定义产物下载地址(Download URL)。当且仅当 @updateURL 处发现更高版本时,管理器才会从 @downloadURL 下载最新脚本正文并执行替换。
- 特殊机制:若 @downloadURL none,则该脚本永久禁用自动更新。
2. Violentmonkey 的更新协议¶
与 Tampermonkey 略有不同,Violentmonkey 的官方文档将 @downloadURL 作为自动检查更新并下载的主要 URL。虽然在新版本中兼容 @updateURL,但其内部实现更倾向于从同一不可变源拉取检查。
3. 跨管理器兼容的致命陷阱:静默拉平漏洞¶
很多开发者在尝试设计“稳定版”时容易犯一个典型错误:
// 错误示例:稳定版试图只修改 downloadURL
// @version 1.3.3
// @downloadURL https://github.com/owner/repo/releases/download/v1.3.3/script.user.js
// @updateURL https://raw.githubusercontent.com/owner/repo/main/script.user.js
@updateURL 依然指向滚动分支 main,一旦 main 分支因为日常提交或词库自动同步版本号递增(例如变成 1.3.4),Tampermonkey 在后台执行轮询时:
1. 请求 @updateURL(main 分支),发现版本号 1.3.4 > 1.3.3;
2. 判定存在新版本,随后根据配置触发更新,直接将用户辛辛苦苦锁定的稳定脚本彻底覆盖为未经测试的最新滚动版!
跨管理器最佳实践结论:
在稳定通道中,
@updateURL与@downloadURL必须同时绑定到不可变源或 Release 专属的永久最新直链,严禁与main分支混用!
三、 双通道架构全景设计¶
┌────────────────────────┐
│ 开发者提交 / 上游同步 │
└───────────┬────────────┘
│
push main │ push tag (v*)
┌─────────────────┴─────────────────┐
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────┐
│ Rolling 通道 (main) │ │ Stable 通道 (Release) │
├─────────────────────────┤ ├─────────────────────────┤
│ @version: <base>.<build>│ │ @version: <base>.<build>│
│ @updateURL: /main/ │ │ @updateURL: /releases/│
│ @downloadURL: /main/ │ │ @downloadURL: /releases/│
└───────────┬─────────────┘ └───────────┬─────────────┘
│ │
▼ ▼
实时跟随主干演进 版本冻结 / 仅跟随正式发版
1. 滚动开发通道(Rolling Track)¶
- 定位:面向绝大多数尝鲜用户,追求第一时间的特性覆盖与上游词库热更新。
- 地址策略:
@downloadURL:https://raw.githubusercontent.com/<owner>/<repo>/main/<name>.user.js@updateURL: 与 downloadURL 保持一致(国内镜像可配对 jsDelivr CDN)。- 更新节奏:每当
main分支合并提交,用户脚本管理器后台按设定周期自动同步。
2. 稳定发布通道(Stable Track)¶
- 定位:面向需要稳定性、离线审计、可回滚的企业与保守用户。
- 地址策略:
- 利用 GitHub Releases 提供的固定永久最新重定向直链:
https://github.com/<owner>/<repo>/releases/latest/download/<name>.user.js - 管理器后台请求该地址时,GitHub 会自动通过 302 重定向跟随到最新一次发布的稳定 Release 资产;
- 只有在仓库正式打出新 Release Tag 时,稳定版用户才会收到版本提升信号;
- 若用户希望永久死锁某一版本,只需手动将
@updateURL改为none。
四、 构建脚本解耦工程实践¶
为了避免维护两套独立源码造成逻辑漂移,推荐在构建脚本(如 build.mjs)中引入通道参数解析(Channel Decoupling)。
1. 构建层 URL 动态路由实现(Node.js / ESM)¶
// build.mjs 核心片段
const REPO_OWNER = 'your-username';
const REPO_NAME = 'your-repo';
const SCRIPT_NAME = 'demo-script';
/**
* 根据 CLI 参数动态决定发布通道 URL
* 默认未传参:rolling 通道(指向 main)
* 传入 --channel=stable 或 --channel stable:stable 通道(指向 releases/latest/download)
*/
export function resolveChannelUrl(args = process.argv.slice(2)) {
const isStable = args.some((arg, idx) =>
arg === '--channel=stable' || (arg === '--channel' && args[idx + 1] === 'stable')
);
if (isStable) {
return {
channel: 'stable',
url: `https://github.com/${REPO_OWNER}/${REPO_NAME}/releases/latest/download/${SCRIPT_NAME}.user.js`
};
}
return {
channel: 'rolling',
url: `https://raw.githubusercontent.com/${REPO_OWNER}/${REPO_NAME}/main/${SCRIPT_NAME}.user.js`
};
}
function main() {
const { channel, url } = resolveChannelUrl();
const version = "1.3.3";
const header = `// ==UserScript==
// @name My Userscript
// @version ${version}
// @downloadURL ${url}
// @updateURL ${url}
// ==/UserScript==\n`;
// 组装并写入单文件产物
const content = header + readSourceCode();
writeFileSync(`${SCRIPT_NAME}.user.js`, content, 'utf8');
console.log(`✅ [${channel}] 产物已构建: ${url}`);
}
五、 GitHub Actions 自动化持续交付流程¶
将通道与 GitHub Actions 触发器绑定,建立无需人工干预的自动化流水线。
1. 滚动通道:主干构建与一致性校验 (ci.yml)¶
在日常 PR 和 Push 时,强制执行:构建 ➔ 语法预检 ➔ 提交物一致性校验:
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
- name: 默认构建并验证语法
run: |
node build.mjs
node --check your-script.user.js
git diff --exit-code -- your-script.user.js # 严防漏提交构建产物
- name: 运行单元测试
run: npm test
2. 稳定通道:Tag 驱动的自动 Release 发布 (release.yml)¶
通过 Git Tag 触发独立工作流,动态构建带 Stable 头部的专属发布资产:
name: Release
on:
push:
tags: ['v*']
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
- name: 构建稳定版产物 (Stable Track)
run: |
node build.mjs --channel=stable
node --check your-script.user.js
- name: 发布 Release 并上传稳定版资产
uses: softprops/action-gh-release@c062e08bd532815e2082a85e87e3ef29c3e6d191 # v2.0.8
with:
generate_release_notes: true
files: |
your-script.user.js
六、 避坑与实战经验铁律¶
- 版本号基线决不可倒退(单调递增铁律):
- 如果一个仓库在过去未曾创建 Git Tag,开发者切忌凭直觉“从 v1.0.0 或 v1.1.0 开始打 Tag”。
- 必须先用代码查验当前实际产物中的
@version。若代码当前已是1.4.4,首个稳定 Tag 必须顺延为v1.4.4。如果打了较低的v1.1.0Tag,脚本管理器在按数值比对时会认为这是“旧版本”,导致安装与更新完全失效。 - 供应链 Actions 全量 40 位 SHA 钉版:
- 严禁在 release 工作流中随意引用
@v2或@master动态标签;使用 40 位 SHA 钉版并附带注释,防止第三方 Action 遭到投毒导致发布的 Userscript 资产被植入恶意脚本。 - 工作区源码防污染:
- 在 CI release 任务中运行
node build.mjs --channel=stable仅生成资产用于上传,不要将该文件反向提交回 Gitmain分支。main分支物理代码库中的@updateURL应当始终保持指向main,保证主干代码的自洽性。