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


# 用脚本做响应脱敏

响应脚本（`response_transform`）对上游返回的非流式响应体做后处理：脱敏 PII、注入请求元数据、剥离内容降成本。本章给几个实战示例。

前置：先读 [写第一个脚本变换](/zh-CN/howto/write-script-transform.md) 了解基本结构、挂载方式、Context API。

> 流式响应的脚本暂不支持。`response_transform` 仅在非流式请求时生效。

> **响应脚本看到的 body 是上游协议格式**。响应管线的顺序是：上游响应 → **响应脚本** → 反向协议转换。也就是说，脚本拿到的是**上游返回的原始响应体**（OpenAI 上游是 `choices`，Anthropic 上游是顶层 `content`，Google 上游是 `candidates`），网关把它翻译回客户端协议是在脚本跑完之后。因此下面的示例都按上游是 **OpenAI**（`choices`）写的——上游是 Anthropic / Google 时 body 形状不同，需按协议分支处理，否则脚本直接抛错。

## 例 1：响应中脱敏 PII

把响应里的邮箱地址替换为 `[REDACTED]`（**上游为 OpenAI 时**，遍历 `choices`）：

```javascript
function transform(body, context) {
    // 响应体为上游协议格式；本例基于 OpenAI 上游（body.choices）
    if (!Array.isArray(body.choices)) return body;
    for (let i = 0; i < body.choices.length; i++) {
        if (body.choices[i].message.content) {
            body.choices[i].message.content = regex_replace(
                body.choices[i].message.content,
                "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}",
                "[REDACTED]"
            );
        }
    }
    return body;
}
```

挂在模型或上游的 `response_transform` 字段。响应返回客户端前，所有邮箱地址都被替换。

上游是 Anthropic 时响应体形状不同（顶层 `content`，无 `choices`），同样的脱敏要遍历 `body.content`：

```javascript
function transform(body, context) {
    // Anthropic 上游：content 是块数组
    if (!Array.isArray(body.content)) return body;
    for (const block of body.content) {
        if (block.type === "text" && block.text) {
            block.text = regex_replace(
                block.text,
                "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}",
                "[REDACTED]"
            );
        }
    }
    return body;
}
```

> 安全建议：脱敏脚本建议配 `script_error_mode = log-and-reject`，确保脚本失败时不放过未脱敏的内容。

## 例 2：在响应中添加请求元数据

把网关的请求 ID 和耗时注入到响应的 `usage` 字段：

```javascript
function transform(body, context) {
    if (body.usage) {
        body.usage.gateway_request_id = context.requestId;
        body.usage.gateway_elapsed_ms = context.elapsedMs;
    }
    return body;
}
```

挂在 `response_transform`。响应里的 `usage` 字段会多出 `gateway_request_id` 和 `gateway_elapsed_ms`，便于客户端排查问题。

## 例 3：剥离图片内容（降低成本）

把响应里的图片块过滤掉，保留文字。注意遍历的是 `choices[].message.content`（**上游为 OpenAI 时**），不是顶层 `messages`——响应体里没有顶层 `messages` 字段，遍历它会在默认 `log-and-continue` 下**静默抛错、过滤完全不生效**：

```javascript
function transform(body, context) {
    // 响应体为上游协议格式；本例基于 OpenAI 上游（body.choices）
    if (!Array.isArray(body.choices)) return body;
    for (const choice of body.choices) {
        const content = choice.message?.content;
        if (Array.isArray(content)) {
            choice.message.content = content.filter(block => block.type !== "image_url");
        }
    }
    return body;
}
```

> 上游是 Anthropic 时遍历 `body.content`（块数组），过滤 `block.type === "image"`，写法同例 1 的分支版。
>
> 简单场景建议用 `filter-content-types` request_payload 规则代替脚本，性能更好。

## 例 4：响应注入告警标记

当上游响应状态码不是 200 时，在响应里注入一个告警字段：

```javascript
function transform(body, context) {
    if (context.responseStatus >= 400) {
        body._gateway_warning = {
            reason: "upstream_error",
            status: context.responseStatus,
            elapsed_ms: context.elapsedMs
        };
    }
    return body;
}
```

## Context 响应阶段可用字段

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

加上请求阶段也有的字段（`clientModel`、`upstreamModel`、`sourceProtocol`、`accessKeyName`、`accessKeyGroup`、`requestId` 等）。完整表见 [脚本 API 参考](/zh-CN/reference/scripting-api.md)。

> **安全**：`authorization`、`cookie`、`x-api-key` 等敏感 headers 在脚本可见之前已被移除，`context.responseHeaders` 也会过滤敏感头。

## 内置函数参考

响应脱敏常用的内置函数：

```javascript
// 正则替换
regex_replace("foo123bar", "[0-9]+", "N");  // "fooNbar"

// 字符串操作
str_contains("hello world", "world"); // true
str_replace("foo-bar", "-", "_");    // "foo_bar"
str_starts_with("hello", "he");     // true
str_trim("  hello  ");              // "hello"

// 编码
base64_encode("hello");    // "aGVsbG8="
base64_decode("aGVsbG8="); // "hello"
sha256("hello");           // "2cf24dba5fb0..."

// JSON 操作
json_encode(body.choices);  // 序列化为字符串（OpenAI 上游响应体）
json_parse(jsonStr);        // 从字符串解析
```

完整内置函数表见 [脚本 API 参考](/zh-CN/reference/scripting-api.md)。

## 错误处理

### script_error_mode

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

> 脱敏脚本建议配 `log-and-reject`，避免脚本失败时未脱敏内容流到客户端。

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

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

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

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

## 常见问题

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

**Q：脚本改了 body.choices[0].message.content 但客户端没看到改后内容？**
检查：① 请求是否流式（流式不跑响应脚本）；② `script_error_mode` 是否为 `log-and-continue`（脚本失败时静默继续）；③ 脚本是否挂在正确的模型/上游上。

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

**Q：脱敏脚本失败时怎么保证安全？**
配 `script_error_mode = log-and-reject`。脚本失败时拒绝请求，确保未脱敏内容不流到客户端。

**Q：响应体太大，脚本跑得慢？**
脚本执行的开销主要来自 JSON 序列化/反序列化。body 越大越慢。简单字段操作优先用 `request_payload`（无序列化开销）。避免不必要的 `deep_clone` / `json_encode` + `json_parse`。

**下一步**：[脚本 API 参考](/zh-CN/reference/scripting-api.md) 看完整内置函数表；[脚本配置](/zh-CN/reference/scripting-config.md) 看运行限制与配置；[审计与安全配置](/zh-CN/reference/audit-and-security-config.md) 看其他安全设置。
