> 原始 Markdown 孪生体（构建期从源 Markdown 生成）。渲染页：https://docs.gatellm.io/zh-CN/howto/write-script-transform · 文档索引：https://docs.gatellm.io/zh-CN/llms.txt


# 写第一个脚本变换

GateLLM 内置 **JavaScript 脚本引擎**（基于 QuickJS），可对请求和响应的 JSON body 做自定义变换。脚本运行在沙箱中，有严格资源限制，安全且可控。

本章带你写第一个脚本：基本结构 → 挂载方式 → 配置方式 → Context API 简介。完整 API 表见 [脚本 API 参考](/zh-CN/reference/scripting-api.md)。

## 什么时候用脚本

`request_payload` 覆盖约 90% 的简单场景（简单字段增删改），`switch-route` 处理纯路由决策（检测图片/联网→切模型），脚本处理剩下的复杂逻辑（跨字段计算、响应脱敏）。完整决策表与选型见 [请求改写与路由：怎么选](/zh-CN/practices/routing-and-transform.md)。

> ⚠️ **纯路由决策优先用 `switch-route`**：「检测图片/联网→切模型」这类纯路由用原生 `switch-route` 规则，零脚本开销，且**支持跨协议切换**（见 [把带图请求切到视觉模型](/zh-CN/howto/route-image-requests-to-vision-model.md)）。脚本路径已默认开启懒投影，不再因整段物化 body 而 OOM；但 `script_error_mode = log-and-continue` 下脚本抛错会被**静默跳过**，故脚本应保证不抛错。仅当判定逻辑超出 `switch-route` 结构检查（字段存在 / 小值相等）能表达的范围时，才用 before 槽脚本（读客户端原始格式）。完整反模式与耗时参考见 [脚本性能与内存](/zh-CN/practices/script-performance.md)。

## 快速开始

### 基本结构

每个脚本定义一个 `transform` 函数，接收 `body` 和 `context`，**必须返回 body**：

```javascript
function transform(body, context) {
    // 读取和修改 body
    // 读取 context 获取元数据（只读）
    return body;  // 必须！
}
```

| 参数 | 可修改 | 说明 |
|------|--------|------|
| `body` | 是 | JSON 请求体或响应体 |
| `context` | 否 | 请求/响应元数据（模型名、headers 等） |

### 挂载方式

脚本可以挂在**上游**或**模型**上：

| 级别 | 作用范围 | 适用场景 |
|------|---------|---------|
| 上游 | 经过此上游的所有模型 | 协议适配、公共字段注入 |
| 模型 | 仅此模型 | 模型特定调整、参数调优 |

### 配置方式

**内联脚本**（短脚本直接写在输入框里）——控制台 → 模型详情（或上游）→ **请求/响应脚本** → 在对应槽（`request_transform_after` / `request_transform_before` / `response_transform`）黏贴：

```javascript
function transform(body, context) { body.temperature = 0.7; return body; }
```

**文件脚本**（较大脚本引用外部 `.js` 文件）：控制台界面的脚本编辑器是**纯内联**的，没有「选文件」控件——`{file}` 引用无法在 UI 里创建，只能通过 Console API（PUT 上游 / 模型）带 `request_transform_before: {"file": "scripts/my-transform.js"}` 设置；UI 会原样保留已设置的 `{file}` 引用，直到你在该槽输入内联内容覆盖它。

## Context 对象

`context` 提供当前请求/响应的元数据，**只读**：

### 请求和响应阶段均可用

| 字段 | 类型 | 说明 |
|------|------|------|
| `context.clientModel` | string | 客户端看到的模型名 |
| `context.upstreamModel` | string | 发送给上游的模型 ID |
| `context.upstreamName` | string | 上游分组名 |
| `context.sourceProtocol` | string | 客户端协议（如 `"OpenAIChatCompletions"`） |
| `context.targetProtocol` | string | 上游协议（如 `"Anthropic"`） |
| `context.stream` | boolean | 是否流式请求 |
| `context.headers` | object | 请求 headers（敏感头已过滤） |
| `context.query` | object | URL 查询参数 |
| `context.requestId` | string | 唯一请求 ID |
| `context.clientIp` | string | 客户端 IP |
| `context.accessKeyName` | string | 访问密钥名 |
| `context.accessKeyGroup` | string | 访问密钥分组 |

### 仅响应阶段可用

| 字段 | 类型 | 说明 |
|------|------|------|
| `context.responseStatus` | number | 上游 HTTP 状态码 |
| `context.responseHeaders` | object | 上游响应 headers |
| `context.elapsedMs` | number | 耗时（毫秒） |

> **安全**：`authorization`、`cookie`、`x-api-key` 等敏感 headers 在脚本可见之前已被移除。

完整字段表见 [脚本 API 参考](/zh-CN/reference/scripting-api.md)。

## 请求脚本的两个槽（before / after）

请求侧脚本不再是一个脚本加位置开关，而是**两个独立槽**，各自可空、可同时配置：

| 槽 | 字段 | 运行时机 | 看到的 body | useModel 能力 |
|----|------|---------|-------------|---------------|
| 翻译后 | `request_transform_after` | 协议翻译**之后** | 上游协议格式 | 仅同协议切换 |
| 翻译前 | `request_transform_before` | 协议翻译**之前** | 客户端原始格式 | 支持跨协议切换 |

- **`request_transform_after`**：在协议转换后运行，body 已是上游格式。适合简单的字段修改、上游方言改写。
- **`request_transform_before`**：在协议转换前运行，body 是客户端原始格式。适合需要检测客户端输入（如图片、搜索、plan 模式标记）并切换路由的场景。
- **两个槽可同时配置**：同一路由的 before 与 after 各自独立合并（model 覆盖 upstream），两个脚本都会运行——before 先跑（可触发跨协议 立即 `useModel` 切换），after 后跑。注意：一旦 before 槽触发了 立即 `useModel` 切换路由，**原路由的 after 槽不再执行**（整套 pipeline 换成目标路由的）；after 槽声明的 `useModel` 是同协议 延后切换。`response_transform` 无槽概念，保持单字段。

## 执行顺序

before 槽与 after 槽各自独立运行；同一路由两者都配时，before 先在翻译前阶段跑，after 在翻译后阶段跑。下面分别给出两槽在管线中的位置。

### 请求侧 — after 槽（翻译后）

```text
客户端请求 → 协议转换 → 自动归一化 → request_payload 规则 → 上游脚本 → 模型脚本 → 缓存注入 → 发送到上游
```

### 请求侧 — before 槽（翻译前）

```text
客户端请求 → 上游脚本 → 模型脚本 → (useModel swap) → 协议转换 → 自动归一化 → request_payload 规则 → 缓存注入 → 发送到上游
```

### 响应侧（仅非流式）

```text
上游响应 → 响应脚本 → 反向协议转换 → 返回客户端
```

> 流式请求的响应脚本暂不支持。

## 第一个示例：覆盖模型名

```javascript
function transform(body, context) {
    body.model = context.upstreamModel;
    return body;
}
```

挂在模型或上游的 `request_transform_after` 字段。请求发送到上游前，body 的 model 字段被替换为上游真实模型 ID。

## 切换模型：context.useModel

脚本可以动态切换请求路由到的模型：

```javascript
function transform(body, context) {
    // 检测多模态输入（图片），切换到多模态模型
    const msgs = body.messages || [];
    const hasImage = msgs.some(m =>
        Array.isArray(m.content) && m.content.some(c => c.type === "image_url"));
    if (hasImage) {
        context.useModel("gpt-4o-vision");
    }
    return body;
}
```

**`context.useModel` vs `body.model =`**：

| 操作 | 效果 |
|------|------|
| `body.model = "X"` | 仅改请求体的 model 字段，路由不变 |
| `context.useModel("X")` | 完整路由切换（base_url、协议、凭证一起换） |

用 `body.model` 改同端点内的模型名；用 `context.useModel` 做跨端点/跨协议切换。

## 通过 Console API 测试

部署前先测试脚本（`$CONSOLE_TOKEN` 的获取见 [脚本配置 → 脚本测试](/zh-CN/reference/scripting-config.md#script-testing)）：

```bash
curl -X POST http://localhost:7890/console/api/scripts/test \
  -H "Authorization: Bearer $CONSOLE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "script": "function transform(body, context) { body.model = context.upstreamModel; return body; }",
    "body": {"model": "gpt-4o", "messages": []},
    "context": {
      "clientModel": "gpt-4o",
      "upstreamModel": "gpt-4o-2024-05-13",
      "upstreamName": "openai",
      "sourceProtocol": "OpenAIChatCompletions",
      "targetProtocol": "OpenAIChatCompletions",
      "stream": false
    }
  }'
```

响应：
```json
{
  "result_body": {"model": "gpt-4o-2024-05-13", "messages": []},
  "execution_time_ms": 0.12
}
```

## 错误处理

### script_error_mode

| 模式 | 行为 | 适用场景 |
|------|------|---------|
| `log-and-continue`（默认） | 脚本报错时记录 warn 日志，请求以原始 body 继续 | 脚本失败不应阻断请求 |
| `log-and-reject` | 脚本报错时拒绝请求，返回网关错误 | 脚本正确性至关重要（如安全脱敏） |

### 提前返回时必须返回 body

```javascript
function transform(body, context) {
    // ✅ 正确
    if (!body.messages) return body;

    // ❌ 错误：return 无值 → 脚本静默失败
    if (!body.messages) return;

    body.messages[0].content = "modified";
    return body;
}
```

## 常见问题

**Q：脚本在控制台界面哪里配？**
上游详情页和模型详情页都有「请求脚本」「响应脚本」输入框。也可以直接通过 Console API 设置。

**Q：脚本改了 body.model，为什么路由没变？**
`body.model` 只改请求体的字段，不改路由。要切换路由用 `context.useModel("模型名")`。

**Q：流式请求的响应脚本生效吗？**
暂不支持流式响应的脚本。请求脚本对流式请求有效，但响应脚本仅在非流式时生效。

**Q：脚本报错了但请求正常通过？**
默认的 `log-and-continue` 模式下，脚本报错不会阻断请求。查看 warn 日志了解报错原因。如需脚本必须成功，改用 `log-and-reject`。

**Q：context.headers 里为什么没有 Authorization？**
安全设计：`authorization`、`cookie`、`x-api-key` 等敏感 headers 在脚本可见之前已被移除，防止凭证泄露。

**Q：Before 和 After 脚本位置怎么选？**
- 只改上游格式 body 的字段 → After（默认）
- 需要检测客户端原始格式（如不同协议的图片字段不同）且判定超出 `switch-route` 能表达的范围 → Before
- 「检测图片/联网→切模型」这类纯路由决策（含跨协议）→ 优先用原生 `switch-route` 规则，零脚本开销
- Before 槽的 `context.useModel` 也能跨协议切换，但仅当切换逻辑复杂到 `switch-route` 表达不了时才值得写脚本

**下一步**：[脚本 API 参考](/zh-CN/reference/scripting-api.md) 看完整内置函数表；[脚本配置](/zh-CN/reference/scripting-config.md) 看运行限制与配置；[用脚本做协议翻译修正](/zh-CN/howto/script-protocol-translate.md) 看实战示例。
