JavaScript 脚本
Chute 支持 JavaScript 脚本,用于高级请求/响应修改、自定义规则匹配、DNS 解析和定时任务。脚本使用 Apple 的 JavaScriptCore 引擎,遵循 Surge 兼容的脚本 API。
脚本在配置文件的 [Script] 部分中定义。
配置
[Script]
MyScript = type=http-request, script-path=/path/to/script.js, pattern=^https?://example\.com, requires-body=true, max-size=262144, timeout=10, argument=myArg, debug=true
CronJob = type=cron, script-path=/path/to/cron.js, cron-expression=* * * * *, wake-system=true
脚本参数
| 参数 | 必填 | 默认值 | 描述 |
|---|---|---|---|
type |
是 | http-request |
脚本触发类型(见下文) |
script-path |
是 | — | JS 脚本的本地文件路径或 HTTP(S) URL |
pattern |
否 | (匹配所有) | 过滤脚本触发时机的 URL 正则表达式 |
requires-body |
否 | 自动检测 | 强制脚本接收完整的请求/响应 Body |
max-size |
否 | 131072 (128KB) | 需要访问 Body 的脚本的最大 Body 大小(字节) |
timeout |
否 | 5.0 秒 | 每个脚本的执行超时时间 |
argument |
否 | — | 自定义字符串参数,在 JS 中以 $argument 访问 |
debug |
否 | false | 跳过编译缓存以方便调试 |
cron-expression |
否 | — | Cron 调度表达式(仅 cron 类型) |
wake-system |
否 | false | 唤醒系统以执行 cron 脚本(仅 iOS) |
enable |
否 | true | 启用或禁用此脚本 |
script-update-interval |
否 | 0 | 远程脚本更新的间隔秒数(-1 = 从不更新,0 = 仅启动时更新) |
脚本类型
| 类型字符串 | 枚举 | 描述 |
|---|---|---|
http-request |
HTTP Request | 在请求发送到上游之前拦截并修改 HTTP 请求 |
http-response |
HTTP Response | 在响应返回给客户端之前拦截并修改 HTTP 响应 |
http-request-before-send |
HTTP Request Before Send | 在完整 Body 收集后、发送前修改请求 |
rule |
Rule | 自定义规则匹配逻辑 |
dns |
DNS | 自定义 DNS 解析 |
cron |
Cron | 定时/计划脚本 |
event |
Event | 系统事件处理(例如 network-changed) |
Body 自动检测:如果脚本源码中包含
$request.body或$response.body,Body 将自动提供,最大不超过max-size。使用requires-body=true可强制此行为。
JavaScript API 参考
脚本在沙盒化的 JavaScriptCore 环境中运行,以下全局对象可用。
$request(只读)
可用范围:http-request、http-response、http-request-before-send、rule
| 属性 | 类型 | 描述 |
|---|---|---|
.url |
String | 完整请求 URL |
.method |
String | HTTP 方法(GET、POST 等),DNS 查询时为 QUERY |
.headers |
Object | 请求头键值对 |
.body |
String 或 null | 请求 Body(UTF-8 解码) |
.hostname |
String | 目标主机名 |
.destPort |
Number | 目标端口 |
.processPath |
String | 发起请求的进程路径(仅 macOS) |
.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 | 响应头键值对 |
.body |
String 或 null | 响应 Body(UTF-8 解码) |
$done(value) — 完成处理函数
必须在脚本结束时恰好调用一次,以表示脚本完成。脚本执行将阻塞,直到 $done() 被调用或超时到期。
$done({}) // 直通 — 不做任何修改
$done() // 中止连接
$done({matched: true}) // 规则匹配结果(仅 rule 脚本)
$done({address: "1.2.3.4"}) // DNS 结果(仅 dns 脚本)
HTTP Request 脚本返回值:
$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 直接向客户端返回合成响应,而不联系上游服务器。这对于阻止请求、Mock API 或返回缓存内容非常有用。
HTTP Response 脚本返回值:
$done({
status: 200, // 修改状态码
headers: {"X-Custom": "value"}, // 修改响应头
body: "new response body", // 修改响应 Body
url: "https://other.example.com" // 触发 302 重定向
})
当提供 url 时,Chute 返回一个到给定 URL 的 302 重定向,替代原始响应。
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: "8.8.8.8"}) // 转发到特定 DNS 服务器
$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)
回调签名:callback(error, response, data)
error:错误字符串或nullresponse:{status: Number, headers: Object}或nulldata:UTF-8 字符串响应 Body 或null
$persistentStore — 键值存储
持久化的键值存储,在脚本和进程重启后仍然保留。底层使用 NSUserDefaults。
$persistentStore.write(data, key) // 存储一个值
$persistentStore.read(key) // 读取一个值
$persistentStore.remove(key) // 删除一个值
$notification — 本地通知
发送本地系统通知。
$notification.post("Title", "Subtitle", "Notification body text")
$network — 网络信息
只读的网络状态信息。
$network.dns // DNS 服务器 IP 数组
$network.wifi // {ssid: "WiFiName", bssid: "aa:bb:cc:dd:ee:ff"}
$environment — 运行时信息
$environment.system // "iOS" 或 "macOS"
$environment.appVersion // 应用版本字符串
$utils — 工具函数
$utils.geoip("1.2.3.4") // 国家代码(例如 "US")
$utils.ipasn("1.2.3.4") // ASN 号码(例如 "13335")
$utils.ungzip(data) // 解压 gzip 数据
$klne — 代理控制 API
从脚本中控制代理运行时。
$klne.policyGroups // 获取所有策略组
$klne.selectPolicy("Group", "Proxy") // 切换策略组中的策略
$klne.getActiveConnections() // 列出活跃连接
$klne.closeConnection("id") // 关闭连接
$klne.flushDNS() // 清除 DNS 缓存
$klne.startURLTest("Group") // 对策略组触发 URL 测试
$klne.reloadConfiguration() // 重新加载所有配置
$klne.setOutboundMode("rule") // 设置模式:"global"、"proxy"、"direct"、"rule"
$klne.setHTTPCaptureEnabled(true) // 启用/禁用 MITM
$script — 脚本元数据
$script.name // 配置中的脚本名称
$script.type // 脚本类型字符串
$script.startTime // 纪元时间戳
每次执行的全局变量
以下变量在每次脚本执行时注入,并且特定于某些脚本类型。
$argument — 脚本参数
来自脚本配置中 argument= 参数的字符串值。可用范围:http-request、http-response、http-request-before-send、rule、dns、cron。
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 脚本)
有关触发事件的信息。目前仅支持 network-changed。
console.log("Event: " + $event.name) // "network-changed"
console — 日志
console.log("Debug message") // 详细日志
console.warn("Warning message") // 警告日志
console.error("Error message") // 带有 [JS-ERROR] 前缀的警告日志
setTimeout(fn, seconds) — 定时器
安排函数在延迟后执行。
setTimeout(function() {
console.log("Delayed execution")
}, 2.5) // 2.5 秒
$script(subScriptPath) — 子脚本加载器
加载并执行另一个 JavaScript 文件。
$script("/path/to/helper.js")
$script("https://example.com/remote-script.js")
// 使用 $persistentStore 在脚本之间传递数据
脚本类型详解
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
HTTP 请求发送前脚本
在完整的请求 Body 收集完成后、即将发送到上游之前执行。适用于修改 POST/PUT 请求 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 解析
}
Cron 脚本
使用 Cron 表达式进行定时执行。最小间隔为 60 秒。
[Script]
HourlyTask = type=cron, script-path=hourly.js, cron-expression=0 * * * *
支持简化的 Cron 语法(例如 */30 * * * * 表示每 30 分钟,支持逗号分隔的值)。
Event 脚本
由系统事件触发。目前支持 network-changed 事件(在 Wi-Fi 或蜂窝网络变更时触发)。
[Script]
NetChange = type=event, script-path=network-changed.js
$event 对象可用:
$event.name // "network-changed"
实用示例
重定向移动设备
一个根据 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
// network-switch.js
if (!$network.wifi.ssid) {
// 在蜂窝网络上 — 使用低流量策略组
$klne.selectPolicy("MainGroup", "LowDataProxy")
console.log("Switched to cellular profile")
} else if ($network.wifi.ssid === "Office") {
$klne.selectPolicy("MainGroup", "DIRECT")
console.log("Switched to office profile")
}
定期健康检查
一个每 30 分钟检查代理健康状况的 cron 脚本:
[Script]
HealthCheck = type=cron, script-path=health-check.js, 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()
用外部数据丰富 API 响应
一个通过调用辅助 API 来丰富用户数据的 http-response 脚本:
[Script]
EnrichUsers = type=http-response, script-path=enrich.js, pattern=^https://api\\.example\\.com/users, requires-body=true
// enrich.js
var users = JSON.parse($response.body)
var pending = users.length
if (pending === 0) { $done({}) }
users.forEach(function(user, index) {
$httpClient.get("https://internal-api.example.com/avatar/" + user.id,
function(error, resp, data) {
if (!error && resp.status === 200) {
users[index].avatar = JSON.parse(data).url
}
pending--
if (pending === 0) {
$done({body: JSON.stringify(users)})
}
}
)
})
执行模型
- 所有脚本在专用的串行队列上运行,以保证线程安全。
- 每次脚本执行都有按脚本设置的超时时间;如果在超时时间内未调用
$done(),该脚本将被视为直通。 - 使用 JSContext 池(大小 3)以提高性能;上下文在执行之间被重用。
- 脚本编译默认缓存;使用
debug=true可绕过缓存。 - 在 iOS/tvOS 上,脚本受 10MB 内存限制;在 macOS 上为 512MB。
- 远程脚本(HTTP/HTTPS 路径)在启动时获取,并可选择按
script-update-interval秒重新获取。
模块脚本集成
脚本也可以在 Module 文件(.sgmodule)的 [Script] 部分中定义。