scriptcat-userscript

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Writing ScriptCat userscripts

编写 ScriptCat 用户脚本

ScriptCat runs Tampermonkey-style userscripts and adds two kinds of its own: background scripts and scheduled scripts, which run in the extension's background sandbox instead of a web page. Most Tampermonkey knowledge carries over; the differences below are the ones that break scripts.
Authority. The API surface is
scriptcat.d.ts
— in this repository
src/types/scriptcat.d.ts
, otherwise https://raw.githubusercontent.com/scriptscat/scriptcat/main/src/types/scriptcat.d.ts. Working examples live in https://github.com/scriptscat/scriptcat/tree/main/example. The developer docs at https://docs.scriptcat.org/docs/dev/ can lag behind the types; when they disagree, trust the types.
ScriptCat 可以运行 Tampermonkey 风格的用户脚本,并新增两类自身特有的脚本:后台脚本和定时脚本。这两类脚本在扩展的后台沙箱中运行,而不是在网页中运行。大多数 Tampermonkey 知识都适用;以下差异才是会导致脚本失效的地方。
权威依据。 API 接口以
scriptcat.d.ts
为准——在本仓库中位于
src/types/scriptcat.d.ts
,否则位于 https://raw.githubusercontent.com/scriptscat/scriptcat/main/src/types/scriptcat.d.ts。可运行的示例位于 https://github.com/scriptscat/scriptcat/tree/main/example。开发者文档 https://docs.scriptcat.org/docs/dev/ 可能落后于类型定义;二者发生冲突时,以类型定义为准。

1. Pick the script type

1. 选择脚本类型

The script must…TypeKey metadata
Change, read or automate a web pagePage script
@match
/
@include
,
@run-at
Keep running while the browser is open, with no page (listeners, polling, a menu in the popup)Background script
@background
Run on a schedule (check-in, sync, report, periodic fetch)Scheduled script
@crontab <expr>
Background and scheduled scripts have no web page: no
document
of any site, no
unsafeWindow
page, no
GM_getTab
/
GM_saveTab
. To touch a page from the background, open it with
GM_openInTab
and let a page script do the DOM work, or fetch data with
GM_xmlhttpRequest
instead of scraping a rendered page.
脚本必须……类型关键元数据
更改、读取或自动化处理网页页面脚本
@match
/
@include
、
@run-at
浏览器打开期间持续运行,即使没有页面(监听器、轮询、弹出窗口中的菜单)后台脚本
@background
按计划运行(签到、同步、报告、定期请求)定时脚本
@crontab <expr>
后台脚本和定时脚本没有网页:无法访问任何站点的
document
,也没有页面的
unsafeWindow
,并且不能使用
GM_getTab
/
GM_saveTab
。如需从后台操作页面,请使用
GM_openInTab
打开页面,再让页面脚本完成 DOM 操作;或者使用
GM_xmlhttpRequest
获取数据,而不是抓取已渲染的页面。

2. Start from the right skeleton

2. 从正确的骨架开始

These match the templates ScriptCat's own "new script" uses.
js
// ==UserScript==
// @name         Example page script
// @namespace    https://example.com/your-namespace
// @version      0.1.0
// @description  What it does, in one line
// @author       You
// @match        https://example.com/*
// @grant        none
// @noframes
// ==/UserScript==

(function () {
  "use strict";
  // ...
})();
js
// ==UserScript==
// @name         Example background script
// @namespace    https://example.com/your-namespace
// @version      0.1.0
// @description  What it does, in one line
// @author       You
// @background
// @grant        GM_xmlhttpRequest
// @grant        GM_notification
// ==/UserScript==

return new Promise((resolve, reject) => {
  // ... resolve() when the work is really finished, reject(err) on failure
});
A scheduled script is the background skeleton with
@crontab * * once * *
(or another expression) in place of
@background
. Background and scheduled bodies are wrapped in a function by ScriptCat, so a top-level
return
is legal there.
这些骨架与 ScriptCat 自身“新建脚本”所使用的模板一致。
js
// ==UserScript==
// @name         Example page script
// @namespace    https://example.com/your-namespace
// @version      0.1.0
// @description  What it does, in one line
// @author       You
// @match        https://example.com/*
// @grant        none
// @noframes
// ==/UserScript==

(function () {
  "use strict";
  // ...
})();
js
// ==UserScript==
// @name         Example background script
// @namespace    https://example.com/your-namespace
// @version      0.1.0
// @description  What it does, in one line
// @author       You
// @background
// @grant        GM_xmlhttpRequest
// @grant        GM_notification
// ==/UserScript==

return new Promise((resolve, reject) => {
  // ... resolve() when the work is really finished, reject(err) on failure
});
定时脚本使用后台脚本骨架,但以
@crontab * * once * *
(或其他表达式)代替
@background
。ScriptCat 会将后台脚本和定时脚本的主体包装在一个函数中,因此这些脚本的顶层
return
是合法的。

3. Rules that break scripts when ignored

3. 忽略后会导致脚本出问题的规则

Identity and updates
  • @name
    +
    @namespace
    identify a script. Keep both stable once the user has installed it; changing either makes a different script. Bump
    @version
    on every change you hand back.
  • A page script cannot become a background or scheduled script (or the reverse) under the same
    @name
    +
    @namespace
    : ScriptCat rejects that install with a script type mismatch. Give a temporary script of the other type its own
    @name
    .
Permissions
  • Every API needs a
    @grant
    line (
    @grant GM_xmlhttpRequest
    , …). An ungranted API is simply undefined at runtime. Granting
    GM_foo
    also exposes
    GM.foo
    and vice versa — only when the names match exactly:
    GM.xmlHttpRequest
    needs its own
    @grant GM.xmlHttpRequest
    (
    @grant GM_xmlhttpRequest
    does not expose it).
  • GM_info
    needs no grant.
    unsafeWindow
    needs no grant either, but only exists in the sandbox (any grant other than
    none
    , or no
    @grant
    line at all);
    @grant unsafeWindow
    is harmless.
  • @grant none
    runs the script without the sandbox:
    window
    is the page's own window, and the only API is
    GM_info
    /
    GM.info
    — no other GM API and no
    unsafeWindow
    (referencing it throws a
    ReferenceError
    ). Use it only for scripts that need nothing but the page.
  • With any other grant — and also with no
    @grant
    line at all, unlike Tampermonkey — the script is sandboxed: its
    window
    is not the page's. Reach page globals (a site's own JS objects, variables it defines) through
    unsafeWindow
    .
  • Declare every host you request with
    @connect <host>
    (it also covers the host's subdomains;
    @connect self
    covers subdomains of the page's host; requests to exactly the page's host need nothing). For a host that is not covered,
    GM_xmlhttpRequest
    is refused ("This domain is not a part of the @connect list") as soon as the script has any
    @connect
    line, unless the user allowed that host before; only a script with no
    @connect
    at all asks the user instead.
    GM_download
    in native mode always asks, and
    GM_cookie
    throws for a host outside
    @connect
    .
Page timing and SPAs
  • @run-at
    :
    document-start
    (as early as possible),
    document-end
    (DOM parsed),
    document-idle
    (the default; everything loaded),
    document-body
    (wait until
    <body>
    exists),
    context-menu
    (run when the user picks the script from the page's right-click menu). Only the first value counts, and an unknown value silently falls back to the default.
  • Add
    @early-start
    together with
    document-start
    when the script must beat the page's own scripts, and wait for
    CAT_scriptLoaded()
    before relying on the full API.
  • Single-page apps change the URL without reloading, so a page script runs once and then misses later views. Listen with
    @grant window.onurlchange
    and
    window.addEventListener("urlchange", e => …)
    , and use a
    MutationObserver
    for content that renders late. Never assume an element exists at injection time — wait for it.
  • @noframes
    keeps the script out of iframes; without it, a
    *://*/*
    match also runs inside every iframe.
  • @inject-into content
    runs in the content-script world: unaffected by the page's CSP, but
    unsafeWindow
    is then not the page's
    window
    . The default is
    page
    .
  • GM_addStyle
    and
    GM_addElement
    bypass the page's CSP; prefer them over creating
    <style>
    /
    <script>
    by hand. Exception: on Firefox a
    <script>
    injected with
    GM_addElement
    is still subject to the page's CSP.
Storage
  • GM_setValue
    stores JSON-like data only (no class instances). Setting
    undefined
    deletes the key (Tampermonkey stores it instead).
  • Writes are asynchronous. Before
    window.close()
    or the end of a background run,
    await GM.setValue(...)
    /
    await GM.setValues(...)
    so the write is not lost.
  • @storageName <name>
    lets several scripts share one value store;
    GM_addValueChangeListener
    sees writes from the other scripts (and from other tabs) — a simple cross-script channel.
Background and scheduled scripts
  • Return a
    Promise
    and settle it after the last asynchronous step. Once it resolves or rejects, ScriptCat treats the run as finished and later GM calls from that run are not guaranteed to work. ScriptCat wraps the body in an
    async
    function, so a body that returns nothing is marked complete as soon as its synchronous part (and any top-level
    await
    ) finishes — pending callbacks and timers do not keep the run open.
  • resolve(value)
    is logged as success,
    reject(err)
    as an error; the script list then shows the run status as error, and the message is in the log.
  • To retry,
    reject(new CATRetryError("reason", seconds))
    (or a
    Date
    ).
    CATRetryError
    is undefined under
    @grant none
    ; it exists with any other grant or with no
    @grant
    line. The retry queue is checked every 5 seconds, so a retry fires up to 5 seconds late; keep run time plus retry delay shorter than the cron interval so runs do not overlap.
  • Write exactly one
    @crontab
    line. Every line is scheduled at runtime, but installation validates and the script list shows only the first, and one invalid extra line stops all of the script's jobs. Use 5 fields (
    min hour day month weekday
    ); a 6-field form with seconds exists but is not recommended. The
    once
    extension limits a field's period to one run, counted when the run starts — a failed run uses up the period too, so retry failures with
    CATRetryError
    :
    * * once * *
    = at most once a day,
    * once * * *
    = at most once an hour,
    * once(9-17) * * *
    = once in each hour from 9 to 17. Full cron and
    once
    rules: references/background.md.
  • Log with
    GM_log(message, "debug" | "info" | "warn" | "error")
    : it lands in ScriptCat's log, the only place a background script's output shows up — ScriptCat's Logs page (filter by the script's
    name
    or
    uuid
    ), or
    sctl logs
    . The script list shows only the run status (running / error / stopped) and a scheduled script's next run time. Release builds do not store
    debug
    , so log anything someone must read at
    info
    or above.
User configuration
  • A
    /* ==UserConfig== … ==/UserConfig== */
    YAML block after the header gives the script a settings form in ScriptCat. Read a value with
    GM_getValue("group.key")
    . Format: references/userconfig.md.
身份标识与更新
  • @name
    和
    @namespace
    共同标识一个脚本。用户安装后应保持两者不变;修改其中任何一个都会被识别为另一个脚本。每次交回修改后的脚本时,都应递增
    @version
    。
  • 在
    @name
    和
    @namespace
    保持不变的情况下,页面脚本不能变为后台脚本或定时脚本,反之亦然;ScriptCat 会因脚本类型不匹配而拒绝安装。另一类型的临时脚本应使用独立的
    @name
    。
权限
  • 每个 API 都需要一行
    @grant
    (
    @grant GM_xmlhttpRequest
    等)。未授权的 API 在运行时将直接是
    undefined
    。授权
    GM_foo
    也会公开
    GM.foo
    ,反之亦然——但仅限名称完全一致的情况:
    GM.xmlHttpRequest
    需要自己的
    @grant GM.xmlHttpRequest
    (
    @grant GM_xmlhttpRequest
    不会公开它)。
  • GM_info
    无需授权。
    unsafeWindow
    也无需授权,但它仅存在于沙箱中(使用除
    none
    以外的任何 grant,或者完全不写
    @grant
    行);
    @grant unsafeWindow
    不会造成影响。
  • 使用
    @grant none
    时,脚本在无沙箱环境中运行:
    window
    是页面自身的
    window
    ,唯一可用的 API 是
    GM_info
    /
    GM.info
    ——其他任何 GM API 都不可用,而且没有
    unsafeWindow
    (引用它会抛出
    ReferenceError
    )。仅将其用于完全不需要页面之外能力的脚本。
  • 使用任何其他 grant 时——与 Tampermonkey 不同,完全不写
    @grant
    行时也会进入沙箱——脚本的
    window
    并不是页面的
    window
    。应通过
    unsafeWindow
    访问页面全局对象(网站自身定义的 JS 对象和变量)。
  • 使用
    @connect <host>
    声明每一个要请求的主机(它也涵盖该主机的子域;
    @connect self
    涵盖页面主机的子域;请求与页面主机完全相同的主机则无需声明)。对于未被涵盖的主机,只要脚本存在任何
    @connect
    行,
    GM_xmlhttpRequest
    就会立即被拒绝(“This domain is not a part of the @connect list”),除非用户此前已允许该主机;只有完全没有
    @connect
    行的脚本才会改为询问用户。原生模式下的
    GM_download
    始终会询问用户,而
    GM_cookie
    对
    @connect
    范围之外的主机会抛出异常。
页面执行时机与 SPA
  • @run-at
    :
    document-start
    (尽早执行)、
    document-end
    (DOM 已解析)、
    document-idle
    (默认;所有内容均已加载)、
    document-body
    (等待
    <body>
    存在)、
    context-menu
    (当用户从页面右键菜单中选择该脚本时运行)。仅第一个值生效,未知值会静默回退到默认值。
  • 当脚本必须早于页面自身的脚本运行时,请在
    document-start
    的同时添加
    @early-start
    ,并在依赖完整 API 前等待
    CAT_scriptLoaded()
    。
  • 单页应用会在不重新加载页面的情况下更改 URL,因此页面脚本运行一次后就会错过后续视图。请使用
    @grant window.onurlchange
    和
    window.addEventListener("urlchange", e => …)
    监听变化,并使用
    MutationObserver
    处理延迟渲染的内容。切勿假定元素在注入时已经存在——应等待它出现。
  • @noframes
    可防止脚本在 iframe 中运行;如果没有它,
    *://*/*
    匹配规则还会使脚本在每个 iframe 中运行。
  • @inject-into content
    会在 content-script world 中运行:不受页面 CSP 影响,但此时
    unsafeWindow
    并不是页面的
    window
    。默认值为
    page
    。
  • GM_addStyle
    和
    GM_addElement
    可以绕过页面的 CSP;应优先使用它们,而不是手动创建
    <style>
    /
    <script>
    。例外情况:在 Firefox 上,通过
    GM_addElement
    注入的
    <script>
    仍然受页面 CSP 约束。
存储
  • GM_setValue
    只能存储类 JSON 数据(不能存储类实例)。设置
    undefined
    会删除对应键(Tampermonkey 则会存储该值)。
  • 写入操作是异步的。在调用
    window.close()
    或后台运行结束之前,应执行
    await GM.setValue(...)
    /
    await GM.setValues(...)
    ,以免写入内容丢失。
  • @storageName <name>
    可让多个脚本共享同一个值存储;
    GM_addValueChangeListener
    能监听其他脚本(以及其他标签页)的写入——这是一个简单的跨脚本通信通道。
后台脚本和定时脚本
  • 返回一个
    Promise
    ,并确保它在最后一个异步步骤完成后才结束。Promise 一旦 resolve 或 reject,ScriptCat 就会将本次运行视为完成,此后该次运行中的 GM 调用不再保证可用。ScriptCat 会将主体包装在一个
    async
    函数中,因此没有返回值的脚本会在其同步部分(以及任何顶层
    await
    )结束时立即被标记为完成——待处理的回调和计时器不会让运行保持开启。
  • resolve(value)
    会记录为成功,
    reject(err)
    会记录为错误;随后脚本列表会将运行状态显示为错误,错误消息则出现在日志中。
  • 如需重试,请使用
    reject(new CATRetryError("reason", seconds))
    (或传入一个
    Date
    )。在
    @grant none
    下,
    CATRetryError
    是 undefined;使用任何其他 grant 或完全不写
    @grant
    行时,它才存在。重试队列每 5 秒检查一次,因此重试最多可能延迟 5 秒;应确保运行时长加重试延迟短于 cron 间隔,以免各次运行发生重叠。
  • 必须且只能写一行
    @crontab
    。虽然每一行都会在运行时被调度,但安装时会进行校验,脚本列表中也只显示第一行;多出的任何无效行都会停止该脚本的所有任务。使用 5 个字段(
    min hour day month weekday
    );也存在带秒的 6 字段形式,但不推荐。
    once
    扩展会将某个字段的周期限制为仅运行一次,并以运行开始时计数——运行失败也会占用该周期,因此应使用
    CATRetryError
    重试失败的运行:
    * * once * *
    = 每天最多运行一次,
    * once * * *
    = 每小时最多运行一次,
    * once(9-17) * * *
    = 每天 9 点至 17 点期间每小时运行一次。完整的 cron 和
    once
    规则请参阅 references/background.md。
  • 使用
    GM_log(message, "debug" | "info" | "warn" | "error")
    记录日志:日志会进入 ScriptCat 的日志系统,这是后台脚本输出唯一可见的位置——可在 ScriptCat 的“日志”页面(按脚本的
    name
    或
    uuid
    过滤),或使用
    sctl logs
    查看。脚本列表只显示运行状态(运行中 / 错误 / 已停止)以及定时脚本的下次运行时间。发布版本不会存储
    debug
    级别,因此需要用户阅读的内容应以
    info
    或更高级别记录。
用户配置
  • 在脚本头之后添加
    /* ==UserConfig== … ==/UserConfig== */
    YAML 块,可为脚本提供 ScriptCat 设置表单。使用
    GM_getValue("group.key")
    读取值。格式请参阅 references/userconfig.md。

4. API lookup

4. API 查询

references/api.md lists every GM and CAT API ScriptCat implements, with the grant name, the cases where ScriptCat differs from Tampermonkey, and which APIs are unavailable in the background. Read it before using an API you are unsure about, then check the exact signature in
scriptcat.d.ts
.
references/patterns.md has ready-made building blocks: waiting for an element, single-page-app route changes, a persistent menu switch, cross-origin requests, styles, localisation, and a background run with retry.
references/api.md 列出了 ScriptCat 实现的每个 GM 和 CAT API,包括对应的 grant 名称、ScriptCat 与 Tampermonkey 存在差异的场景,以及在后台脚本中不可用的 API。使用不确定的 API 前应先阅读该文档,然后检查
scriptcat.d.ts
中的准确签名。
references/patterns.md 提供了现成的构建模块:等待元素、单页应用路由变更、持久菜单开关、跨源请求、样式、本地化,以及带重试的后台运行。

5. Install and verify

5. 安装与验证

Writing the file is half the job; the other half is seeing it run.
First run the static check bundled with this skill (Node 18+, no dependencies; paths are relative to this skill's directory):
sh
node scripts/check.mjs my-script.user.js   # exit 1 = errors that will break the script at runtime
It reports
error|warn <rule> <file>:<line> <message>
: APIs used without their
@grant
, GM APIs or
unsafeWindow
under
@grant none
, a page script without
@match
, invalid
@crontab
expressions, request hosts missing from
@connect
, and similar. Fix every error; read each warning and fix it or say why it does not apply. It is a static check — it cannot tell whether the script does what the user asked.
  • sctl is available (
    sctl status
    shows ScriptCat connected): install it into the user's ScriptCat and verify it in their browser, following references/dev-loop.md. It also covers scouting the target page before you write code, iterating on a local file without an approval per change, and running background and scheduled scripts and reading their logs (
    sctl run
    /
    sctl logs
    ). That loop runs inside the user's real browser, so the sctl skill's rules apply too — read it first if it is installed.
  • No sctl: save the code as
    <name>.user.js
    and tell the user to install it — drag the file onto the ScriptCat dashboard, or create a new script in ScriptCat and paste the code. Say which page to open (page script) or where to look (background: the run status in the script list, and the script's output on ScriptCat's Logs page).
Whichever path you take, report what you actually observed: "installed and the button appears on example.com" is a result; "should work" is not.
编写文件只是完成了一半;另一半是确认它实际运行。
首先运行此 skill 附带的静态检查(需要 Node 18+,无依赖;路径相对于此 skill 的目录):
sh
node scripts/check.mjs my-script.user.js   # exit 1 = errors that will break the script at runtime
它会报告
error|warn <rule> <file>:<line> <message>
:未声明
@grant
就使用 API、在
@grant none
下使用 GM API 或
unsafeWindow
、页面脚本缺少
@match
、无效的
@crontab
表达式、请求主机未列入
@connect
等。必须修复每个 error;逐一阅读每个 warning,并修复它或说明为何不适用。这只是静态检查——无法判断脚本是否实现了用户要求的功能。
  • sctl 可用(
    sctl status
    显示 ScriptCat 已连接):按照 references/dev-loop.md 将脚本安装到用户的 ScriptCat,并在其浏览器中验证。该流程还涵盖在编写代码前勘察目标页面、无需每次修改都获得批准即可迭代本地文件,以及运行后台脚本和定时脚本并读取其日志(
    sctl run
    /
    sctl logs
    )。该流程会在用户的真实浏览器中运行,因此 sctl skill 的规则也适用——如果已安装,请先阅读它。
  • 没有 sctl:将代码保存为
    <name>.user.js
    ,并告知用户如何安装——把文件拖到 ScriptCat 仪表板上,或在 ScriptCat 中新建脚本并粘贴代码。同时说明应打开哪个页面(页面脚本),或者去哪里查看结果(后台脚本:脚本列表中的运行状态,以及 ScriptCat“日志”页面中的脚本输出)。
无论采用哪种路径,都应报告实际观察到的结果:“已安装,且按钮出现在 example.com 上”属于验证结果;“应该可以运行”则不属于。