diff --git a/src/content/docs-lite/en/features.md b/src/content/docs-lite/en/features.md index 63f384b..76113d6 100644 --- a/src/content/docs-lite/en/features.md +++ b/src/content/docs-lite/en/features.md @@ -101,7 +101,7 @@ The app watches these files while it runs, and a new finding raises a system not ## Plugins -Plugins are short JavaScript files that change requests before they go to an upstream and answers before they reach the client, such as asking for answers in a chosen language or converting file paths in tool calls between WSL and Windows. They run after routing, in a sandbox inside core with no network, files or memory between requests, see placeholders instead of the keys in a request, and pass through the same protections afterwards. Two plugins ship with the app, both off until turned on. Each plugin is one file that also holds its scope, its behavior on errors and its settings. It is edited in one editor with a Settings tab, whose changes are written into the file, and a Code tab; adding a plugin opens the same editor. Routine changes are saved directly; for a plugin that may change tool calls, installing it, turning it on, changing its code and approving a changed file are confirmed in a system dialog. A plugin whose file changes outside the app stops running until the new version is approved. The page lists each plugin with its status, permissions, scope and statistics, and offers a trial run on a recent request and its log; requests changed by plugins are marked in Traffic. The API, permissions, limits and security model are described in [Plugins](/docs/lite/plugins). +Plugins are short JavaScript files that change requests before they go to an upstream and answers before they reach the client, such as asking for answers in a chosen language or converting file paths in tool calls between WSL and Windows. They run after routing, in a sandbox inside core with no network, files or memory between requests, see placeholders instead of the keys in a request, and pass through the same protections afterwards. Two plugins ship with the app, both off until turned on. A global `tw` gives plugins helpers for common edits, such as adding a line to the system prompt or replacing words in streamed answers. Each plugin is one file that also holds its scope, its behavior on errors and its settings. It is edited in one editor with a Settings tab, whose changes are written into the file, and a Code tab; adding a plugin opens the same editor. Routine changes are saved directly; for a plugin that may change tool calls, installing it, turning it on, changing its code and approving a changed file are confirmed in a system dialog. A plugin whose file changes outside the app stops running until the new version is approved. The page lists each plugin with its status, permissions, scope and statistics, and offers a trial run on a recent request and its log; requests changed by plugins are marked in Traffic. The API, permissions, limits and security model are described in [Plugins](/docs/lite/plugins). ## Settings diff --git a/src/content/docs-lite/en/plugins.md b/src/content/docs-lite/en/plugins.md index a69a062..54ab518 100644 --- a/src/content/docs-lite/en/plugins.md +++ b/src/content/docs-lite/en/plugins.md @@ -89,12 +89,12 @@ export const manifest = { export function onRequest(req, ctx) { const notes = String(ctx.settings.notes).trim(); - if (notes === "") return; // nothing to add: the request is sent as it is - req.system = req.system ? `${req.system}\n\n${notes}` : notes; - return req; + if (notes !== "") tw.appendSystem(req, notes); } ``` +Changes made to `req` take effect when the hook ends, without returning it. `tw.appendSystem` is one of the [helpers](#helpers) for common edits. + ### Manifest | Field | Required | Value | @@ -138,11 +138,11 @@ All three lists apply to request hooks and answer hooks alike, and patterns igno | `onReplyTextEnd(ctx)` | `reply.text` | In stream mode, at the end of each text block. Optional. | | `onToolCall(call, ctx)` | `reply.tool_calls` | For each tool call in the answer, once it is complete. | -**`onRequest`** receives the [request view](#the-request-view) and returns it changed, or returns nothing to leave the request as it is. Calling `reject("reason")` refuses the request, and the client receives an error that names the plugin; `reject` ends the hook by throwing, and the refusal stands even if the plugin catches what it throws. The hook runs once for each attempt to send the request to an upstream, as described in [Where plugins run](#where-plugins-run). Besides requests that generate an answer, it runs on token counts (`/v1/messages/count_tokens`, Gemini's `countTokens` and the Responses API's `input_tokens`) and on Responses compaction, which carry the same conversation, so that what a plugin removes is not sent through them either. On these, a changed model is applied but changes to the other `params` are not, because these endpoints do not accept them. A token count that the gateway estimates itself, without contacting an upstream, runs no plugin. +**`onRequest`** receives the [request view](#the-request-view). Changes made to it take effect when the hook ends, whether or not the hook returns it; returning a different object uses that object instead. A view that ends up as it came in leaves the request as it is. Calling `reject("reason")` refuses the request, and the client receives an error that names the plugin; `reject` ends the hook by throwing, and the refusal stands even if the plugin catches what it throws. The hook runs once for each attempt to send the request to an upstream, as described in [Where plugins run](#where-plugins-run). Besides requests that generate an answer, it runs on token counts (`/v1/messages/count_tokens`, Gemini's `countTokens` and the Responses API's `input_tokens`) and on Responses compaction, which carry the same conversation, so that what a plugin removes is not sent through them either. On these, a changed model is applied but changes to the other `params` are not, because these endpoints do not accept them. A token count that the gateway estimates itself, without contacting an upstream, runs no plugin. -**`onReplyText`** in block mode, the default, is called once for each text block with the whole text of the block, and the text reaches the client after the call. In stream mode it is called for each piece of streamed text and returns what to send now; returning `""` holds the text back, and `onReplyTextEnd` returns whatever is still held when the block ends. An answer that is not streamed is passed in one call, followed by `onReplyTextEnd` in stream mode. Returning nothing leaves the text unchanged. +**`onReplyText`** in block mode, the default, is called once for each text block with the whole text of the block, and the text reaches the client after the call. In stream mode it is called for each piece of streamed text and returns what to send now; returning `""` holds the text back, and `onReplyTextEnd` returns whatever is still held when the block ends. An answer that is not streamed is passed in one call, followed by `onReplyTextEnd` in stream mode. Returning nothing leaves the text unchanged. For find-and-replace in stream mode, `tw.streamReplace` provides both hooks; see [Replacing streamed text](#replacing-streamed-text). -**`onToolCall`** receives `{ id, name, input }`. Streamed arguments are collected until the call is complete, and the result is sent in the client's format. The hook returns nothing to leave the call unchanged, an object to replace it, `null` to remove it, or an array to replace it with several calls. An `id` is generated where one is missing. +**`onToolCall`** receives `{ id, name, input }`. Streamed arguments are collected until the call is complete, and the result is sent in the client's format. When the hook returns nothing, changes made to the call take effect, and a call that ends up as it came in is left unchanged. The hook can also return an object to replace the call, `null` to remove it, or an array to replace it with several calls. An `id` is generated where one is missing. Answer hooks run only on the answers to conversations. Thinking blocks are not passed to plugins and reach the client unchanged. @@ -223,9 +223,84 @@ type Ctx = { `ctx` is frozen and cannot be changed. +### Helpers + +The global object `tw` provides functions for common edits. Like the rest of a plugin, they are plain JavaScript that runs with the plugin's permissions and limits, and what they produce is checked like any other result. + +| Function | Does | +|---|---| +| `tw.appendSystem(req, text)` | Adds `text` to the end of the system prompt, after a blank line. Does nothing when the system prompt already contains `text`. Needs `system`. | +| `tw.texts(req, fn)` | Calls `fn(text, place)` for each piece of text that can be changed, in order: the system prompt, the text parts of messages, and tool results. When `fn` returns a string, it replaces the text; returning nothing keeps it. `place` is `{ kind, role }`: `kind` is `"system"`, `"text"` or `"tool_result"`, and `role` is the role of the message, or `null` for the system prompt. Thinking, image and other parts are not passed. For embeddings and legacy completions, the text of each input comes as `"text"`. Needs `system` or `messages`, and passes only the sections granted. | +| `tw.toolCalls(req, fn)` | Calls `fn(call)` for each earlier tool call in the conversation; changes made to `call.input` take effect. Needs `messages`. | +| `tw.mapStrings(value, fn)` | Returns a copy of `value` in which each string is replaced by `fn(string, key)`, where `key` is the property name or array index, or `null` for a string at the top level. Keys and other values are copied as they are. | +| `tw.replace(text, rules)` | Applies [replacement rules](#replacement-rules) to `text` and returns the result. | +| `tw.streamReplace(rules, options)` | Returns `{ onReplyText, onReplyTextEnd }`, which apply replacement rules to streamed answer text; see [Replacing streamed text](#replacing-streamed-text). | +| `tw.lines(text)` | Splits a setting into lines, trims each one and leaves out empty lines. | +| `tw.pairs(text, separator)` | Reads a setting with one pair per line, such as `npm => pnpm`, into `[from, to]` pairs. The separator is `"=>"` unless one is given. A line without the separator, or with nothing before it, is an error that names the line. | + +A helper that needs a section the plugin was not granted throws an error that names the permission. Errors thrown by helpers are errors of the plugin, handled as described in [When a plugin fails](#when-a-plugin-fails). + +For editors that read TypeScript declarations: + +```ts +declare const tw: { + appendSystem(req: RequestView, text: string): void; + texts( + req: RequestView | InputsView, + fn: (text: string, place: { kind: "system" | "text" | "tool_result"; role: Message["role"] | null }) => string | undefined, + ): void; + toolCalls(req: RequestView, fn: (call: { id: string; name: string; input: unknown }) => void): void; + mapStrings(value: T, fn: (s: string, key: string | number | null) => string): T; + replace(text: string, rules: Rules): string; + streamReplace(rules: Rules | ((ctx: Ctx) => Rules), options?: { maxMatch?: number }): { + onReplyText(text: string, ctx: Ctx): string; + onReplyTextEnd(ctx: Ctx): string; + }; + lines(text: string): string[]; + pairs(text: string, separator?: string): [string, string][]; +}; +type Rules = Iterable<[string | RegExp, string | ((match: string, ...groups: unknown[]) => string)]>; +``` + +#### Replacement rules + +Rules are a list of `[find, replacement]` pairs, as an array or a `Map`. `tw.replace` and `tw.streamReplace` treat them the same way. + +- A string matches literally and is case-sensitive. A regular expression matches globally without the `g` flag; the `i` flag ignores case. An empty string and the `y` flag are errors, and so is a regular expression that matches an empty string. +- The replacement for a string is inserted literally. The replacement for a regular expression may use `$1`, `$` and the other forms that `String.prototype.replace` accepts, or be a function. +- The text is scanned once, from left to right. Where several rules match at the same position, the one listed first applies. Replaced text is not scanned again, so with `A => B` and `B => C`, `A` becomes `B`. +- Placeholders, such as `<>`, are skipped whole: a rule never matches inside one or across one, and `^`, `$` and lookarounds treat a placeholder as the edge of the text. + +#### Replacing streamed text + +```js +export const manifest = { + name: "Unify terms", + api: 1, + description: "Replaces the terms listed here in requests and answers.", + permissions: ["messages", "reply.text"], + reply: "stream", + settings: { + terms: { type: "string", label: "Terms, one pair per line (old => new)", value: "" }, + }, +}; + +const rules = (ctx) => tw.pairs(ctx.settings.terms); + +export function onRequest(req, ctx) { + tw.texts(req, (text) => tw.replace(text, rules(ctx))); +} + +export const { onReplyText, onReplyTextEnd } = tw.streamReplace(rules); +``` + +`tw.streamReplace` needs `reply: "stream"`. Its `rules` are either a list of rules or a function that receives `ctx` and returns one; the function is called at the start of each text block, which is how rules come from settings. + +The streamed result is the same as `tw.replace` applied to the whole block. To catch a match that spans two pieces of the stream, the end of the text is held back until it cannot be the start of a match. With string rules only, that is just a tail that could begin one of the strings, which usually causes no visible delay. With any regular expression, it is the last 256 characters, so the client sees the text up to that many characters behind the model. `options.maxMatch` sets that number; a regular expression match longer than it may be missed when it spans two pieces. Text blocks streamed at the same time, such as the choices of a Chat Completions request with `n` greater than 1, are held separately. A replacement function, and `` $` `` and `$'` in a replacement string, see only the text held at that moment rather than the whole block. + ### Available JavaScript -Plugins have the standard JavaScript built-ins, such as `JSON`, `RegExp`, `Map`, `Date` and `Math`; `console.log`, `console.info`, `console.warn` and `console.error`, which write to the plugin's log; and `reject`, which is valid only in `onRequest`. There is no `fetch`, `require` or timer, `import` and `import()` load nothing, and there is no access to files, the network, environment variables or processes. The sandbox's clock is UTC. Nothing a plugin stores in a variable outlives the request attempt or answer it runs for. +Plugins have the standard JavaScript built-ins, such as `JSON`, `RegExp`, `Map`, `Date` and `Math`; `console.log`, `console.info`, `console.warn` and `console.error`, which write to the plugin's log; `reject`, which is valid only in `onRequest`; and `tw`, the [helpers](#helpers). There is no `fetch`, `require` or timer, `import` and `import()` load nothing, and there is no access to files, the network, environment variables or processes. The sandbox's clock is UTC. Nothing a plugin stores in a variable outlives the request attempt or answer it runs for. ## Permissions diff --git a/src/content/docs-lite/zh-CN/features.md b/src/content/docs-lite/zh-CN/features.md index 8e107cd..7cc6855 100644 --- a/src/content/docs-lite/zh-CN/features.md +++ b/src/content/docs-lite/zh-CN/features.md @@ -101,7 +101,7 @@ MCP 页管理客户端从自己的配置文件中加载的内容,这些内容 ## 插件 -插件是简短的 JavaScript 文件,在请求发往上游之前改写请求、在回答交给客户端之前改写回答,例如要求用指定的语言回答、在 WSL 与 Windows 之间转换工具调用里的路径。插件在路由之后、于 core 内部的沙箱中运行,没有网络和文件,两次请求之间不留任何数据;看到的是占位符而不是请求里的密钥,改动之后照常经过各项防护。应用自带两个插件,出厂均为关闭。每个插件是一个文件,适用范围、出错时的处理方式和设置项也写在其中;编辑器分为「设置」与「代码」两页,「设置」页的更改写回文件,添加插件时打开的也是同一个编辑器。日常的更改直接保存,只有能改动工具调用的插件,安装、打开、更改代码和确认文件变更才要在系统对话框中确认。文件在应用之外被改动后,插件停止运行,确认新版本后恢复。插件页列出每个插件的状态、权限、适用范围与统计,提供用最近一条请求试运行和日志;被插件改动过的请求在「流量」页带有标记。接口、权限、限额与安全模型见[插件](/zh-CN/docs/lite/plugins)。 +插件是简短的 JavaScript 文件,在请求发往上游之前改写请求、在回答交给客户端之前改写回答,例如要求用指定的语言回答、在 WSL 与 Windows 之间转换工具调用里的路径。插件在路由之后、于 core 内部的沙箱中运行,没有网络和文件,两次请求之间不留任何数据;看到的是占位符而不是请求里的密钥,改动之后照常经过各项防护。应用自带两个插件,出厂均为关闭。全局对象 `tw` 提供常用改写的辅助函数,例如在系统提示词末尾附加一句、在流式回答中替换用词。每个插件是一个文件,适用范围、出错时的处理方式和设置项也写在其中;编辑器分为「设置」与「代码」两页,「设置」页的更改写回文件,添加插件时打开的也是同一个编辑器。日常的更改直接保存,只有能改动工具调用的插件,安装、打开、更改代码和确认文件变更才要在系统对话框中确认。文件在应用之外被改动后,插件停止运行,确认新版本后恢复。插件页列出每个插件的状态、权限、适用范围与统计,提供用最近一条请求试运行和日志;被插件改动过的请求在「流量」页带有标记。接口、权限、限额与安全模型见[插件](/zh-CN/docs/lite/plugins)。 ## 设置 diff --git a/src/content/docs-lite/zh-CN/plugins.md b/src/content/docs-lite/zh-CN/plugins.md index 3009fce..510a72b 100644 --- a/src/content/docs-lite/zh-CN/plugins.md +++ b/src/content/docs-lite/zh-CN/plugins.md @@ -89,12 +89,12 @@ export const manifest = { export function onRequest(req, ctx) { const notes = String(ctx.settings.notes).trim(); - if (notes === "") return; // 没有要附加的内容:请求原样发出 - req.system = req.system ? `${req.system}\n\n${notes}` : notes; - return req; + if (notes !== "") tw.appendSystem(req, notes); } ``` +钩子结束时,对 `req` 的修改即生效,无须返回。`tw.appendSystem` 是常用改写的[辅助函数](#辅助函数)之一。 + ### manifest | 字段 | 必填 | 取值 | @@ -138,11 +138,11 @@ manifest 是纯数据,因此 core 可以直接从源码中读出它,应用 | `onReplyTextEnd(ctx)` | `reply.text` | 逐段模式下,每段文字结束时调用。可选。 | | `onToolCall(call, ctx)` | `reply.tool_calls` | 回答中的每个工具调用完整之后调用。 | -**`onRequest`** 接收[请求视图](#请求视图),返回改过的视图;不返回则请求保持原样。调用 `reject("原因")` 拒绝这次请求,客户端收到注明插件名称的错误;`reject` 通过抛出异常结束钩子,即使插件自己接住这个异常,拒绝依然成立。每次把请求发往一个上游之前调用一次,详见[插件在哪一步运行](#插件在哪一步运行)。除了生成回答的请求,计算 token 数的请求(`/v1/messages/count_tokens`、Gemini 的 `countTokens` 与 Responses 接口的 `input_tokens`)和 Responses 的压缩请求携带的是同一段对话,也经过它,插件删去的内容不会经由这些接口发出。这些请求只采用插件对模型的修改,不采用对其他 `params` 的修改,因为这些接口不接受那些参数。网关自行估算、不发往上游的 token 计数不运行插件。 +**`onRequest`** 接收[请求视图](#请求视图)。钩子结束时,对视图的修改即生效,无论是否返回它;返回另一个对象时,以返回的对象为准。视图与传入时相同时,请求保持原样。调用 `reject("原因")` 拒绝这次请求,客户端收到注明插件名称的错误;`reject` 通过抛出异常结束钩子,即使插件自己接住这个异常,拒绝依然成立。每次把请求发往一个上游之前调用一次,详见[插件在哪一步运行](#插件在哪一步运行)。除了生成回答的请求,计算 token 数的请求(`/v1/messages/count_tokens`、Gemini 的 `countTokens` 与 Responses 接口的 `input_tokens`)和 Responses 的压缩请求携带的是同一段对话,也经过它,插件删去的内容不会经由这些接口发出。这些请求只采用插件对模型的修改,不采用对其他 `params` 的修改,因为这些接口不接受那些参数。网关自行估算、不发往上游的 token 计数不运行插件。 -**`onReplyText`** 在整段模式(默认)下,每段文字到齐后调用一次,参数是整段文字,调用结束后文字才交给客户端。逐段模式下随流式输出逐段调用,返回值是此刻要发出的文字;返回 `""` 表示先扣住,这段文字结束时 `onReplyTextEnd` 的返回值把扣住的内容放出。非流式的回答一次传入全部文字,逐段模式下随后再调用 `onReplyTextEnd`。不返回表示文字不变。 +**`onReplyText`** 在整段模式(默认)下,每段文字到齐后调用一次,参数是整段文字,调用结束后文字才交给客户端。逐段模式下随流式输出逐段调用,返回值是此刻要发出的文字;返回 `""` 表示先扣住,这段文字结束时 `onReplyTextEnd` 的返回值把扣住的内容放出。非流式的回答一次传入全部文字,逐段模式下随后再调用 `onReplyTextEnd`。不返回表示文字不变。逐段模式下的查找替换可以由 `tw.streamReplace` 提供这两个钩子,见[逐段替换](#逐段替换)。 -**`onToolCall`** 接收 `{ id, name, input }`。流式输出的参数先收集完整,处理后按客户端的格式发出。不返回表示保持原样;返回一个对象替换这个调用;返回 `null` 删除它;返回一个数组则替换为多个调用。缺少 `id` 的由 core 生成。 +**`onToolCall`** 接收 `{ id, name, input }`。流式输出的参数先收集完整,处理后按客户端的格式发出。不返回时,对这个调用的修改即生效,与传入时相同则保持原样;返回一个对象替换这个调用;返回 `null` 删除它;返回一个数组则替换为多个调用。缺少 `id` 的由 core 生成。 回答钩子只在对话的回答上运行。思考内容不交给插件,原样交给客户端。 @@ -223,9 +223,84 @@ type Ctx = { `ctx` 已冻结,不能修改。 +### 辅助函数 + +全局对象 `tw` 提供常用改写的函数。它们和插件的其他代码一样是普通的 JavaScript,受插件的权限和限额约束,产生的结果和其他结果一样接受检查。 + +| 函数 | 作用 | +|---|---| +| `tw.appendSystem(req, text)` | 在系统提示词末尾附加 `text`,与原文之间空一行;系统提示词已包含 `text` 时不再附加。需要 `system`。 | +| `tw.texts(req, fn)` | 依次对每段可修改的文字调用 `fn(text, place)`:系统提示词、消息中的文字片段与工具结果。`fn` 返回字符串时替换这段文字,不返回则保持不变。`place` 为 `{ kind, role }`:`kind` 取 `"system"`、`"text"` 或 `"tool_result"`,`role` 为消息的角色,系统提示词为 `null`。思考、图片和其他片段不传入。嵌入与旧版补全的每项输入按 `"text"` 传入。需要 `system` 或 `messages`,只传入已授权的部分。 | +| `tw.toolCalls(req, fn)` | 对对话中此前的每个工具调用调用 `fn(call)`,对 `call.input` 的修改即生效。需要 `messages`。 | +| `tw.mapStrings(value, fn)` | 返回 `value` 的副本,其中每个字符串替换为 `fn(string, key)` 的返回值;`key` 为属性名或数组下标,顶层的字符串为 `null`。对象的键和其他值原样复制。 | +| `tw.replace(text, rules)` | 按[替换规则](#替换规则)改写 `text`,返回结果。 | +| `tw.streamReplace(rules, options)` | 返回 `{ onReplyText, onReplyTextEnd }`,在逐段输出的回答文字上应用替换规则,见[逐段替换](#逐段替换)。 | +| `tw.lines(text)` | 把设置拆成各行,去除每行首尾的空白,略去空行。 | +| `tw.pairs(text, separator)` | 把每行一对的设置(例如 `npm => pnpm`)读成 `[原文, 替换]` 对;分隔符缺省为 `"=>"`。缺少分隔符或分隔符前为空的行按出错处理,错误信息指明行号。 | + +辅助函数需要插件未获授权的部分时,抛出错误并指明所需的权限。辅助函数抛出的错误即插件的错误,处理方式见[插件出错时](#插件出错时)。 + +供读取 TypeScript 声明的编辑器使用: + +```ts +declare const tw: { + appendSystem(req: RequestView, text: string): void; + texts( + req: RequestView | InputsView, + fn: (text: string, place: { kind: "system" | "text" | "tool_result"; role: Message["role"] | null }) => string | undefined, + ): void; + toolCalls(req: RequestView, fn: (call: { id: string; name: string; input: unknown }) => void): void; + mapStrings(value: T, fn: (s: string, key: string | number | null) => string): T; + replace(text: string, rules: Rules): string; + streamReplace(rules: Rules | ((ctx: Ctx) => Rules), options?: { maxMatch?: number }): { + onReplyText(text: string, ctx: Ctx): string; + onReplyTextEnd(ctx: Ctx): string; + }; + lines(text: string): string[]; + pairs(text: string, separator?: string): [string, string][]; +}; +type Rules = Iterable<[string | RegExp, string | ((match: string, ...groups: unknown[]) => string)]>; +``` + +#### 替换规则 + +规则是 `[查找, 替换]` 对的列表,可以是数组或 `Map`。`tw.replace` 与 `tw.streamReplace` 对规则的处理相同。 + +- 字符串按字面匹配,区分大小写。正则表达式按全局匹配,无须 `g` 标志;`i` 标志表示不区分大小写。空字符串和 `y` 标志按出错处理,正则表达式匹配到空串时同样按出错处理。 +- 查找为字符串时,替换按字面写入。查找为正则表达式时,替换可以使用 `$1`、`$` 等 `String.prototype.replace` 支持的写法,也可以是函数。 +- 文字从左到右扫描一次;同一位置有多条规则命中时,取列表中靠前的一条;替换写入的文字不再参与匹配,因此 `A => B` 与 `B => C` 并存时,A 换成 B。 +- 占位符(例如 `<>`)整体跳过:规则不会在占位符内部匹配,也不会跨过占位符匹配;`^`、`$` 与前后断言把占位符视为文字的边界。 + +#### 逐段替换 + +```js +export const manifest = { + name: "统一用词", + api: 1, + description: "替换请求和回答中这里列出的用词。", + permissions: ["messages", "reply.text"], + reply: "stream", + settings: { + terms: { type: "string", label: "用词,每行一对(旧词 => 新词)", value: "" }, + }, +}; + +const rules = (ctx) => tw.pairs(ctx.settings.terms); + +export function onRequest(req, ctx) { + tw.texts(req, (text) => tw.replace(text, rules(ctx))); +} + +export const { onReplyText, onReplyTextEnd } = tw.streamReplace(rules); +``` + +`tw.streamReplace` 要求 `reply: "stream"`。`rules` 可以是规则列表,也可以是接收 `ctx`、返回规则列表的函数;函数在每块文字开始时调用一次,用于从设置中读取规则。 + +逐段替换的结果与对整块文字调用 `tw.replace` 相同。为了找到跨越两段的命中,文字的末尾会先扣住,直到确定它不是一处命中的开头。只有字符串规则时,只扣住可能是某个字符串开头的部分,通常没有可察觉的延迟。含有正则表达式时,扣住末尾 256 个字符,客户端看到的文字比模型的输出滞后至多这么多字符。`options.maxMatch` 设置这个数;长于它的正则命中跨越两段时可能漏掉。同时输出的多块文字(例如 Chat Completions 请求 `n` 大于 1 时的各个选项)分别扣住。替换函数收到的整段文字参数,以及替换字符串中的 `` $` `` 与 `$'`,在逐段替换时只涵盖当时扣住的文字,而不是整块文字。 + ### 可用的 JavaScript -插件可以使用 JavaScript 标准内置对象,例如 `JSON`、`RegExp`、`Map`、`Date` 与 `Math`;`console.log`、`console.info`、`console.warn` 与 `console.error` 写入插件日志;`reject` 只在 `onRequest` 中有效。没有 `fetch`、`require` 和定时器,`import` 与 `import()` 加载不了任何模块,也无法访问文件、网络、环境变量和进程。沙箱里的时钟是 UTC。插件存进变量的内容,不会留到它所处理的这一次发送或这个回答之后。 +插件可以使用 JavaScript 标准内置对象,例如 `JSON`、`RegExp`、`Map`、`Date` 与 `Math`;`console.log`、`console.info`、`console.warn` 与 `console.error` 写入插件日志;`reject` 只在 `onRequest` 中有效;`tw` 提供[辅助函数](#辅助函数)。没有 `fetch`、`require` 和定时器,`import` 与 `import()` 加载不了任何模块,也无法访问文件、网络、环境变量和进程。沙箱里的时钟是 UTC。插件存进变量的内容,不会留到它所处理的这一次发送或这个回答之后。 ## 权限