Skip to content

✨ 外部接入:支持 sctl 触发后台/定时脚本运行并读取脚本运行日志 - #1825

Merged
CodFrm merged 6 commits into
mainfrom
feature/script-run-logs
Nov 5, 2026
Merged

CodFrm merged 6 commits into
mainfrom
feature/script-run-logs

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。测试与真实验证见下方「验证」;尚未经过人工审查。

背景

sctl 无法触发后台/定时脚本运行,也读不到脚本的运行日志:这两类脚本跑在 offscreen/sandbox 里,不是网页,sctl 也不能 attach 扩展页面。所以 AI 代理写完这类脚本后没法自己验证(见 #1824 的 scriptcat-userscript Skill)。sctl 侧新增了 sctl run / sctl logs 和 MCP scripts_run / scripts_logs(配对 PR:scriptscat/sctl#17)。本 PR 让脚本猫的外部接入 bridge 应答对应的两个新 JSON-RPC 方法。

Spec(在 sctl 仓库):docs/specs/2026-11-04-script-run-logs.md。

本次改动

  • scripts.run:复用脚本列表/编辑器“运行”的同一通路(RuntimeService.runScript,execOnce),只允许后台和定时脚本(启用与否不影响)。
    • 默认等到本次运行结束,返回 complete / error / running(超时,默认 60 秒,上限 240 秒)和本次产生的日志。
    • wait:false 时立即返回。
  • scripts.logs:按 uuid 读取 LoggerDAO 日志,支持 since(默认 1 小时)、severity、text、limit(默认 100,上限 1000)。
    • 按时间从早到晚返回,超出 limit 时保留最新的并标 truncated;按帧大小上限截断。
    • 排除外部接入审计记录:审计记录也带 uuid,而且每次 logs 调用都会新增一条。
  • 两个方法都不经审批,由维护者决定(理由见 spec 决策 1,sctl threat-model.md 已记录残余风险),并照常写入外部接入审计(不含日志正文)。
  • external_access/generated/ 由 sctl make protocol-sync-scriptcat 生成。schemaVersion 保持 1.0.0:握手要求版本完全相等,升版本会让旧版连不上。

实现考虑

  • scriptRunStatus 没有运行 id。重跑时会先发旧一次运行的 COMPLETE,再发新一次的 RUNNING。所以必须先看到本轮的 RUNNING,才认后面的终态。
  • 同一脚本的多次外部运行串行触发:等上一次外部运行开始、结束、超时或被释放之后,才触发下一次,避免互相误认状态。
  • 错误文本截断到 16K 字符(UTF-16 码元),并计入帧大小预算。否则超大的 reject 消息会让应答超过 4 MiB 帧上限,daemon 会断开连接。

已知限制

  • 外部运行和编辑器“运行”或 cron 触发交叠时,仍可能把对方的状态当成自己的。根治需要 sandbox 发出运行 id,代码中已标注。
  • 沙盒的 “exec script complete” 日志可能晚于终态写入,因此 run 返回的本次日志里可能缺这一行。
  • 生产构建的日志级别是 info,所以 GM_log(..., "debug") 不会入库,run/logs 看不到它。这是原有行为,本 PR 没有改。

建议审查重点

  • external_access/run_logs.ts 中等待本轮 RUNNING → 终态的状态机,以及每条退出路径上的订阅和定时器释放。
  • 不经审批这一取舍是否接受(权限面与 sctl 已有的页面自动化、debug console 同级)。

关联

验证

base 6c0459b8…head bdc914d1。改动只在 src/app/service/service_worker/external_access/(含 generated/)和 service_worker/index.ts,共 7 个文件。

  • pnpm exec vitest run --no-coverage src/app/service/service_worker/external_access src/app/service/service_worker/index.test.ts:9 个文件、187 个用例通过。新增用例先红后绿;另做过变异检查:分别去掉 RUNNING 守卫、审计排除、排序、错误转字符串,相应用例都会失败。
  • pnpm exec tsc --noEmit、改动文件的 eslint 和 prettier:通过。
  • 全量 vitest 以 CI 为准:本地全量在 origin/main 6c0459b8 上就不稳定地大面积失败(跨文件 mock 泄漏、超时),失败文件单独跑都能通过,CI 上同一提交是绿的。
  • 真实验证:本分支构建的扩展在无头 Chromium 中加载,配对一个从 sctl 分支构建、隔离在 127.0.0.1:18643 的 daemon,覆盖 spec 的 18 项要求,全部通过:
    • complete、error、超时 running、--no-wait;
    • 定时脚本和禁用的脚本能运行;页面脚本返回 INVALID_REQUEST,未知 uuid 返回 NOT_FOUND;
    • 各过滤条件和上下限;审计记录被排除,且审计行里没有日志正文;
    • MCP 与 CLI 一致,没有审批进度消息;
    • Ctrl-C 和断开都是 exit 2,且已触发的运行不被撤回;
    • 用 6c0459b8 构建的旧版收到 METHOD_NOT_FOUND 和升级提示,已有命令照常可用。

审查修复(2026-11-05)

又做了一轮独立审查,确认的问题都已修复,先写失败用例再改:

  • 4f7ccc1b sandbox:脚本被重跑或停用后,旧运行晚到的结束状态不再覆盖当前运行的状态,也不再给它安排重试。这个问题原本就存在,脚本列表上的运行状态同样受影响;scripts.run 依赖这个状态,所以在这里一并修复。
  • 3b0038f5:
    • @run-in 与当前环境不符时,立即返回 INVALID_REQUEST,不触发运行,判断逻辑与 sandbox 共用 sandbox/utils.ts。
    • 截止时间从收到请求起算,覆盖排队和等待两段;wait:false 不再排队。
    • $/cancelRequest 或断开连接时,放弃还在排队的运行。
    • 日志字段遇到 BigInt 或循环引用也能编码,不会让整次查询失败。
    • LoggerDAO.walkNewestFirst 从最新往回读,够数就停,不再整表读取。
  • spec 已同步修订(sctl 729a663)。

验证(head bdc914d1):

  • 相关测试 216/216 通过,tsc 与 eslint 均为 exit 0;每项都做了变异检查。

  • 真实浏览器复测,三项都通过:

    • run-in 不符时立即报错;
    • BigInt 结果写进日志后,run / logs 正常;
    • 先用 --no-wait 启动运行 A,1 秒后启动运行 B;A 等待的本地延迟请求返回后,B 得到的仍是自己的结束结果,A 晚到的结束只记进日志。
  • bdc914d1 ♻️ 新增 sandbox/utils.ts:run_in.ts 并入其中;runtime.execScript 里内联的 once 周期门控提成纯函数 isOnceRunDone,行为不变。补了 utils.test.ts(once 门控以前没有测试),做过变异检查。相关测试 225/225 通过,tsc 和 eslint 均为 exit 0。

原用例按帧上限构造 4 MiB 错误文本与 2 MiB 日志,单跑约 40ms,但在 CI 覆盖率与分片负载下
达 280–630ms,超出 340ms 单测预算(PR #1825 CI shard 2/2)。2 MiB 日志本身不区分实现对错,
改为错误文本刚超过 16384 码元并直接断言截断结果;去掉截断时该用例仍失败。
CodFrm added a commit that referenced this pull request Nov 5, 2026
…间接办法

- 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 的版本
stopScript 只清掉旧运行的计时器并移出 execScriptMap,旧运行的 Promise 之后结束时仍会发布
COMPLETE/ERROR,CATRetryError 还会把已停用的定时脚本加回重试队列。运行状态不带运行 id,
脚本列表与外部接入 scripts.run 会把旧运行的终态当成新运行的。现在只有仍登记为该脚本当前
运行的 exec 才发布终态与安排重试;旧运行的结果照常写日志。
按 sctl spec(729a663)修订后的约定:
- 脚本 @run-in 与本实例环境不符时 sandbox 不会运行它,scripts.run 现在立即返回 INVALID_REQUEST
  而不是等满超时报 running。判断抽成 sandbox/run_in.ts 的 backgroundRunInAllowed,sandbox 与外部接入
  共用(含 Firefox spanning 例外),外部接入按 sandbox 同样合并 selfMetadata。
- 一个截止时间从收到请求起算,覆盖排队与等待,排队调用不再可能耗时“上一次超时 + 本次超时”;
  wait=false 不排队。排队耗尽截止时间时照样触发并返回 running。
- $/cancelRequest 与断开连接现在也送达进行中的 scripts.run:尚未触发的排队运行被放弃
  (OPERATION_EXPIRED,不再触发),正在等待的调用立即结束并释放订阅与计时器;审批取消语义不变。
- 日志标签含 BigInt 或循环引用时不再让整次查询 INTERNAL_ERROR:BigInt 取十进制文本,
  无法编码的值返回 "[unencodable]";运行错误文本同样处理。
- 日志改用 LoggerDAO.walkNewestFirst 沿 createtime 索引从新到旧游标读取,凑够 limit/字节预算即停,
  不再把 since 之后全部脚本的日志载入内存再排序;同毫秒按 id 先后由索引顺序保证。
run_in.ts 更名为 utils.ts;runtime.execScript 内联的 once 门控(按 once 所在字段比对 lastruntime)
提成纯函数 isOnceRunDone,行为不变。两者此前分别只被间接覆盖与完全没有测试,补上 utils.test.ts。
@CodFrm
CodFrm merged commit e829ff7 into main Nov 5, 2026
11 checks passed
@CodFrm
CodFrm deleted the feature/script-run-logs branch November 5, 2026 06:11
CodFrm added a commit that referenced this pull request Nov 5, 2026
…#1824)

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

- 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 不变)

* 📝 修正 scriptcat-userscript Skill 中与运行时不符的描述

- @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

* 📝 scriptcat-userscript Skill 补充写脚本的方法与常用写法

- 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 结构表与已知限制同步

* ✨ scriptcat-userscript Skill 附带静态检查与壳脚本生成工具

- 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 补充用法

* 📝 scriptcat-userscript Skill:后台/定时脚本优先用 sctl run / sctl logs 验证,旧版本退回间接办法

- 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 的版本

* 🐛 scriptcat-userscript check.mjs:修复对象键误报、关键字后正则漏报与 @connect 同域误判

- 对象字面量的键(`{ 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 单测预算

* 📝 scriptcat-userscript Skill:按代码订正运行结果查看位置、@connect 拒绝规则与若干易错点

- 脚本列表只显示运行状态(运行中/错误/已停止)与定时脚本下次运行时间,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
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