Skip to content

📝 新增 scriptcat-userscript Skill:指导 AI 编码代理编写页面/后台/定时脚本,并通过 sctl 安装与验证 - #1824

Merged
CodFrm merged 7 commits into
mainfrom
feature/userscript-skill
Nov 5, 2026
Merged

CodFrm merged 7 commits into
mainfrom
feature/userscript-skill

Conversation

@CodFrm

@CodFrm CodFrm commented Nov 4, 2026 •

Copy link
Copy Markdown
Member

Checklist / 检查清单

  • Fixes mentioned issues / 修复已提及的问题
  • Code reviewed by human / 代码通过人工检查
  • Changes tested / 已完成测试

N/A — 无关联 issue。扩展运行时代码没有改动;附带的两个 Node 工具脚本有单测,验证方式见下方「验证」。

背景

AI 编码代理(Claude Code、Codex 等)写脚本猫脚本时,常按 Tampermonkey 的习惯来写,在脚本猫特有的地方出错:后台/定时脚本没有页面、Promise 生命周期、CATRetryError、once cron、==UserConfig==、@grant none 的语义等。sctl 已经能把脚本装进用户的真实浏览器,但代理还缺一套「侦察 → 编写 → 安装 → 验证 → 迭代」的固定流程,也缺一个能在安装前发现常见错误的工具。

本次改动

新增 skills/scriptcat-userscript/。这是一个 Agent Skill,不进入扩展构建产物。

  • SKILL.md:入口,包括选脚本类型、脚本骨架(与「新建脚本」模板一致)、按权限/时机/存储/后台/用户配置分组的易错规则,以及有 sctl 和没有 sctl 两种情况下的验证分流。
  • references/api.md:按 src/pkg/utils/script_compat.ts 的 grant 白名单列出已实现的 GM / CAT API,以及与 TM 的差异。
  • references/background.md:Promise 生命周期、CATRetryError 重试、cron 与 once 扩展、日志。
  • references/userconfig.md:==UserConfig== 格式,类型推断按 UserConfigPanel 的实际行为描述。
  • references/dev-loop.md:sctl 安装与验证流程,另有三节:
    • 本地热重载:装一次带 @require file:///… 的壳脚本,之后改本地文件、reload 页面即生效,不必每次审批。
    • 写之前先侦察页面:用 page snapshot / page eval 找选择器、试跑逻辑;用 debug network / debug request --body 找站点接口,改用 GM_xmlhttpRequest。
    • 后台脚本可观测:通过共享 @storageName 的临时探针页面脚本把值打到 console;用 disable/enable 或临时 crontab 触发运行。
  • references/patterns.md:常用写法(等待元素、SPA 路由、菜单开关持久化、跨域请求、样式注入、本地化、后台脚本 Promise + 重试)。
  • scripts/check.mjs:零依赖静态检查。能发现未声明 grant 的 API 调用、@grant none 下使用 GM API 或 unsafeWindow、cron/once 写法错误、多行 @crontab、后台脚本使用标签页 API、请求的域名缺 @connect 等问题;有 error 时以 exit 1 退出。
  • scripts/stub.mjs:从本地脚本生成热重载用的壳脚本。
  • vitest.config.ts:把 skills/**/*.test.mjs 加进 scripts(node 环境)测试项目。

已知限制

建议审查重点

  • SKILL.md 第 3 节和 background.md 中关于 @grant none、once、多行 @crontab、返回值语义的描述(这几处是对照代码修正过的)。
  • check.mjs 的规则:哪些按 error 报、哪些按 warn 报。
  • references/dev-loop.md 中 sctl 命令和退出码是否与 sctl v0.3.0 一致。

验证

基于 base 6c0459b8…head bef426d7,git diff --stat 只涉及 skills/scriptcat-userscript/ 下 11 个文件和 vitest.config.ts(1 行)。

  • npx vitest run skills/scriptcat-userscript:30/30 通过。npx vitest run --project scripts:87/87 通过。
  • npx eslint skills/scriptcat-userscript/scripts vitest.config.ts、npx prettier --check skills/scriptcat-userscript vitest.config.ts:通过。
  • 冒烟:用两个故意写错的脚本跑 check.mjs,上述各类错误都报出来了,exit 1;stub.mjs 输出的头部保留原有元数据并追加 @require file:///…。
  • 事实核对(对照 origin/main 源码,部分用一次性 vitest 探针实测,探针没有提交):
    • @grant none 下 unsafeWindow 为 undefined(exec_script.ts:56 只注入 GM/GM_info)。
    • once 按 lastruntime 门控,运行开始时就写入,失败的运行也算一次(sandbox/runtime.ts:148,200)。
    • 每一行 @crontab 都会被注册(sandbox/runtime.ts:270)。
    • 正文总被包在 async function 里,所以「return not promise」分支不会触发。
    • @grant GM_xmlhttpRequest 不会带出 GM.xmlHttpRequest:grant.ts 只互换前缀,而且区分大小写。
  • sctl 对齐:对照 sctl v0.3.0 的 docs/cli.md 与 skills/sctl(多实例 --scriptcat / SCRIPTCAT_AMBIGUOUS 退出码 3,tabs open --background 返回 {"tabId"},edit --replace/--with)。
  • 相对链接:skills/ 下全部链接和锚点都能解析。
  • 隐私扫描:没有内网地址、个人邮箱或凭据。
  • ad709853 只改了 Markdown:prettier 通过,skills/ 下相对链接都能解析。sctl run / sctl logs 的命令、参数、输出、退出码,以及 debug 级别不入库这一点,都来自 ✨ 外部接入:支持 sctl 触发后台/定时脚本运行并读取脚本运行日志 #1825 / sctl#17 的真实验证(隔离的 daemon 加无头 Chromium)。
  • 未做:
    • 没有在真实浏览器里重跑 dev-loop 和热重载流程。
    • 完整的 tsc --noEmit 没跑:本地 worktree 的依赖缺 fake-indexeddb(migrate.test.ts),与本改动无关;vitest.config.ts 不在 tsconfig 范围内。

审查修复(2026-11-05)

又做了一轮独立审查,并对照代码订正:

  • bef426d7:
    • 脚本列表只显示 运行中 / 错误 / 已停止 和下次运行时间;运行输出和错误要看“日志”页或 sctl logs。
    • @connect:脚本只要声明了任何 @connect,未列出的域名就会被拒绝,只有完全没写 @connect 时才会询问用户。
    • @grant none 下没有 CATRetryError。
    • 同名脚本在页面脚本和后台脚本之间切换类型会被拒绝。
    • 不写 @grant 也运行在沙盒里。
    • onurlchange 依赖 Navigation API。
    • GM_addElement 在 Firefox 下的 CSP 例外,与 api.md 保持一致。
    • sctl run 的输出文案和 run-in 报错;壳脚本示例;后台壳脚本用 sctl run 迭代(重新读取本地文件这一点来自代码,未在浏览器里验证)。
  • 4ead49a5 check.mjs:
    • 修复对象键误报和关键字后正则字面量的漏报。
    • @match 通配时,不再把请求当成同域。
    • 某个文件读取失败后,继续检查其余文件。
    • 起子进程的命令行用例沿用 scripts/check-i18n.test.mjs 的 15 秒超时。

验证(head bef426d7):Skill 测试 35/35 通过,eslint 和 prettier 通过,相对链接和锚点检查通过;两处误报/漏报都用探针复现过,修复后结果正确。

- SKILL.md:选脚本类型、骨架、按权限/时机/存储/后台分组的易错规则,有无 sctl 两条验证路径
- references/api.md:按 script_compat.ts 的 grant 白名单列出已实现的 GM/CAT API 与和 TM 的差异
- references/background.md:Promise 生命周期、CATRetryError 重试、cron 与 once 扩展、日志
- references/userconfig.md:==UserConfig== 格式(类型推断按 UserConfigPanel 实际行为)
- references/dev-loop.md:sctl install → 后台标签页 reload → debug console / page eval 验证 → 重装或 sctl edit 迭代;
  已在真实浏览器实测:用户脚本 console 可见、整文件 edit 生效、同 name+namespace 重装原地更新(uuid 不变)
- @grant none 下只有 GM_info/GM.info,没有 unsafeWindow(exec_script.ts 只注入 { GM, GM_info },unsafeWindow 仅在沙盒上下文 create_context.ts 设置)
- cron 的 once 以「开始运行」计周期,失败的运行也占用本周期(lastruntime 在运行开始与每次状态更新时写入),失败重跑须用 CATRetryError
- GM_foo 与 GM.foo 互通仅在名字完全一致时成立:@grant GM_xmlhttpRequest 不会暴露 GM.xmlHttpRequest(getGrantCandidates 只换前缀、区分大小写)
- 每一行 @crontab 都会在运行时注册,安装校验和脚本列表只看第一行,任一行非法会停掉全部任务,因此改为「只写一行」
- 脚本体被包在 async 函数里,返回值总是 Promise;不 return Promise 时同步部分结束即记为完成,不会出现 "return not promise" 警告
- 解析 cron 的是 npm 包 cron(非 node-cron);UserConfig 的 min 不作用于 text;sctl grep 在多实例时同样报 SCRIPTCAT_AMBIGUOUS;page reload 已等待 load
- dev-loop.md:写之前用 sctl 侦察页面(snapshot/eval 试选择器与逻辑,debug network/request 找站点接口,鉴权信息不外泄)
- dev-loop.md:用 @require file:/// 壳脚本迭代本地文件,审批一次即可(每次页面加载重读文件,需「允许访问文件网址」)
- dev-loop.md:后台/定时脚本的可观测办法(重装即重跑 @background、测试时临时每分钟 cron、共享 @storageName 的探针页面脚本)
- 新增 references/patterns.md:等待元素、SPA 路由切换、带记忆的菜单开关、跨域请求、样式、多语言、带重试的后台运行
- SKILL.md 加入口链接,README 结构表与已知限制同步
- scripts/check.mjs:零依赖静态检查,按运行时规则报告未声明 grant 的 API 调用(含 depend 一并注入的 API)、@grant none 下用 GM API/unsafeWindow、none 与其他 grant 混写、页面脚本缺 @match/@include、后台脚本用 GM_getTab 系列、cron 字段/once/空格写法错误;警告未知 grant、后台脚本无顶层 return、多行 @crontab、@run-at/@early-start 不生效、请求域名不在 @connect 也不在 @match 下;有 error 时退出码 1
- scripts/stub.mjs:从本地脚本生成 @require file:/// 壳脚本(保留头部与 UserConfig),配合本地热重载
- check.test.mjs 对照 SUPPORTED_GRANTS 守卫内置 grant 清单;vitest 的 scripts 项目纳入 skills/**/*.test.mjs
- SKILL.md、dev-loop.md、README 补充用法
…间接办法

- dev-loop.md:新增 sctl run / sctl logs 用法与注意事项(只限后台/定时脚本、exit 0 看状态、不经审批但有真实副作用、
  手动运行不排重试、debug 级 GM_log 不入库、与编辑器运行或 cron 交叠可能误认),旧 sctl / 旧脚本猫的识别与回退
- background.md:删去无代码依据的「编辑器运行不同步值、菜单不触发」,改为与 sctl run 同一通路
- SKILL.md、api.md:GM_log 的 debug 级别在正式版不入库
- README 已知限制:依赖 scriptscat/sctl#17 与 #1825 的版本
- 对象字面量的键(`{ GM_getValue: 1 }`)不再算 API 调用;三元表达式里的仍算
- return/typeof/case/throw/in/of/void/delete/new 等关键字后的 `/` 按正则开头处理,正则里的引号不再吞掉同一行后面的调用
- @match 带通配子域或有多个域名时,请求其中一个域名不再视为同域(运行时只放行与页面完全相同的域名,见 getConnectMatched)
- connect-missing 文案区分:脚本已有 @connect 时 GM_xmlhttpRequest 会被拒绝,没有任何 @connect 时询问用户;仍为 warn,因为检查只看 URL 字面量,无法确定它是否真被请求
- 某个文件读不了时继续检查其余文件,最后以退出码 2 结束
- check/stub 命令行用例真实 spawn node,按 scripts/check-i18n.test.mjs 的做法单独设超时,避免满载时超出 340ms 单测预算
- 脚本列表只显示运行状态(运行中/错误/已停止)与定时脚本下次运行时间,GM_log 与错误信息改指向日志页(按 name/uuid 过滤)或 sctl logs
- @connect:脚本已有 @connect 时未列入的域名被 GM_xmlhttpRequest 直接拒绝(之前允许过的除外),只有完全没有 @connect 才询问;native GM_download 总是询问;仅与页面完全相同的域名免声明
- CATRetryError 在 @grant none 下不存在;没有任何 @grant 行的脚本同样运行在沙盒里
- 同 @name/@namespace 不能在页面脚本与后台/定时脚本之间切换(类型不符被拒),临时页面脚本需另起名字
- GM_addElement 注入 <script> 在 Firefox 仍受页面 CSP 约束;urlchange 依赖 Navigation API,onurlchange === null 只说明授权生效
- dev-loop:补 get/run/logs 的 --scriptcat 行为、sctl run 的实际状态行与 JSON 字段(含 truncated)、@run-in 不符返回 INVALID_REQUEST;壳脚本示例的说明移到注释行;后台壳脚本可用 sctl run / 编辑器运行免审批重读本地 @require
@CodFrm
CodFrm merged commit 4c3c1f1 into main Nov 5, 2026
11 checks passed
@CodFrm
CodFrm deleted the feature/userscript-skill branch November 5, 2026 06:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant