脚本开发指引
本指南涵盖使用 RelayCore 脚本引擎的常用模式、最佳实践和调试技巧。
快速开始
// script.js — 最简脚本
globalThis.onRequest = (ctx, flow) => {
console.log(`${flow.layer.data.request.method} ${flow.layer.data.request.url}`);
}; 通过 HTTP API 加载:
curl -X POST http://127.0.0.1:8082/api/v1/script \
-H "Content-Type: application/json" \
-d '{"script": "globalThis.onRequest = (ctx, flow) => { console.log(flow.layer.data.request.url); }"}' 0.5.x API 备忘
- Flow 结构:HTTP 请求/响应在
flow.layer.data.request/flow.layer.data.response(不是flow.request) - sharedState:使用
get(key)/set(key, value)/delete(key),不是普通对象属性 - 规则变量:脚本在
onRequestHeaders阶段先于规则执行;读取flow.rule_variables请在onRequest或更晚钩子 - Mock / 直接响应:0.5.1 脚本尚不支持
{ action: "respond" },请用规则MockResponse(见规则指南) - 沙箱限制:无
URL全局对象,解析路径请用字符串 helper(见 §2)
常用模式
1. 认证注入
为所有到内部 API 的请求自动注入认证头:
globalThis.onRequestHeaders = (ctx, flow) => {
if (flow.layer.data.request.url.includes("/api/")) {
const existing = flow.layer.data.request.headers.find(h => h[0] === "authorization");
if (!existing) {
flow.layer.data.request.headers.push(["authorization", "Bearer ${getToken()}"]);
}
}
return flow; // 返回修改后的 flow
};
function getToken() {
return relay.env("API_TOKEN") || "dev-token";
} 2. Mock 响应
0.5.1 中 Mock 请优先使用规则引擎 MockResponse(见规则指南 §2)。脚本侧若需匹配路径,沙箱内没有 URL 对象:
function pathname(url) {
const start = url.indexOf("://");
const slash = start >= 0 ? url.indexOf("/", start + 3) : url.indexOf("/");
return (slash >= 0 ? url.slice(slash) : "/").split("?")[0];
}
// 示例:为特定路径打 tag,配合 MockResponse / Drop 规则
globalThis.onRequestHeaders = (ctx, flow) => {
const path = pathname(flow.layer.data.request.url);
if (path === "/api/users" || path === "/api/health") {
flow.tags.push("mock-candidate");
}
return flow;
}; 3. 请求重写
修改请求目标,将 API 调用重定向到不同环境:
globalThis.onRequestHeaders = (ctx, flow) => {
// 将 prod API 调用重定向到 staging
if (flow.layer.data.request.url.includes("api.example.com")) {
flow.layer.data.request.url = flow.layer.data.request.url.replace(
"api.example.com",
"staging.example.com"
);
// 更新 Host header
flow.layer.data.request.headers = flow.layer.data.request.headers.filter(
h => h[0].toLowerCase() !== "host"
);
flow.layer.data.request.headers.push(["host", "staging.example.com"]);
}
return flow;
}; 4. 响应脱敏
自动移除敏感字段如手机号、证件号:
globalThis.onResponse = (ctx, flow) => {
if (flow.layer.type !== "Http" || !flow.layer.data.response?.body) return;
const raw = atob(flow.layer.data.response.body.content);
if (!isJson(raw)) return;
const data = relay.json.parseSafe(raw);
if (!data) return;
const sanitized = redactPersonalInfo(data);
const newBody = relay.json.stringifyPretty(sanitized);
flow.layer.data.response.body.content = btoa(newBody);
flow.layer.data.response.body.size = newBody.length;
return flow;
};
function redactPersonalInfo(obj) {
if (typeof obj !== "object" || obj === null) return obj;
if (Array.isArray(obj)) return obj.map(redactPersonalInfo);
const result = {};
for (const [k, v] of Object.entries(obj)) {
if (/phone|mobile|id_card|ssn/i.test(k)) {
result[k] = "***REDACTED***";
} else {
result[k] = redactPersonalInfo(v);
}
}
return result;
}
function isJson(s) {
try { JSON.parse(s); return true; } catch { return false; }
} 5. 限速与熔断
通过 sharedState 实现简单的请求计数和限速:
const RATE_LIMIT = 100;
const WINDOW_MS = 60_000;
globalThis.onRequest = (ctx, flow) => {
const ip = flow.network.client_ip;
const now = Date.now();
const bucket = sharedState.get("_rateLimit") || {};
const entry = bucket[ip] || { count: 0, reset: now + WINDOW_MS };
if (now > entry.reset) {
entry.count = 0;
entry.reset = now + WINDOW_MS;
}
entry.count++;
bucket[ip] = entry;
sharedState.set("_rateLimit", bucket);
if (entry.count > RATE_LIMIT) {
console.warn(`Rate limit exceeded for ${ip} (${entry.count}/${RATE_LIMIT})`);
flow.tags.push("rate-limited");
// 返回 429 请用规则 RateLimit action(见规则指南 §8)
}
return flow;
}; 6. 跨钩子上下文传递
使用 sharedState 在请求和响应之间传递数据:
globalThis.onRequest = (ctx, flow) => {
sharedState.set(flow.id, {
startTime: Date.now(),
authType: flow.layer.data.request.headers.find(h => h[0].toLowerCase() === "authorization")
? "bearer" : "none",
});
return flow;
};
globalThis.onResponse = (ctx, flow) => {
const meta = sharedState.get(flow.id);
if (!meta) return;
const duration = Date.now() - meta.startTime;
if (duration > 5000) {
console.warn(`Slow request: ${flow.layer.data.request.url} (${duration}ms, auth=${meta.authType})`);
}
sharedState.delete(flow.id);
}; 7. 连接 IP 黑名单
在 TCP 连接建立时直接拒绝:
const BLOCKED_NETS = ["10.0.0.", "192.168.254."];
globalThis.onConnect = (ctx, conn) => {
for (const net of BLOCKED_NETS) {
if (conn.client_ip.startsWith(net)) {
console.warn(`Blocked connection from ${conn.client_ip}`);
return { drop: true };
}
}
}; 8. 外部鉴权(relay.fetch)
调用外部服务验证每个请求:
globalThis.onRequest = async (ctx, flow) => {
const auth = flow.layer.data.request.headers.find(h => h[0] === "authorization");
if (!auth) return;
try {
const resp = await relay.fetch("http://auth.internal/verify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token: auth[1] }),
timeout_ms: 3000,
});
if (resp.status !== 200) {
flow.tags.push("auth-failed");
// 0.5.1:返回 401 / 阻断请配合 MockResponse 或 Drop 规则
}
} catch (e) {
console.error("Auth service unreachable:", e);
// fail-open: 允许通过
}
}; 调试技巧
日志级别
// 开发时用 console.debug,生产不可见
console.debug("Flow id:", flow.id); // RUST_LOG=relay_core_script=debug
console.log("Request handled"); // 默认可见
console.error("This should not happen!"); // 永远可见 onError 全局捕获
globalThis.onError = (ctx, flow, error, stage) => {
// stage 告诉你哪个钩子出错了
console.error(`[${stage}] ${error}`);
// 记录详细信息用于排查
const errors = sharedState.get("_errors") || [];
errors.push({
flow_id: flow?.id,
stage,
message: String(error),
time: new Date().toISOString(),
});
sharedState.set("_errors", errors);
}; Prometheus 指标
# 查看脚本执行指标
curl http://127.0.0.1:8082/api/v1/metrics/prometheus | grep script_
# 输出:script_hook_duration_us{hook="onRequest"}
# script_hook_invocations_total{hook="onRequest"} 12345 性能注意事项
- 钩子执行阻塞代理线程——单个钩子 > 10ms 会显著影响吞吐
- 避免同步大文件处理——对 > 1MB body 进行 JSON 解析会导致 OOM
- sharedState 及时清理——不清理会导致内存增长(旧条目 > 10k 时发 warning)
- relay.fetch 有并发上限——默认最大 8 个并发请求,超出会排队等待
- 引擎池独立 V8 isolate——不同引擎的 sharedState 互不可见(同 flow 保证路同引擎)
- Bundle 模式打包——用 esbuild 的
--minify可以减少脚本加载时间
测试脚本
无需启动完整代理即可在单元测试中验证脚本逻辑:
# Rust 侧已有完整的脚本集成测试
# 见 relay-core-script/tests/integration_test.rs
cargo test --package relay-core-script
# 覆盖:全部 11 钩子、relay.* 工具、relay.fetch、
# env 白名单、sharedState、onError、
# matched_rules、rule_variables