不同脚本管理器对 GM API 的支持差异

2026年10月26日李来群李来群

关于作者: 工具开发工程师,曾就职于高通与联发科,现独立开发 MonkeyMom。

实测 57 个 GM 接口在四家用户脚本管理器上的支持情况与返回值差异——暴力猴、Via、Stay for Safari、Userscripts,以及 MonkeyMom 自己实现来抹平差异的 API。

每个脚本管理器都按自己的理解实现 GM API。名字都一样,于是「在一个管理器上能跑」被默认为「在别的管理器上也能跑」。实际上经常不能,而且失败得很安静:函数存在但没有返回值,回调永远不触发,监听器拿不到 id 所以永远卸不掉。

MonkeyMom 要运行自己没写过的脚本,而且运行在它控制不了的管理器上。在设计子脚本桥之前,得先知道地面到底长什么样,于是我们对 57 个 API 在四家管理器上做了一轮实测。

实测是怎么做的

每项检查记录什么

检查项怎么查失败长什么样
存在性与类型对标识符取 typeofundefined,或者一个什么都不做的空函数
调用结果真的调用一次并捕获返回值Promise、元素、undefined,或者直接抛错
异步回调发起调用,带超时等待回调始终不触发,或者超时才触发
返回值的解析类型检查 resolve 出来的值该给元素却给了 Promise,或者反过来

为什么光看文档不够

文档描述的是意图,脚本看到的才是实现。同一个名字,可以四种行为。

API暴力猴ViaStay for Safari
GM.openInTab带 close() 的句柄undefinedPromise,解析出没有 close() 的标签页对象
GM.addStylePromise → <style><style> 元素选择器字符串
GM_xmlhttpRequest回调式回调式回调式
GM_listValues本脚本的键所有脚本的键本脚本的键

只看文档会得到一个假设和三个意外。

实测数据表

检测覆盖 57 个 API。下面依次给出各管理器的汇总、API 支持矩阵、返回值类型差异和 MonkeyMom 的合成清单,用作后续写脚本时的速查。

汇总

下表汇总各管理器的通过情况,并列出每个通过检测的 API 全名。

管理器通过 / 57通过检测的 API
暴力猴48 / 57GM_setValue
GM_getValue
GM_deleteValue
GM_listValues
GM_setValues
GM_getValues
GM_deleteValues
GM_addValueChangeListener
GM_removeValueChangeListener
GM_addStyle
GM_addElement
GM_openInTab
GM_setClipboard
GM_notification
GM_download
GM_registerMenuCommand
GM_unregisterMenuCommand
GM_xmlhttpRequest
GM_webRequest
GM_cookie.set
GM_cookie.list
GM_cookie.delete
GM_audio.removeStateChangeListener
GM_getResourceText
GM_getResourceURL
GM_info
GM_log
unsafeWindow
GM.info
GM.setValue
GM.getValue
GM.deleteValue
GM.setValues
GM.getValues
GM.deleteValues
GM.listValues
GM.addStyle
GM.addElement
GM.notification
GM.download
GM.registerMenuCommand
GM.unregisterMenuCommand
GM.getResourceUrl
GM.getResourceText
GM.openInTab
GM.closeTab
GM.setClipboard
GM.xmlHttpRequest
Via41 / 57GM_setValue
GM_getValue
GM_deleteValue
GM_listValues
GM_setValues
GM_getValues
GM_deleteValues
GM_addStyle
GM_addElement
GM_openInTab
GM_setClipboard
GM_notification
GM_download
GM_registerMenuCommand
GM_unregisterMenuCommand
GM_xmlhttpRequest
GM_webRequest
GM_audio.removeStateChangeListener
GM_getResourceText
GM_getResourceURL
GM_info
GM_log
unsafeWindow
GM.info
GM.setValue
GM.getValue
GM.deleteValue
GM.setValues
GM.getValues
GM.deleteValues
GM.listValues
GM.addStyle
GM.addElement
GM.notification
GM.registerMenuCommand
GM.unregisterMenuCommand
GM.getResourceUrl
GM.openInTab
GM.closeTab
GM.setClipboard
GM.xmlHttpRequest
Stay for Safari44 / 57GM_setValue
GM_getValue
GM_deleteValue
GM_listValues
GM_addValueChangeListener
GM_removeValueChangeListener
GM_saveTab
GM_getTab
GM_getTabs
GM_addStyle
GM_addElement
GM_openInTab
GM_setClipboard
GM_notification
GM_download
GM_registerMenuCommand
GM_unregisterMenuCommand
GM_xmlhttpRequest
GM_webRequest
GM_cookie.set
GM_cookie.list
GM_cookie.delete
GM_audio.removeStateChangeListener
GM_getResourceText
GM_getResourceURL
GM_info
GM_log
unsafeWindow
GM.info
GM.setValue
GM.getValue
GM.deleteValue
GM.listValues
GM.addStyle
GM.addElement
GM.notification
GM.registerMenuCommand
GM.unregisterMenuCommand
GM.getResourceUrl
GM.openInTab
GM.closeTab
GM.setClipboard
GM.saveTab
GM.xmlHttpRequest
Userscripts for Safari22 / 57GM_setValue
GM_getValue
GM_deleteValue
GM_listValues
GM_saveTab
GM_addStyle
GM_openInTab
GM_setClipboard
GM_xmlhttpRequest
GM_info
GM.info
GM.setValue
GM.getValue
GM.deleteValue
GM.listValues
GM.addStyle
GM.openInTab
GM.closeTab
GM.setClipboard
GM.saveTab
GM.getTab
GM.xmlHttpRequest

API 支持矩阵

API暴力猴ViaStay for SafariUserscripts
GM_setValue✅✅✅✅
GM_getValue✅✅✅✅
GM_deleteValue✅✅✅✅
GM_listValues✅✅✅⚠️ 返回 {}(GM.listValues 正常)
GM_setValues✅✅❌❌
GM_getValues✅✅❌❌
GM_deleteValues✅✅❌❌
GM_addValueChangeListener✅❌✅❌
GM_removeValueChangeListener✅❌✅❌
GM_saveTab❌❌✅✅
GM_getTab❌❌✅⚠️ 超时
GM_getTabs❌❌✅❌
GM_addStyle✅✅✅✅
GM_addElement✅✅✅❌
GM_openInTab✅✅✅✅
GM_setClipboard✅✅✅✅
GM_notification✅✅✅❌
GM_download✅✅✅❌
GM_registerMenuCommand✅✅✅❌
GM_unregisterMenuCommand✅❌✅❌
GM_xmlhttpRequest✅✅✅✅
GM_webRequest⚠️ 空函数⚠️ 空函数⚠️ 空函数❌
GM_cookie.set✅❌✅❌
GM_cookie.list✅❌✅❌
GM_cookie.delete✅❌✅❌
GM_audio.setMute❌❌❌❌
GM_audio.getState❌❌❌❌
GM_audio.addStateChangeListener❌❌❌❌
GM_audio.removeStateChangeListener⚠️ 联合测试⚠️ 联合测试⚠️ 联合测试❌
GM_getResourceText✅✅✅❌
GM_getResourceURL✅✅✅❌
GM_info✅✅✅✅
GM_log✅✅✅❌
unsafeWindow✅✅✅❌
GM.info✅✅✅✅
GM.setValue✅✅✅✅
GM.getValue✅✅✅✅
GM.deleteValue✅✅✅✅
GM.setValues✅✅❌❌
GM.getValues✅✅❌❌
GM.deleteValues✅✅❌❌
GM.listValues✅✅✅✅
GM.addStyle✅✅✅✅
GM.addElement✅✅✅❌
GM.notification✅✅✅❌
GM.download✅❌❌❌
GM.cookie⚠️ 超时❌⚠️ 超时❌
GM.registerMenuCommand✅⚠️ 不能注销✅❌
GM.unregisterMenuCommand✅❌✅❌
GM.getResourceUrl✅✅✅❌
GM.getResourceText✅❌❌❌
GM.openInTab✅✅✅✅
GM.closeTab✅✅✅✅
GM.setClipboard✅✅✅✅
GM.saveTab❌❌✅✅
GM.getTab❌❌⚠️ 返回异常✅
GM.xmlHttpRequest✅✅✅✅

统计:暴力猴 48/57,Stay for Safari 44/57,Via 41/57,Userscripts 22/57。

返回值类型差异

同一函数返回什么,各家并不一致。写跨管理器脚本时这条表比支持矩阵更重要。

API暴力猴ViaStay for Safari
GM.addStylePromise → <style>Promise → <style>Promise → 选择器字符串
GM_addStylePromise → <style><style> 元素<style> 元素
GM.addElementPromise → <div>Promise → <div>直接返回 <div>
GM_addElementPromise → <div><div> 元素<div> 元素
GM.xmlHttpRequestPromise(不要 await)undefined{abort} 对象
GM_xmlhttpRequest{abort} 对象undefined{abort} 对象
GM.openInTab{onclose, closed, close}undefinedPromise → 标签页对象
GM_openInTab{onclose, closed, close}undefinedundefined
GM_download{abort} 对象调用无回调undefined

GM.* 与 GM_ 的对应关系

新脚本优先用 GM.*,同步版本只在兼容旧脚本时才有意义。

GM.* 异步形式GM_ 同步形式
GM.setValueGM_setValue
GM.getValueGM_getValue
GM.deleteValueGM_deleteValue
GM.listValuesGM_listValues
GM.addStyleGM_addStyle
GM.addElementGM_addElement
GM.setClipboardGM_setClipboard
GM.xmlHttpRequestGM_xmlhttpRequest
GM.getResourceUrlGM_getResourceURL
GM.infoGM_info
GM.openInTabGM_openInTab
—GM_download
—GM_getResourceText

MonkeyMom 子脚本桥合成清单

以下 API 不依赖管理器原生实现,由 MonkeyMom 在子脚本环境中自己提供。

APIMonkeyMom 的实现
GM_getResourceText读注入前预取的 @resource 缓存
GM_getResourceURL把缓存文本包成页面侧 blob URL
GM.getResourceUrl缓存命中即时返回,未命中经桥回源下载
GM_registerMenuCommand登记为面板"脚本"页的按钮,点击回调页面侧闭包
GM_unregisterMenuCommand按命令 id 移除按钮
GM_notification页面内 toast,支持 onclick / ondone

明确不做:GM_webRequest、unsafeWindow、GM.closeTab、GM_audio.*、GM.cookie。

管理器之间到底差在哪

差异不是随机的,而是按功能族聚在一起,每一族有各自的坏法。

存储

行为暴力猴ViaStay for SafariUserscripts
基础四项(get / set / delete / list)✅✅✅✅
批量形(setValues / getValues / deleteValues)✅✅❌❌
GM.listValues 返回本脚本的键✅❌ 跨脚本共享✅✅
GM_listValues(经典同步形式)✅✅✅⚠️ 返回 {}

Via 的存储按管理器全局共享:两个脚本都用 state 这个键,就会互相覆盖,除非自己手动加命名空间。Userscripts 的分裂发生在一个管理器内部:异步的 GM.listValues 返回真实键名,而经典的 GM_listValues 返回 {}——这比没有更糟,看起来像支持。

监听器与标签页

行为暴力猴ViaStay for Safari
GM_addValueChangeListener 返回 id✅❌ undefined✅
GM_removeValueChangeListener 认这个 id✅❌✅
GM_saveTab / GM_getTab / GM_getTabs❌❌✅

拿不到 id 的监听器会永远注册着。仅这一条,就足以让桥不去依赖管理器自己的监听器 API。

样式与元素注入

行为暴力猴ViaStay for SafariUserscripts
GM_addStyle✅✅✅✅
GM_addElement✅✅✅❌ 缺失
GM.addStyle 返回Promise → <style>Promise → <style>Promise → 选择器字符串未测
GM_addStyle 返回Promise → <style><style> 元素<style> 元素未测
GM.addElement 返回Promise → <div>Promise → <div>直接返回 <div>未测

Userscripts 干脆没有 GM_addElement。对在严格 CSP 页面上动态加载代码的脚本来说这是致命的:没有管理器帮忙,纯 DOM 路径绕不开 CSP。

网络、下载、剪贴板与标签页

行为暴力猴ViaStay for Safari
GM_xmlhttpRequest✅ 回调式✅ 回调式✅ 回调式
GM.download✅ 返回 {abort}❌ 缺失,回调也不触发✅ 返回 undefined
GM.setClipboard✅✅✅
GM.openInTab 后台打开✅✅✅
GM.openInTab 返回带 close() 的句柄undefinedPromise → 标签页对象

菜单、通知与 Cookie

行为暴力猴ViaStay for Safari
GM_registerMenuCommand✅✅✅
GM_unregisterMenuCommand✅❌✅
GM_notification 真的能弹出来✅✅✅(Edge 需要通知权限)
GM.cookie.set / list / delete✅❌✅
资源缺失时 GM_getResourceText 返回undefinednullundefined

MonkeyMom 自己实现了哪些

桥把子脚本作为注入脚本放进页面,并给它一个合成的 GM 对象。管理器做得可靠的原样透传,缺失、损坏或不一致的部分由 MonkeyMom 自己实现。

API管理器那边的情况MonkeyMom 怎么做
GM_getResourceText / GM_getResourceURL依赖安装时下载 @resource保存时解析元数据,注入前用 GM.xmlHttpRequest 预取(15 秒硬顶),同步读缓存
GM.getResourceUrl同样的分裂,外加 null 与 undefined 之争命中缓存即时返回,未命中经桥回源
GM_registerMenuCommand / GM_unregisterMenuCommand手机端没有菜单可点;Via 不能注销渲染成面板脚本行下方的按钮;返回的 id 能真的注销
GM_notification取决于浏览器通知权限页面内 toast,两种调用形态都收,支持 onclick 和 ondone
GM_getValue / GM_setValue / GM_deleteValue / GM_listValues部分管理器按脚本隔离,Via 全局共享按脚本 id 隔离,同步读缓存、异步落盘
WebKit 上的 GM_addElement插入的元素仍会被页面 CSP 拦复用页面已有的 nonce,样式退回 GM_addStyle

在严格 CSP 下也能注入

在 WebKit 上——Safari、Stay、以及 iOS 上的所有浏览器——扩展插入的元素并不能豁免页面 CSP。桥会读页面自己已有的 nonce,用 MutationObserver 盯着后来才出现的 nonce,并实测内联 script 与 style 会不会被拦。样式退回 GM_addStyle,这条路不受页面 CSP 管;脚本在没有 nonce 可复用时打警告。

明确不支持的

API为什么不做
GM_webRequest子脚本本身就在主世界里运行,不需要拦截层
unsafeWindow主世界里页面的 window 就是 window
GM.closeTab实测的每家管理器上都缺失或损坏
GM_audio.*实测的每家管理器上都没有实现
GM.cookie注入脚本只能碰非 HttpOnly 的 cookie,承诺完整访问等于撒谎

怎么写才能在每家管理器上都活下来

走最窄的那条路

优先用 GM.getValue 这类异步形式而不是同步形式;GM.xmlHttpRequest 一律用回调式,绝不读它的返回值;不需要元素句柄时用 GM_addStyle 而不是 GM.addStyle。既要 GM_addElement 的返回值又要跨管理器的话,先包一层 Promise.resolve() 再 await。

存储键保持命名空间

即便在 MonkeyMom 内部隔离是有保证的,给键加前缀也不花成本,还能让脚本移植到那些脚本间共享存储的管理器时不出事。

导出才是可移植性的验收

子脚本可以导出为标准用户脚本:头部按白名单与兼容清单的并集写 @grant,@resource 声明随脚本一起走。如果导出的文件能在篡改猴、暴力猴或 Stay 上跑起来,说明可移植性的功夫没有白费。