Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions skills/scriptcat-userscript/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# scriptcat-userscript

给 AI 编码代理(Claude Code、Codex 等)用的 Skill:指导编写、修改、调试脚本猫用户脚本——页面脚本(`@match`)、
后台脚本(`@background`)、定时脚本(`@crontab`),并在接入 [sctl](https://github.com/scriptscat/sctl) 时
把脚本装进用户的脚本猫、在真实浏览器里验证。

## 结构

| 文件 | 内容 |
|---|---|
| `SKILL.md` | 入口:选脚本类型、骨架、容易写错的规则、安装与验证的分流 |
| `references/api.md` | 脚本猫实现的全部 GM / CAT API(按 grant 分组),以及与 Tampermonkey 的差异 |
| `references/background.md` | 后台/定时脚本:Promise 生命周期、`CATRetryError` 重试、cron 与 `once` 扩展、日志 |
| `references/userconfig.md` | `==UserConfig==` 用户配置格式 |
| `references/patterns.md` | 常用写法:等待元素、SPA 路由切换、带记忆的菜单开关、跨域请求、样式、多语言、带重试的后台运行 |
| `references/dev-loop.md` | 用 sctl 侦察页面 → 安装 → 看控制台 → 重装或 `sctl edit` 迭代;本地文件热重载(`@require file:///` 壳脚本);用 `sctl run` / `sctl logs` 运行后台脚本并读日志,旧版本退回探针脚本 |
| `scripts/check.mjs` | 零依赖静态检查:未声明 grant、`@grant none` 误用、缺 `@match`、cron 写法、`@connect` 缺失等;有 error 时退出码 1 |
| `scripts/stub.mjs` | 生成本地热重载用的 `@require file:///` 壳脚本 |

API 以 `src/types/scriptcat.d.ts` 和 `src/pkg/utils/script_compat.ts`(支持的指令与 grant 白名单)为准;
修改这两处时同步检查本 Skill。`scripts/check.mjs` 内置的 grant 清单由 `check.test.mjs` 对照 `SUPPORTED_GRANTS` 守卫,
依赖注入表(`DEPENDS`)与 `src/app/service/content/gm_api/` 的 `depend` 声明需手工同步。sctl 本身的操作规范在 sctl 仓库的 `skills/sctl`,这里只引用、不重复。

## 安装

```sh
ln -s "$(pwd)/skills/scriptcat-userscript" ~/.claude/skills/scriptcat-userscript
ln -s "$(pwd)/skills/scriptcat-userscript" ~/.codex/skills/scriptcat-userscript
```

## 已知限制

- `sctl run` / `sctl logs` 需要 sctl 与脚本猫都升级到包含 scriptscat/sctl#17 与 scriptscat/scriptcat#1825 的版本。旧 sctl 不认识
`run`,旧脚本猫返回 `METHOD_NOT_FOUND` 并提示升级;Skill 遇到这两种情况会退回 `references/dev-loop.md` 里的间接办法(重装即重跑
`@background`、测试时临时改成每分钟的 cron、用共享 `@storageName` 的探针页面脚本把进度打到控制台)。
- 脚本猫正式版不把 `debug` 级别的 `GM_log` 写进日志,日志页与 `sctl logs` 都看不到。
- 运行状态事件不带运行 id:`sctl run` 等待期间如果用户在编辑器里点运行或 cron 触发,返回的状态可能属于那一次运行。
195 changes: 195 additions & 0 deletions skills/scriptcat-userscript/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
---
name: scriptcat-userscript
description: "Write, fix and debug userscripts for ScriptCat (Tampermonkey-compatible): page scripts that run on matching sites (@match), background scripts that keep running without a page (@background) and scheduled scripts (@crontab); metadata, @grant and the GM_* / GM.* / CAT_* APIs, user config, retries. When the sctl CLI is connected, install the script into the user's ScriptCat and verify it in their real browser. Use whenever the user wants a userscript, 油猴脚本, 后台脚本, 定时脚本, a .user.js file or a ScriptCat script written, changed or debugged. Not for developing the ScriptCat extension itself, and not for managing installed scripts without changing their code (that is the sctl skill)."
---

# Writing ScriptCat userscripts

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.

## 1. Pick the script type

| The script must… | Type | Key metadata |
|---|---|---|
| Change, read or automate a web page | Page 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.

## 2. Start from the right skeleton

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.

## 3. Rules that break scripts when ignored

**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](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](references/userconfig.md).

## 4. API lookup

[references/api.md](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](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.

## 5. Install and verify

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](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.
Loading
Loading