JavaScript 脚本

Chute 支持 JavaScript 脚本,用于高级请求/响应修改、自定义规则匹配、DNS 解析和定时任务。脚本在 Apple 平台上运行于 JavaScriptCore,在 Android 上运行于 QuickJS,遵循 Surge 兼容的脚本 API。

脚本在配置文件的 [Script] 段中定义。

配置

[Script]
MyScript = type=http-request, script-path=/path/to/script.js, pattern=^https?://example\.com, requires-body=true, max-size=65536, timeout=10, argument=myArg
CronJob = type=cron, script-path=/path/to/cron.js, cron-expression=*/30 * * * *

脚本参数

参数 必填 默认值 描述
type 否 http-request 脚本触发类型(见下文)
script-path 是 — JS 脚本的本地文件路径或 HTTP(S) URL
pattern 否 (匹配所有) 过滤脚本触发时机的 URL 正则表达式。它在请求完整 URL 的任意位置匹配——解密后是 https://…,明文 HTTP 是 http://…——从不单独拿路径去匹配,所以 ^/api 这样的模式不会命中
requires-body 否 自动检测 强制脚本接收完整的请求/响应 Body
max-size 否 131072 (128KB) 需要访问 Body 的脚本的最大 Body 大小(字节)。如果收集到的 Body 超过此大小,该请求将跳过脚本(Body 不会被截断)。在 HTTP/1.x——明文 HTTP 和解密后的 HTTP/1.1——上,无论这里怎么写,Chute 最多只为脚本缓冲 131072 字节,所以在那里它只能调低上限;解密后的 HTTP/2 报文会被完整交给脚本,只按 max-size 检查
timeout 否 5.0 秒 每次执行的超时时间,从执行开始时计时:加载脚本源(对远程脚本来说就是下载它)、运行代码以及等待定时器和请求都共用这段时间。超过 30 秒的值将被限制为 30 秒
argument 否 — 自定义字符串参数,在 JS 中以 $argument 访问
full-header-mode 否 false 为 true 时,$request.headers 和 $response.headers 不再是对象,而是 {field, value} 数组,每个头部行一项(Surge 的格式)。适用于 http-request、http-request-before-send 和 http-response 脚本;无论是否开启,$done() 都接受两种形式。见有多行的头部
debug 否 false 保留参数;目前无任何效果
cron-expression 否 — Cron 调度表达式(仅 cron 类型);也接受 cronexp= 和 Shadowrocket 的 cronexpr=,视为同一个键
event-name 否 (全部事件) event 脚本监听的事件——network-changed、engine-started 或 profile-reloaded;不写名称时三个事件都会运行(见 Event 脚本)
wake-system 否 false 保留参数;会被解析但目前无任何效果
enable 否 true 启用或禁用此脚本
script-update-interval 否 0 远程脚本更新轮询(见执行模型)
binary-body-mode 否 — 为兼容 Surge 而接受(也可写作 binary-mode),保存配置时会保留,但不改变任何行为:原始字节始终在 .bodyBytes 中
engine 否 — 会被接受并保留,但会被忽略:每个脚本都在所在平台自己的引擎上运行

脚本类型

类型字符串 枚举 描述
http-request HTTP Request 在请求发送到上游之前拦截并修改 HTTP 请求
http-response HTTP Response 在响应返回给客户端之前拦截并修改 HTTP 响应
http-request-before-send HTTP Request Before Send 在请求即将发往上游前修改请求
rule Rule 自定义规则匹配逻辑
dns DNS 自定义 DNS 解析
cron Cron 定时/计划脚本
event Event 系统事件处理(例如 network-changed)
generic Generic 为兼容 Surge 而接受;它不挂接到请求、规则、DNS 或事件上,所以 Chute 不会自行运行它。可以在 Chute iOS 或 Chute Android 的脚本列表中用运行来执行它,或者使用 HTTP 控制 API 的 POST /api/scripts/run

未知的 type= 会在日志中报告,并按 http-request 处理。

Body 自动检测:对于 http-request 和 http-response 脚本,源码中包含 $request.body 或 $response.body 时会自动获得 Body,最大不超过 max-size。使用 requires-body=true 可强制此行为。只读取 .bodyBytes 或 .rawBody 的脚本不会被检测到,http-request-before-send 脚本也永远不会被检测:这些都需要 requires-body=true。


JavaScript API 参考

脚本在沙盒化的 JavaScript 环境中运行——Apple 平台上是 JavaScriptCore,Android 上是 QuickJS——以下全局对象可用。

$request(只读)

可用范围:http-request、http-response、http-request-before-send、rule、dns

属性 类型 描述
.url String 完整请求 URL;除非是该 scheme 的默认端口,否则会写出端口,IPv6 主机放在方括号中。请求目标本身已是绝对 URL 时原样使用,不以 / 开头的目标会在前面补上 /,不知道主机时只有路径。在 rule 脚本中是目标主机,在 dns 脚本中是被查询的域名
.method String HTTP 方法(GET、POST 等),DNS 查询时为 QUERY
.headers Object 或 Array 请求头,每个名称对应它的值;有多行的头部,各行的值合并成一个字符串(见有多行的头部)。full-header-mode=true 时是 {field, value} 数组。按名称读取时不区分大小写(见头部名称)
.body String 或 null 请求 Body(UTF-8 解码)
.bodyBytes Bytes 或 null 原始请求 Body 字节(见下面的说明)
.hostname String 目标主机名
.destPort Number 目标端口
.processPath String 发起请求的进程路径(macOS;在 Android 上,对于 VPN 捕获的 TCP 连接,是应用的 APK 路径)
.userAgent String User-Agent 请求头值
.sourceIP String 来源 IP 地址
.listenPort Number 代理监听端口
.requestId String 唯一请求 ID
.dnsResult String 解析后的 IP 地址
.srcPort Number 来源端口
.protocol String 检测到的协议:http、https、tcp、dns

$response(只读)

可用范围:http-response

属性 类型 描述
.status Number HTTP 状态码
.headers Object 或 Array 响应头,每个名称对应它的值;有多行的头部,各行的值合并成一个字符串(见有多行的头部)。full-header-mode=true 时是 {field, value} 数组。按名称读取时不区分大小写(见头部名称)
.body String 或 null 响应 Body(UTF-8 解码)
.bodyBytes Bytes 或 null 原始响应 Body 字节(见下面的说明)
.rawBody Bytes 或 null .bodyBytes 的别名

.body、.bodyBytes 与 .rawBody 拿到的是同一份 Body:交给脚本之前已去掉 chunked 分块,并解开 gzip 或 deflate 的 Content-Encoding。.bodyBytes 与 .rawBody 存放的是一个字节对象。把它传给接受字节的调用——$utils.ungzip()(其结果同样是字节对象),或者 $done() 里的 body,它接受这种对象作为原始 Body。这个对象具体是什么取决于引擎:在 Apple 平台上是一个没有 .length、也不能下标访问的不透明包装对象,在 Android 上是 Uint8Array,所以打算在所有平台上运行的脚本不应依赖 .length 或下标访问。要读内容请用 .body,那就是同样这些字节按 UTF-8 解码的结果。

$done(value) — 完成处理函数

必须在脚本结束时恰好调用一次,以表示脚本完成。脚本返回时若一次都没调用过它,会立即按直通处理——除非还有未完成的 setTimeout 定时器或 $httpClient 请求。timeout= 从执行开始的那一刻计时:加载脚本源、运行代码以及这段等待都计入其中。

$done({})                        // 直通 — 不做任何修改
$done()                          // 中止连接
$done({matched: true})           // 规则匹配结果(仅 rule 脚本)
$done({address: "1.2.3.4"})      // DNS 结果(仅 dns 脚本)

在 http-request、http-request-before-send 或 http-response 脚本里,不带参数(或参数不是对象)地调用 $done() 会中止连接:Chute 不会发出任何东西来代替这个请求或响应,被中止的请求也不会到达服务器。已经在发往客户端的数据会先写完,然后连接立即关闭。在 HTTP/2 上,只有这个请求的流会被重置(RST_STREAM,错误码 CANCEL),不会发出任何替代内容;连接以及其上的其他请求照常进行。

脚本给出的头部和 url 在使用前会先检查。出现以下情况时,头部会被丢弃,并在日志里记一条警告:

  • 名称或值里含换行(CR 或 LF)或 NUL 字符:[JS] Dropped header <name>: a header name or value cannot contain a line break;
  • 名称不是合法的 HTTP 字段名(RFC 9110 §5.1)。合法的名称由一个或多个字母、数字或 !#$%&'*+-.^_`|~ 中的字符组成,所以空名称,或含空格、冒号、非 ASCII 字母的名称都会被丢弃:[JS] Dropped header <name>: not a valid header name。

结果中的其余部分照常生效。以数组形式给出的头部,以及请求脚本返回的 response 里的头部,也按同样的规则检查。含换行、NUL 或空格的 url 会被忽略,同样记警告。url 的路径和查询按原样发送,%20 这类转义不会被解开。

HTTP 请求脚本返回值:

$done({
    url: "https://new.example.com/path",     // 重写 URL
    headers: {"X-Custom": "value"},           // 修改请求头
    body: "new request body",                 // 修改 Body
    response: {                               // 返回合成响应(跳过上游)
        status: 200,
        headers: {"Content-Type": "text/html"},
        body: "<html>Blocked</html>"
    }
})

当提供 response 时,请求被短路:Chute 直接向客户端返回合成响应,请求不会发往上游服务器。无论 Chute 在哪条路径上处理这个请求(包括 HTTP/2),也无论脚本是在请求头到达时运行、拿到完整 Body 后运行,还是作为 http-request-before-send 运行,都是如此。在 HTTP/1.x 上,状态行带该状态码的标准原因短语(HTTP/1.1 404 Not Found),响应发完后连接关闭;在 HTTP/2 上只应答这一个请求的流。status 是 200 到 599 之间的数字,或去掉首尾空白后是这样一个整数的字符串("404");其他值或不给时为 200。Content-Length 按 body 计算,不管 headers 里怎么写。这对于阻止请求、模拟 API 或返回缓存内容非常有用。

Chute 自己生成的应答——模拟响应(Map Local)、脚本的 response,以及 URL 重写和 REJECT 类策略的应答与错误页——遵守 HTTP:对 HEAD 请求的应答只有头部,其中的 Content-Length 与 GET 得到的相同;状态码为 204、205 或 304 的应答既不带 Body,也不带 Content-Length。

HTTP 响应脚本返回值:

$done({
    status: 200,                              // 修改状态码
    headers: {"X-Custom": "value"},           // 修改响应头
    body: "new response body",                // 修改响应 Body
    url: "https://other.example.com"          // 重定向(默认 302)
})

body 只替换响应的 Body,其他不变:状态行和头部仍是服务器的,脚本返回的 status 和 headers 合并进去,服务器原来的 Body 被丢弃。如果脚本只在响应头上运行,之后 Body 重写规则和要读取 Body 的 http-response 脚本照常运行,作用在新 Body 上。对 HEAD 的响应,以及状态码为 1xx、204、205 或 304 的响应没有 Body,所以返回的 body 在这些响应上会被忽略,但 status 和 headers 仍然生效。

status 是 200 到 599 之间的数字,或去掉首尾空白后是这样一个整数的字符串("301")。其他值(包括 true 和 false)都会被忽略,响应保留原来的状态码。改变了状态码的 status 也会让状态行带上新状态码的标准原因短语。status 为 204、205 或 304 时,响应不带 Body(服务器的 Body 和返回的 body 都不发出),也不带长度头;在 HTTP/1.x 上,头部发完后连接即关闭。

当提供 url 时,Chute 返回一个指向它的重定向,替代原始响应。重定向的状态码是返回的 status(例如 301、307 或 308),没有给出或被忽略时为 302;返回的 headers 照常合并进去,Location 就是 url。没有 body 时重定向不带 Body,服务器的 Body 不会发出;在 HTTP/2 上,服务器的 trailers 也不会发出。url 必须与 status / headers / body 中的至少一项一起返回;仅包含 url 的结果会被视为直通。

DNS 脚本返回值:

$done({address: "1.2.3.4"})                  // 单个 IP
$done({addresses: ["1.2.3.4", "5.6.7.8"]})   // 多个 IP
$done({address: "10.0.0.1", ttl: 300})       // 自定义 TTL(秒,默认 60)
$done({server: "1.1.1.1"})                   // 改由该服务器解析
$done({servers: ["tls://dns.google", "8.8.8.8#disable-ipv6"]})

server / servers 不是直接给出答案,而是指定由谁解析:每一项都是普通的 DNS 服务器条目——地址、tls://、https://,并可带常见的 # 选项——它们会被同时询问,最先回答者胜出。若其中没有一项可用,该次查询直接失败,而不会回落到已配置的服务器池。

有多行的头部

一个头部可以出现在多行上:设置两个 Cookie 的响应带有两行 Set-Cookie。http-request、http-request-before-send 和 http-response 脚本可以读到每一行,也可以写出多行。

读取

默认情况下,$request.headers 和 $response.headers 是对象,每个头部名称对应一个字符串。头部有多行时,各行的值按顺序合并:Cookie 的值用 "; " 连接,其他头部(包括 Set-Cookie)的值用 ", " 连接。

$response.headers["Set-Cookie"]  // "a=1; Path=/, b=2; Path=/"
$request.headers["Cookie"]       // "a=1; b=2"

Cookie 的 Expires 日期里含有逗号,所以合并后的 Set-Cookie 字符串无法可靠地拆回一个个 Cookie。要逐行读取,在脚本那一行加上 full-header-mode=true。这时 $request.headers 和 $response.headers 是数组,每行一个 {field, value} 对象,与 Surge 的格式相同。同名的各行保持原来的顺序。

// full-header-mode=true
const cookies = $response.headers
  .filter(h => h.field.toLowerCase() === "set-cookie")
  .map(h => h.value)   // ["a=1; Path=/", "b=2; Path=/"]

在 HTTP/2 上也是如此:客户端把 Cookie 拆成多个 cookie 字段时,脚本读到的是用 "; " 连接成的一个 Cookie;每个 set-cookie 各占一行。

写入

$done() 里的 headers 只改动它列出的头部,其他头部保持原样。请求脚本用来应答的 response 里的 headers 也遵循同样的规则。

值 效果
字符串 该头部只剩一行,值为这个字符串;原有的各行都被替换
字符串数组 每个值一行,按顺序替换该头部原有的各行。{"Set-Cookie": ["a=1", "b=2"]} 发出两行 Set-Cookie
[] 删除该头部
其他值(数字、null、对象、含非字符串元素的数组) 忽略:该头部保持原样

脚本把读到的合并字符串原样返回时,该头部保留原来的各行。所以 const h = $response.headers; h["X-A"] = "1"; $done({headers: h}) 只改动 X-A,两行 Set-Cookie 发出时仍是两行。其他任何字符串都会把该头部的所有行替换成一行。要添加一个 Cookie,请用数组写出完整列表:先在 full-header 模式下读出现有的各行,再返回 {"Set-Cookie": [...existing, "c=3"]}。

一个请求只带一行 Cookie:http-request 或 http-request-before-send 脚本在 headers 里给 Cookie 的数组会用 "; " 连接,所以 {"Cookie": ["a=1", "b=2"]} 发出的是 Cookie: a=1; b=2。

无论是否开启 full-header-mode,$done() 也接受数组形式的 headers,数组元素是 {field, value} 对象:

$done({headers: [{field: "Set-Cookie", value: "a=1"},
                 {field: "Set-Cookie", value: "b=2"},
                 {field: "X-A", value: "1"}]})

各项按名称分组(不区分大小写),每组按给出的顺序替换该头部原有的各行。数组里没有列出的头部保持原样,所以漏写一个头部并不会删除它;要删除头部,请用对象形式并给 []。缺少字符串 field 或字符串 value 的项会被忽略。

  • 头部名称不区分大小写:set-cookie 和 Set-Cookie 是同一个头部,所以每个名称只写一次。不是合法头部名称的名称会被丢弃,见 $done 中的说明。
  • 含换行或 NUL 的值会被丢弃,见 $done 中的说明。在数组里只丢弃这一个值;如果所有值都被丢弃,该头部保持原样。
  • 在 HTTP/2 上,每一行都成为一个单独的字段,名称为小写。只对单个连接有效的头部(例如 Connection 和 Keep-Alive)不会发出。

$request.headers 与 $response.headers 中的头部名称

$request.headers 和 $response.headers 查找头部时不管名称的大小写怎么写:$response.headers['ETag']、$response.headers['etag'] 和 $response.headers['Etag'] 读到的是同一个头部。这同样适用于 in('etag' in $response.headers)、赋值(headers['content-type'] = 'text/html' 改的是对象里已有的 Content-Type,而不是为它另加一个名称)以及 delete。

列出名称时(Object.keys()、for…in、JSON.stringify()),每个头部只出现一次,写法是 Chute 存储它的写法——不管客户端或服务器怎么写,每个单词首字母大写:Content-Type、Etag、X-Api-Key、Www-Authenticate。

只有从 $request.headers 或 $response.headers 读到的那个对象才这样匹配名称。你自己做的副本,例如 Object.assign({}, $request.headers) 或 {...$response.headers},是普通对象;在副本里请用上面那种写法。full-header-mode=true 时,headers 是 {field, value} 对象组成的数组,field 也是同样的写法,这种查找不适用;请改为比较 field.toLowerCase()。

$httpClient — 异步 HTTP 客户端

在脚本中发起 HTTP 请求。所有请求在 $done() 或超时时被取消。

$httpClient.get(url, function(error, response, data) {
    if (error) {
        console.log("Request failed: " + error)
    } else {
        console.log("Status: " + response.status)
        console.log("Response: " + data)
    }
})

$httpClient.post(url, {headers: {...}, body: "...", timeout: 5}, callback)
$httpClient.put(url, options, callback)
$httpClient.del(url, options, callback)
$httpClient.head(url, options, callback)
$httpClient.options(url, options, callback)
$httpClient.patch(url, options, callback)

选项对象还可以携带 policy,即配置中已定义的策略名称:该请求会改为经该策略发出,而不是走默认路由。配置中没有定义的名称会在日志中给出警告,请求走默认路由。Surge 的内联 policy-descriptor 形式不受支持:它会在日志中给出警告,请求同样走默认路由——请在配置中定义该策略并传入它的名称。

回调签名:callback(error, response, data)

  • error:错误字符串或 null
  • response:{status: Number, headers: Object} 或 null。headers 查找头部时不区分名称的大小写,列出的名称与 $request.headers 的写法相同——每个单词首字母大写,如 Content-Type、Etag、X-Api-Key;带不带 policy 都一样
  • data:按 UTF-8 解码的响应 Body 字符串;不是合法 UTF-8 的 Body 会以像 .bodyBytes 那样的字节对象到达,而不是 null。只有根本没有 Body 时才是 null

$httpClient 可在 http-request、http-response、http-request-before-send、cron 和 event 脚本中使用。它在 rule 和 dns 脚本中不可用。

单个脚本最多同时有 8 个未完成的请求,所有脚本合计最多 16 个。超过任一上限的请求不会被发送,其回调也永远不会运行;每次运行只在日志中提示一次。响应 Body 超过 4 MB 时请求失败。

$persistentStore — 键值存储

持久化的键值存储,在脚本和进程重启后仍然保留;所有脚本读写的是同一组键。在 Android 上,它保存在 Chute 的私有存储中,不会被纳入设备备份。

$persistentStore.write(data, key)   // 存储一个值
$persistentStore.read(key)          // 读取一个值
$persistentStore.remove(key)        // 删除一个值

$notification — 本地通知

发送本地系统通知。Chute Apple TV 不显示任何通知。

$notification.post("Title", "Subtitle", "Notification body text")

可选的第四个参数携带 Surge 的选项:url(点击通知时打开的链接)、action(写了 url 时默认为 open-url,这也是应用唯一会处理的动作——其他动作,如 Surge 的 clipboard,会被携带,但没有任何地方处理它们)和 auto-dismiss(秒)。它们随通知一起附带;其他键会被忽略并给出警告。url 由 Chute iOS、Chute Mac 与 Chute Android 处理,点击通知时会打开它。auto-dismiss 只有 Chute Android 会处理,它会在这么多秒后移除该通知;Apple 平台上的应用则让通知留在原处。

$notification.post("Title", "", "Tap to open", {url: "https://example.com", "auto-dismiss": 5})

脚本发出的通知与带 notification-text 的规则发出的通知一样,遵循应用里的通知开关:Chute iOS 是允许通知,Chute Mac 是显示事件上报通知,Chute Android 是系统授予 Chute 的通知权限。把它关掉就什么都不会显示。Chute Android 会把它们发到单独的通知渠道脚本通知中,这个渠道可以在系统的通知设置里单独关闭。参见通知报告。

$network — 网络信息

只读的网络状态信息。

$network.dns   // DNS 服务器 IP 数组
$network.wifi          // {ssid: "WiFiName", bssid: null}
$network.v4            // {primaryAddress, primaryRouter}
$network.v6            // {primaryAddress, primaryRouter}
$network.primaryRouter // IPv4 默认网关,没有时为 null
$network.cellularData  // {radio: "LTE" | "5G" | ..., carrier: null}

ssid 与 bssid 在 iOS 上有值——那里隧道可以读取 Wi-Fi 身份;在 Android 上,当 Chute 拥有 Android 读取网络名称所需的权限时也有值(见 Android 快速上手);在 macOS 与 tvOS 上 ssid 为空、bssid 为 null。primaryAddress 会跳过隧道自身的地址和链路本地地址,因此它就是设备接入网络所用的地址。carrier 恒为 null——iOS 16 起不再提供运营商名称。

$environment — 运行时信息

$environment.system     // "iOS"、"macOS" 或 "Android"
$environment.appVersion   // KLNEKit SDK 版本字符串
$environment.surgeVersion // 引擎自身的版本,使用 Surge 脚本读取的名称
$environment.language     // 以 BCP 47 标签表示的首选语言,例如 "en-US"
$environment.deviceModel  // "iPhone"、"Mac" 或 "AppleTV";Android 上是设备型号,例如 "Pixel 8"

$utils — 工具函数

$utils.geoip("1.2.3.4")   // 国家代码(例如 "US")
$utils.ipasn("1.2.3.4")   // ASN 号码(例如 "13335")
$utils.ipaso("1.2.3.4")   // ASN 所属组织(例如 "CLOUDFLARENET")
$utils.ungzip(data)       // 解压 gzip 数据

$klne — 代理控制 API

从脚本中控制代理运行时。$klne 在所有脚本类型中均可用。

$klne.getPolicyGroups()               // 获取所有策略组
$klne.selectGroupDetails()            // 同样这些策略组,采用 Surge 的数据结构
$klne.selectPolicy("Group", "Proxy")  // 切换策略组中的策略
$klne.getActiveConnections()          // 列出活跃连接
$klne.closeConnection("id")           // 按 getActiveConnections() 给出的 id 关闭一条连接
$klne.flushDNS()                      // 清除 DNS 缓存
$klne.startURLTest("Group")           // 对策略组触发 URL 测试
$klne.reloadConfiguration()           // 重新加载所有配置
$klne.setOutboundMode("rule")         // 设置模式:"global"/"proxy"、"direct"、"rule"
$klne.setHTTPCaptureEnabled(true)     // 启用/禁用 MitM
$klne.setRewriteEnabled(true)         // 启用/禁用改写族

getPolicyGroups() 返回可选择的策略组:

{
  count: 1,                    // Number — 策略组数量
  policyGroups: [{
    name: "MainGroup",         // String — 策略组名称
    type: 0,                   // Number — 0 select、1 url-test、2 fallback、3 ssid、5 load-balance
    policyNames: ["A", "B"],   // Array of String — 组的实际成员,包括订阅带来的节点
    selectedIndex: 0,          // Number — 当前生效策略在 policyNames 中的下标(无法确定时不存在此字段)
    selectedPolicy: "A"        // String — 当前生效策略的名称(无法确定时不存在此字段)
  }]
}

selectGroupDetails() 按 Surge 的结构返回同样这些策略组——policyGroups 把每个组名映射到其成员名列表,decisions 把每个组名映射到当前选中的成员:

{
  policyGroups: {MainGroup: ["A", "B"]},
  decisions: {MainGroup: "A"}
}

选择结果无法确定的组不会出现在 decisions 里。

getActiveConnections() 返回描述当前连接的数组:

[{
  id: 1042,             // Number —— 连接号,也就是 closeConnection 接受的值
  host: "example.com",   // String — 目标主机(未知时不存在此字段)
  port: 443              // Number — 目标端口
}]

其余方法接受普通参数,无返回值:

  • selectPolicy(group, policy) — 将 policy(取自该组 policyNames 中的名称)设为 group 的当前生效策略。未知的组名或策略名会被忽略,并在日志中记录一条警告。
  • closeConnection(id) — 关闭该 id 对应的连接,也就是 getActiveConnections() 返回的那个编号。若该 id 并未打开,则忽略并记录警告。
  • flushDNS() — 清除 DNS 缓存。
  • startURLTest(group) — 对 url-test、fallback 或 load-balance 类型的策略组发起异步延迟测试;其他组类型或未知组名会被忽略并记录警告。
  • reloadConfiguration() — 立即重新拉取配置的 #!MANAGED-CONFIG 源(而不是等到更新间隔),内容有变化时应用它。重新应用引擎已经持有的配置不会带来任何变化,因此没有托管源的配置只会记录一条警告。
  • setOutboundMode(mode) — "global" 与 "proxy" 均表示全部走代理,"direct" 表示全部直连,其他任何值都会选择规则模式。
  • setHTTPCaptureEnabled(enabled) — 在运行时启用或禁用 HTTPS 解密(MitM);参数为布尔值。
  • setRewriteEnabled(enabled) — 在运行时启用或禁用整个改写族——URL 重写、Header 重写、Body 重写和模拟响应;参数为布尔值。

为兼容 Surge 脚本,也提供 $surge,其中含义完全相同的调用有:$surge.setSelectGroupPolicy(group, policy)(与 selectPolicy 相同)、$surge.selectGroupDetails()(与 selectGroupDetails 相同)、$surge.setOutboundMode(mode)、$surge.setHTTPCaptureEnabled(enabled)、$surge.setRewriteEnabled(enabled)(改写族总开关,涵盖 URL 重写、Header 重写、Body 重写与模拟响应)以及 $surge.retestGroup(name)。Surge $surge 的其余部分在这里没有对应实现,读取结果为 undefined,脚本可据此判断。

$httpAPI — 控制 API 桥接

从脚本里调用引擎自己的 HTTP 控制 API。$httpAPI 在所有脚本类型中均可用。

$httpAPI("GET", "/api/status", null, function(result) {
    console.log(result.statusCode)   // Number —— HTTP 状态码
    console.log(result.body.data)    // Object —— 解析后的 JSON Body
})

$httpAPI("/api/status")                  // 只给一个参数:对该路径做 GET
$httpAPI("DELETE", "/api/dns/cache")     // 方法加路径
var result = $httpAPI("GET", "/api/status")  // 同一个对象也会作为返回值给出

这个调用是同步的——请求在引擎内部路由,{statusCode, body} 对象既传给回调,也作为返回值给出。path 必须以 / 开头。字符串形式的 body 原样发送,其他任何值都会编码成 JSON。因为请求根本不经过监听器,所以不涉及令牌,API 也不必开启。POST /api/scripts/run 会以 409 和 would_reenter 被拒绝——它需要的正是调用方脚本此刻占着的那个引擎;12 秒内没有应答的路由会得到 504。

$script — 脚本元数据

$script.name       // 配置中的脚本名称
$script.type       // 脚本类型字符串
$script.startTime  // 单调时间戳(自系统启动基准以来的秒数,非纪元时间)
$script.sessionID  // 每次执行都不同,用于把同一次运行的状态串起来

每次执行的全局变量

以下变量在每次脚本执行时注入,并且特定于某些脚本类型。

$argument — 脚本参数

来自脚本配置中 argument= 参数的字符串值;脚本没有该参数时为 null。可用范围:http-request、http-response、http-request-before-send、rule、dns、cron、event。

console.log("Argument: " + $argument)

$domain — DNS 域名(仅 DNS 脚本)

正在查询的域名。仅可在 dns 脚本中使用。

var domain = $domain  // 例如 "example.com"

$cronexp — Cron 表达式(仅 Cron 脚本)

来自脚本配置的 Cron 调度表达式。仅可在 cron 脚本中使用。

console.log("Schedule: " + $cronexp)  // 例如 "*/30 * * * *"

$event — 事件信息(仅 Event 脚本)

有关触发事件的信息。Chute 会触发 network-changed、engine-started 和 profile-reloaded——见 Event 脚本。

console.log("Event: " + $event.name)  // "network-changed"

$event.name 是实际触发的那个事件。写了 event-name= 的脚本只会为那一个事件运行,所以这个名字永远是它自己声明的那个;没写 event-name= 的脚本三个事件都会跑,靠 $event.name 来区分。

console — 日志

console.log("Debug message")    // 详细日志
console.warn("Warning message")  // 警告日志
console.error("Error message")   // 标记为 JavaScript 错误的警告日志

每条消息会被截断到 256 个字符,其中的凭据——Authorization 或 Cookie 请求头、password= 或 token= 的值之类——会被记录为 <redacted>。console.log 以 verbose 级别写入,所以只有 loglevel 为 verbose 时才会显示;console.warn 和 console.error 在默认的 warning 级别下就会显示。

setTimeout(fn, seconds) — 定时器

安排函数在延迟后执行。

setTimeout(function() {
    console.log("Delayed execution")
}, 2.5)  // 2.5 秒

$scriptImport(subScriptPath) — 子脚本加载器

加载并执行另一个 JavaScript 文件。仅支持本地文件路径;http(s):// 和 file:// URL 会被拒绝。($script 保留给脚本元数据对象使用。)

$scriptImport("/path/to/helper.js")

// 使用 $persistentStore 在脚本之间传递数据

脚本类型详解

三种 HTTP 脚本类型只在明文 HTTP 请求经 Chute 的 HTTP 代理进入时才能看到该请求;HTTPS 请求则只有在其主机被解密时才能看到——把该主机加入 HTTPS 解密中的 hostname。

HTTP 请求脚本

在收到请求头时执行。可以在请求转发之前修改 URL、请求头和 Body。

[Script]
ModifyHeaders = type=http-request, script-path=modify.js, pattern=^https://api\.example\.com

HTTP 响应脚本

在收到响应头时执行。可以在返回给客户端之前修改状态码、响应头和 Body。

[Script]
ModifyResponse = type=http-response, script-path=response.js, pattern=^https://api\.example\.com

需要 Body 的脚本要等 Body 全部到齐后才运行。在 HTTP/1.x 上,如果服务器先关闭了连接:既没有 Content-Length 也不是分块编码的 Body 以连接关闭为结尾,此时已经完整,脚本照常运行;按 Content-Length 或分块编码分帧、却被关闭截断的 Body 不会交给脚本,而是按收到的原样发给客户端,随后关闭连接。同一条报文匹配的 Body 重写规则会先应用,脚本看到的是改写后的 Body。

HTTP 请求发送前脚本

在请求即将发往上游之前执行。Body 被暂存时(本脚本设了 requires-body=true,或有 Body 重写规则、要读取 Body 的 http-request 脚本命中),脚本等 Body 全部到齐后运行;没有 Body 的请求,或 Body 超过 max-size 的请求不会被暂存,暂存途中超过 max-size 的 Body 则按原样继续发送:这两种情况下,脚本都在请求头发出时以空 Body 运行,设了 requires-body=true 的脚本则不运行。适用于修改 POST/PUT 请求 Body。它最后执行:在任何 Body 重写规则和要读取 Body 的 http-request 脚本之后,看到的是它们处理后的 Body。脚本交回的 Body 会替换请求原来的 Body(原 Body 丢弃),Content-Length 随之按新 Body 设置。

[Script]
BeforeSend = type=http-request-before-send, script-path=before-send.js, pattern=^https://api\.example\.com, requires-body=true

规则脚本

自定义规则匹配。脚本必须调用 $done({matched: true}) 或 $done({matched: false})。

[Rule]
SCRIPT,MyRuleScript,DIRECT

[Script]
MyRuleScript = type=rule, script-path=rule.js

DNS 脚本

自定义 DNS 解析。接收 $domain 并返回解析后的地址。

// dns.js
var domain = $domain
if (domain === "internal.example.com") {
    $done({address: "10.0.0.1", ttl: 300})
} else {
    $done({})  // 直通到正常的 DNS 解析
}

[Host] 中的 <域名> = script:<名称> 条目会把匹配域名的解析交给指定名称的 DNS 脚本,参见本地 DNS 映射。

Cron 脚本

使用 Cron 表达式进行定时执行。最小间隔为 60 秒。

[Script]
HalfHourlyTask = type=cron, script-path=task.js, cron-expression=*/30 * * * *

表达式必须正好是五个字段,字段之间用单个空格分隔。其他写法——双空格、制表符、四个字段、六个字段——都会让该脚本静默失效:它根本不会被排期,日志里也不会写任何东西。

这五个字段里只有分钟字段会被采用:*/N 表示每 N 分钟运行一次。其他任何分钟字段都会每 60 秒触发一次,脚本必须自行检查当前时间来决定是否执行操作。

Event 脚本

由系统事件触发。会触发的事件有三个:

事件 何时触发
network-changed Wi-Fi 或蜂窝网络发生了变化
engine-started 引擎启动完成——策略、规则和监听器都已就绪
profile-reloaded 一次配置重载完成,触发的是被重载的那份配置自己的脚本
[Script]
NetChange = type=event, script-path=network-changed.js

$event 对象可用:

$event.name  // "network-changed"、"engine-started" 或 "profile-reloaded"

Surge 用 event-name= 指定事件,Chute 同样会读取:event-name=engine-started 表示该脚本只在这个事件上运行。没有指定事件的脚本三个事件都会跑,靠读 $event.name 判断是哪一个触发的。绑定到其他事件的脚本(Surge 还会触发 notification 等事件)在 Chute 中永远不会运行,配置载入时日志会说明这一点。


实用示例

重定向移动设备

一个根据 User-Agent 重定向移动用户的 http-request 脚本:

[Script]
MobileRedirect = type=http-request, script-path=mobile-redirect.js, pattern=^https://example\.com
// mobile-redirect.js
var ua = $request.headers["User-Agent"] || ""
if (/Mobile|Android|iPhone/.test(ua)) {
    $done({
        response: {
            status: 302,
            headers: {"Location": "https://m.example.com" + $request.url.replace(/.*example\.com/, "")},
            body: ""
        }
    })
} else {
    $done({})
}

屏蔽 API 响应中的内容

一个从 JSON API 响应中移除广告和赞助内容的 http-response 脚本:

[Script]
RemoveAds = type=http-response, script-path=remove-ads.js, pattern=^https://api\.example\.com/feed, requires-body=true
// remove-ads.js
var body = JSON.parse($response.body)
if (body.ads) {
    delete body.ads
}
if (body.recommendations) {
    body.recommendations = body.recommendations.filter(function(r) {
        return !r.sponsored
    })
}
$done({body: JSON.stringify(body)})

发送前修改请求 Body

一个清理 POST 负载的 http-request-before-send 脚本:

[Script]
SanitizePayload = type=http-request-before-send, script-path=sanitize.js, pattern=^https://api\.example\.com/submit, requires-body=true
// sanitize.js
var body = JSON.parse($request.body)
body.clientSecret = "[REDACTED]"
body.timestamp = Math.floor(Date.now() / 1000)
$done({body: JSON.stringify(body)})

自定义规则:基于时间的路由

一个根据时间选择不同代理的 rule 脚本:

[Rule]
SCRIPT,TimeBasedRule,ProxyA

[Script]
TimeBasedRule = type=rule, script-path=time-rule.js
// time-rule.js
var hour = new Date().getHours()
if (hour >= 9 && hour < 18) {
    $done({matched: false})  // 工作时间内让给下一条规则处理
} else {
    $done({matched: true})   // 非工作时间使用 ProxyA
}

为内部域名自定义 DNS

一个将内部主机名解析为本地 IP 的 dns 脚本:

[Script]
InternalDNS = type=dns, script-path=internal-dns.js
// internal-dns.js
var internalHosts = {
    "gitlab.local": "10.0.0.10",
    "registry.local": "10.0.0.11",
    "monitor.local": "10.0.0.12"
}
if (internalHosts[$domain]) {
    $done({address: internalHosts[$domain], ttl: 3600})
} else {
    $done({})  // 直通到正常的 DNS 解析
}

网络变更时自动切换策略

一个在网络变更后探测连通性并据此切换策略组的 event 脚本:

[Script]
NetSwitch = type=event, script-path=network-switch.js, timeout=15
// network-switch.js
$httpClient.head("https://www.google.com/generate_204", {timeout: 5},
    function(error, response, data) {
        if (error) {
            // 新网络下探测失败 — 切换到备用策略组
            $klne.selectPolicy("MainGroup", "BackupProxy")
            console.log("Probe failed after " + $event.name)
        } else {
            $klne.selectPolicy("MainGroup", "MainProxy")
        }
        $done({})
    })

注意:$network 对所有脚本都可见,但其 wifi.ssid 只在 iOS 和 Android 上有值(见 $network 的注意事项)。macOS 与 tvOS 上该名称为空,所以要在所有平台上都能工作的 event 或 cron 脚本请照上面用 $httpClient 探测,而不是读 SSID。

定期健康检查

一个每 30 分钟检查代理健康状况的 cron 脚本:

[Script]
HealthCheck = type=cron, script-path=health-check.js, timeout=15, cron-expression=*/30 * * * *
// health-check.js
$httpClient.head("https://www.google.com/generate_204", {timeout: 10},
    function(error, response, data) {
        if (error || response.status !== 204) {
            console.error("Health check failed: " + (error || "status " + response.status))
            $notification.post("Chute Alert", "Health Check", "Cannot reach Google")
        } else {
            console.log("Health check OK")
        }
        $done({})
    }
)

请在回调内部调用 $done({})——脚本完成时会取消所有待处理的 $httpClient 请求,因此若在脚本末尾同步调用 $done(),健康检查会在收到响应之前就被取消。出于同样的原因,请求自己的 timeout 必须比脚本的短:脚本的 timeout(默认 5 秒)先到期时,请求会被取消,其回调永远不会运行。上面两个示例都在 [Script] 行上设置了 timeout=15。

用外部数据丰富 API 响应

一个通过调用辅助 API 来丰富用户数据的 http-response 脚本:

[Script]
EnrichUsers = type=http-response, script-path=enrich.js, pattern=^https://api\.example\.com/users, requires-body=true, timeout=20
// enrich.js — 同时最多 8 个请求在途,即单个脚本的上限
var users = JSON.parse($response.body)
var next = 0
var pending = 0

function launch() {
    while (pending < 8 && next < users.length) {
        fetchAvatar(next++)
    }
    if (pending === 0) {
        $done({body: JSON.stringify(users)})
    }
}

function fetchAvatar(index) {
    pending++
    $httpClient.get("https://internal-api.example.com/avatar/" + users[index].id,
        function(error, resp, data) {
            if (!error && resp.status === 200) {
                users[index].avatar = JSON.parse(data).url
            }
            pending--
            launch()
        }
    )
}

launch()

执行模型

  • 所有脚本在专用的串行队列上运行,以保证线程安全。
  • 每次脚本执行都有按脚本设置的超时时间,从执行开始时计时:加载脚本源(远程脚本在这段时间内下载)、运行代码以及等待定时器和请求都共用它。如果在超时时间内未调用 $done(),该脚本将被视为直通。脚本返回时没有调用 $done()、又没有未完成的 setTimeout 定时器或 $httpClient 请求,会立刻按直通处理。
  • 维护一个包含 3 个预热上下文的池;每次执行后上下文会被丢弃并替换为全新的上下文,因此全局变量绝不会在多次运行之间泄漏。
  • 脚本引擎启动时,Chute 会记下进程的内存占用。此后一旦增长超过 10 MB(iOS 和 tvOS)或 512 MB(macOS),所有脚本都会被跳过,其报文直通;在 Android 上衡量的是已使用的 Java 堆,余量为 512 MB。这个余量针对整个进程,而不是单个脚本;起点在 Chute 运行期间也不会重新记录。
  • iOS、tvOS 和 Android 上同时最多有 16 个脚本执行在进行(macOS 为 64 个),它们的脚本源合计最多 4 MB(macOS 为 8 MB)。超过任一上限的执行会被跳过,其报文直通;日志会写 execution admission is full。
  • 单个脚本源上限为 macOS 1 MB、iOS/tvOS/Android 512 KB。一份配置能声明多少个脚本没有限制;有上限的是同时在加载脚本源的数量——macOS 32 个,iOS/tvOS/Android 8 个。超过上限时日志会写 source load queue is full,那一次执行会在没有脚本源的情况下跑,也就是直通。
  • 远程脚本(HTTP/HTTPS 路径)在每次执行时获取。script-update-interval 为正数,或恰好是 -1 时,Chute 还会每 10 分钟用条件 HEAD 请求轮询该 URL。这个值只是一个开关,不是周期:不管写多少,轮询都是每 10 分钟一次。0(默认值)和其他任何负值都不开启轮询。

模块脚本集成

脚本也可以在模块文件(.sgmodule)的 [Script] 段中定义。

S. Smart Rabbit LLC © All Rights Reserved            updated 2026-09-29 21:57:05

本页为英文版的翻译。如内容有出入,以英文版为准。

results matching ""

    No results matching ""