脚本 API 参考

RelayCore 内嵌 Deno/V8 运行时,通过 globalThis 全局钩子函数拦截和修改流量。脚本引擎支持 JavaScript 和 TypeScript。

线路语义(0.13.1+):在 onResponseHeaders / onRequestHeaders 里 return flow 只表示改头并继续流式传输上游 body,不会把响应裁成空体。异步钩子会泵事件循环;读 body 有约 1 MiB 预算与超时,超时后放行原 body,避免卡死整引擎。改头优先用同步钩子;需要改 body 时再在 body 钩子里写回 flow.layer.data.*.body。

生命周期钩子

脚本通过定义 globalThis 上的命名函数注册钩子。RelayCore 1.0 提供 11 个钩子,覆盖 HTTP、WebSocket 和连接生命周期:

钩子触发时机签名
onRequestHeaders请求头接收后(ctx, flow) => flow | undefined
onRequest请求体就绪后,转发前(body, flow) => flow | undefined
onResponseHeaders响应头接收后(ctx, flow) => flow | undefined
onResponse响应体就绪后(body, flow) => flow | undefined
onWebSocketMessageWebSocket 帧到达(ctx, flow, message) => message | "DROP" | undefined
onWebSocketStartWebSocket 握手完成(ctx, flow) => void
onWebSocketEndWebSocket 正常关闭(ctx, flow, code, reason) => void
onWebSocketErrorWebSocket 协议/IO 错误(ctx, flow, error) => void
onConnect客户端建立 TCP 连接(ctx, conn) => {drop:true} | undefined
onDisconnect连接关闭(ctx, conn, stats) => void
onError任何钩子抛错时(ctx, flow, error, stage) => void

onWebSocketMessage 必须返回消息、字符串 DROP 或空。把 flow 返回去是改头钩子的写法,这一次调用会失败。WebSocket flow 的 layer 类型是 WebSocket,不是 Http。抛错只给这一条 flow 打上 script-error;下一条 flow 不受影响,再加载一份脚本就会换掉钩子。

Flow 对象

每个钩子收到的 flow 参数包含当前流的完整信息:

// Flow 结构(脚本侧视角)
{
  id: string,           // UUID
  start_time: string,    // ISO 8601
  tags: string[],       // 标签列表

  network: {
    client_ip: string,
    server_ip: string,
    protocol: "TCP" | "UDP",
    tls: boolean,
    sni: string | null,
  },

  // 规则桥接(RelayCore 独家)
  matched_rules: string[],       // 命中的规则 ID 列表
  rule_variables: Record,  // SetVariable 设置的变量

  // 应用层协议数据
  layer: {
    type: "Http" | "WebSocket",
    data: {
      // type = "Http" 时有:
      request: {
        method: string,
        url: string,        // 完整 URL
        version: string,    // "HTTP/1.1" | "HTTP/2.0"
        headers: [string, string][],
        body: {
          encoding: string,
          size: number,
          content: string,  // encoding 为 utf-8 或 base64
        } | null,
        cookies: [string, string][],
      },
      response: {
        status: number,
        status_text: string,
        version: string,
        headers: [string, string][],
        body: { encoding, size, content } | null,
        cookies: [string, string][],
      } | null,
    }
  }

内置工具库 relay.*

全局对象 relay 提供常用工具函数:

relay.uuid()

const id = relay.uuid();  // "550e8400-e29b-41d4-a716-446655440000"

relay.hash(algorithm, data)

const sha = relay.hash("sha256", "hello");
// 支持: sha1, sha256, sha512, md5
// 返回 hex 字符串

relay.base64.encode / relay.base64.decode

const b64 = relay.base64.encode("hello world");   // "aGVsbG8gd29ybGQ="
const raw = relay.base64.decode(b64);              // Uint8Array

relay.json.parseSafe / relay.json.stringifyPretty

const obj = relay.json.parseSafe('{"a":1}');      // {a:1} 或 undefined
const txt = relay.json.stringifyPretty(obj);          // 2 空格缩进

relay.env(name)

读取白名单内的环境变量,需 CLI 参数 --script-env-allow 显式声明:

// CLI: relay-core run --script-env-allow=BACKEND_HOST
const host = relay.env("BACKEND_HOST");  // "api.example.com"
const miss = relay.env("SECRET");        // undefined(未在白名单)

relay.fetch(url)

发起一次 HTTP GET。默认关闭;传 --script-fetch-allow 才会启用。没有 method / headers / body 参数,返回 { ok, status, body } 或 { ok: false, error }。

// CLI: relay-core run --script-fetch-allow=auth.internal
const resp = relay.fetch("http://auth.internal/verify");
if (!resp.ok) {
  throw new Error(resp.error || ("HTTP " + resp.status));
}
安全控制说明
默认关闭不传 --script-fetch-allow 时 relay.fetch 返回 script fetch disabled
白名单逗号分隔主机名;* 表示任意主机。须在加载脚本之前设置
防递归打到本代理监听端口的 localhost / 127.0.0.1 / ::1 会被拒绝
并发与超时引擎内并发上限 8、单次超时 5s,没有对应的 CLI 参数

RelayBody

body 钩子的第一个参数是 RelayBody,不是 relay.readBody。超预算时 truncated 为 true,text() / json() 会抛错。

globalThis.onResponse = async (body, flow) => {
  const text = await body.text();
  flow.layer.data.response.body = { encoding: "utf-8", size: text.length, content: text };
  return flow;
};

全局状态 sharedState

globalThis.sharedState 是同一引擎实例内所有钩子共享的键值存储:

// 在请求头阶段记下开始时间
globalThis.onRequestHeaders = (ctx, flow) => {
  sharedState.set(flow.id, { start: Date.now() });
  return flow;
};

// 在 onResponse 中读取
globalThis.onResponse = (body, flow) => {
  const meta = sharedState.get(flow.id);
  if (meta) {
    console.log(`Flow ${flow.id} took ${Date.now() - meta.start}ms`);
    sharedState.delete(flow.id);
  }
};

日志分级 console.*

脚本中 5 级日志分别映射到 Rust 的 tracing:

console.log("请求到达");     // tracing::info!    — 运行时可见
console.info("状态正常");     // tracing::info!
console.warn("限速触发");     // tracing::warn!    — 需关注
console.error("鉴权失败");    // tracing::error!   — 必定可见
console.debug("变量值: " + x); // tracing::debug!  — RUST_LOG=debug 才可见

连接级钩子

// 连接过滤 — onConnect 返回 {drop:true} 直接拒绝连接
globalThis.onConnect = (ctx, conn) => {
  // conn: { id, client_addr, server_addr, tls_sni }
  if (String(conn.client_addr).startsWith("192.168.1.")) {
    return { drop: true };
  }
};

// 连接统计
globalThis.onDisconnect = (ctx, conn, stats) => {
  // stats: { duration_ms, bytes_sent, bytes_received, flows_count }
  console.log(`Connection ${conn.id}: ${stats.flows_count} flows`);
};

WebSocket 生命周期

globalThis.onWebSocketStart = (ctx, flow) => {
  console.log(`WS opened: ${flow.layer.data.request.url}`);
};

globalThis.onWebSocketEnd = (ctx, flow, code, reason) => {
  console.log(`WS closed: ${code} ${reason}`);
};

globalThis.onWebSocketError = (ctx, flow, error) => {
  console.error(`WS error: ${error}`);
};

规则桥接

RelayCore 独家能力——脚本可以感知规则引擎的执行结果:

globalThis.onResponse = (body, flow) => {
  // 读取命中的规则
  if (flow.matched_rules.includes("rule-block-tracking")) {
    flow.layer.data.response.headers.push(["X-Blocked-By", "RelayCore"]);
  }

  // 读取规则引擎 SetVariable 设置的变量
  const count = flow.rule_variables["rate_limit_count"];
  if (count && parseInt(count) > 100) {
    console.warn(`Rate limit exceeded: ${count}`);
  }
};

脚本管理与部署

CLI 命令

# 通过 HTTP API 加载脚本
curl -X POST http://127.0.0.1:8082/api/v1/script \
  -H "Content-Type: application/json" \
  -d '{"script": "globalThis.onRequestHeaders = (ctx, flow) => { console.log(flow.layer.data.request.url); }"}'

# MCP 工具 set_script,参数是脚本文本,不是文件路径

# Bundle 模式:使用 npm 包
relay-core scripts init my-project      # 脚手架
cd my-project && npm install jwt-decode  # 安装依赖
relay-core scripts build                 # esbuild 打包
relay-core scripts dev                   # build + 热上传 + watch

Bundle 模式与 npm 生态

RelayCore 的 V8 isolate 不内置 npm 解析器。要通过 esbuild 将脚本和依赖打包为单文件:

// src/main.ts — 正常 import npm 包
import jwtDecode from "jwt-decode";
import { XMLParser } from "fast-xml-parser";

globalThis.onRequestHeaders = (ctx, flow) => {
  const auth = flow.layer.data.request.headers.find(h => h[0] === "authorization");
  if (auth) {
    const payload = jwtDecode(auth[1]);
    console.log("JWT subject:", payload.sub);
  }
};

// relay-core scripts build → dist/bundle.js(单文件 IIFE)
// 直接加载 bundle.js 即可

脚本环境变量

# 启动时允许脚本读取的环境变量
relay-core run --script-env-allow API_TOKEN,DEBUG

可观测性

脚本执行指标自动上报到 Prometheus:

指标说明
relay_core_script_hook_duration_us{hook}单次钩子执行耗时
relay_core_script_hook_invocations_total{hook}钩子累计调用次数
relay_core_script_hook_errors_total{hook}钩子累计错误次数
relay_core_script_fetch_totalfetch 调用计数(无标签)
relay_core_script_fetch_rejected_total被拒绝的 fetch
relay_core_script_env_access_totalenv 访问计数(不记录变量名或值)