插件#
插件让请求和回答适应具体的使用场景:在系统提示词中附加说明、去掉某个上游不接受的参数、要求用指定的语言回答,或者在 WSL 与 Windows 之间改写工具调用里的路径。插件是一段简短的 JavaScript,在 core 内部的沙箱中运行,看到的是占位符而不是请求里的密钥;它做的每一处改动都记录在请求上,并和客户端发来的内容一样经过各项防护的检查。
本页说明插件能改什么、在哪一步运行、如何添加和编辑、编写插件的接口、权限、限额与安全模型。应用自带两个插件,出厂均为关闭,末尾逐一说明。
插件能改什么#
| 方向 | 可以改动的内容 |
|---|---|
| 请求,发往上游之前 | 系统提示词;对话消息,含工具结果和此前工具调用的参数;工具定义;模型、max_tokens、temperature、top_p 与 stop。插件也可以拒绝这次请求。 |
| 回答,交给客户端之前 | 回答的文字,可以整段处理,也可以随流式输出逐段处理;回答里的工具调用,可以修改、删除或新增。 |
无论客户端使用哪种接口格式(Anthropic Messages、OpenAI Chat Completions、OpenAI Responses 或 Gemini),插件看到的都是同一种结构。插件默认处理对话:生成回答的请求,以及携带同一段对话的计算 token 数请求与 Responses 压缩请求。声明了它们的插件还处理嵌入与旧版补全,可以修改每一项输入的文字,见嵌入与旧版补全。其他接口(例如图片、音频)不经过任何插件,原样转发。
Codex 通过 WebSocket 使用 Responses 接口时,插件同样生效:每个 response.create 经过请求钩子,每次回答经过回答钩子。一条连接沿用它建立时的插件。其他 WebSocket 连接(例如 Realtime 接口)不经过插件。
core 按客户端自己的格式写回改动,只动改过的条目,缓存标记、签名、图片和不认识的字段都保持原样。没有被任何插件改动的请求逐字节原样转发,不影响上游的提示词缓存。
请求头、上游地址与凭据不对插件开放。图片只提供媒体类型,不提供内容;思考内容只读,不能修改。
插件在哪一步运行#
请求先按客户端发来的原样路由:路由规则、规则对模型的改名、策略组的故障转移、同一会话固定使用同一上游,以及密钥的可用模型,依据的都是客户端的原始请求,插件改变不了请求的去向。之后,每次把请求发往一个上游之前:
- 把请求里的密钥替换为占位符;
- 从客户端发来的原始请求开始,按列表顺序运行适用范围覆盖这一次发送的请求钩子,运行之后换回占位符;
- 插件改动了请求时,内容过滤(包括隐藏字符规则)再检查一遍,只报告插件加入的内容;拦截时整个请求被拒绝,不会转到别的上游;
- 需要时转换为上游的格式,经过出站脱敏后发出。
一次发送失败、请求转到另一个上游时,请求钩子从客户端的原始请求重新运行,为一个上游做的改动不会带到另一个上游。向同一个上游重发(例如登录令牌刷新后的重试)时,沿用请求钩子已有的结果。
插件修改模型时,只改变这一次发往上游的模型名,效果与路由规则改名相同,并取代规则改过的名字。请求不会重新路由,也不再核对上游的模型列表,但密钥的可用模型依然有效:改成密钥不可用的模型时,请求被拒绝。
回答一侧,回答钩子在回答转换为客户端的格式之后、工具调用审查之前运行。
添加与编辑插件#
每个插件是一个文件,插件的一切都写在其中:代码、处理哪些请求、出错时如何处理,以及每个设置项的取值。单凭这个文件就能完整描述插件,复制文件时设置也随之带走。只有插件是否启用不写在文件里,它是应用中的一个开关。
在插件所在行选择「设置」即打开它的编辑器。编辑器分为两页,只有一个「保存」:
- 设置:「启用」开关、「适用范围」、「出错时」的处理方式,以及插件自己的设置项。除「启用」外,这一页的更改都写回代码中的 manifest:应用只改写 manifest,文件中的其他字节保持原样。
- 代码:完整的文件内容。输入停顿时,core 重新读取代码,「设置」页随之更新。代码无法加载时,标出出错的行号与列号,「设置」页等到代码能够加载之后才能修改。
在两页之间切换时,未保存的更改都会保留,保存时一并写入。
「添加插件」打开同一个编辑器,从「代码」页开始,里面是一个可以直接安装的简单插件。在这里修改或粘贴代码,或者用右上角的「从文件导入…」载入文件,选择「安装」即安装插件。新插件的「设置」页多一项「插件 ID」:小写字母、数字和连字符,最多 40 个,按文件名或插件名预先填好,安装后不可修改。新插件安装后处于关闭状态,除非安装前打开了「启用」。插件只能从本地文件或粘贴的代码安装,不支持通过链接安装,没有插件市场,也不会自动更新。
「插件」页还提供:
- 顺序:插件按列表顺序依次运行,可在「调整顺序」中拖动或用箭头更改。后一个看到的是前一个改过的结果,各自按自己的权限核对。
- 试运行:用请求历史中最近的一条请求运行插件,按这条请求当时的路由(作答的上游和发给它的模型名),显示改动前后的请求或回答以及插件日志。试运行不发往上游,也不计入统计。
- 日志:插件通过
console写下的最近 500 行。 - 统计:网关启动以来的运行次数、改写次数、拒绝次数、出错次数和平均 CPU 时间。
「流量」页中,被插件改动过的请求带有标记。请求详情按每一次发送分组列出运行过的插件、各自的结果(未改动、已改写、已拒绝、出错或已跳过)与 CPU 时间,并可以对照客户端发来的原始请求和发往作答上游的请求。插件出错时发送提醒。
应用连接远程 core 时,插件安装在服务器上,也在服务器上运行。
在系统对话框中确认#
日常的更改直接保存。只有能改动工具调用的插件(拥有 reply.tool_calls 权限)需要在系统对话框中确认,而且只限四种操作:安装、打开、保存对代码的更改,以及确认在应用之外对文件所做的更改。代码更改前后任何一版拥有这项权限,都要确认,因此新增 reply.tool_calls 同样要确认;读不出权限的插件按拥有这项权限对待。
系统对话框由应用自身弹出,不属于页面。对话框写明插件名称、它能做什么和这次改动的内容;涉及新代码时,还给出代码 SHA-256 哈希的开头部分,可与应用中显示的对照。core 只接受经过系统对话框的这四种操作,注入页面的脚本无法自行完成。
以下更改都无须系统对话框:任何插件的设置项、适用范围和出错时的处理方式,关闭插件、调整顺序和删除,以及安装、打开和编辑不能改动工具调用的插件。在应用中编辑配置文件,或者从版本历史恢复某个版本,都不能安装能改动工具调用的插件、打开它或更改它的代码;这类更改在那里会被拒绝,须在「插件」页进行。
文件在应用之外被改动时#
插件只按确认过的样子运行。core 为每个插件保留一份确认过的副本及其 SHA-256 哈希,在应用中保存时,文件、副本与哈希一同更新。文件在应用之外被改动或删除后,插件在几秒内停止运行,状态显示为「文件已更改」。「审核更改」列出与确认过的副本之间的差异,选择「确认更改」后插件恢复运行;也可以在插件的编辑器中保存,用编辑器里的代码写回文件。
编写插件#
插件是一个 UTF-8 编码的 ES 模块文件,不超过 1 MiB,导出 manifest 和一个或多个钩子函数;manifest 描述插件,同时保存它的配置。
export const manifest = {
name: "附加项目说明",
api: 1,
description: "在系统提示词末尾附上这里填写的说明。",
permissions: ["system"],
match: { clients: ["claude-code"], models: [], upstreams: [] },
on_error: "reject",
settings: {
notes: { type: "string", label: "说明内容", value: "使用 pnpm,不用 npm。" },
},
};
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;
}
manifest#
| 字段 | 必填 | 取值 |
|---|---|---|
name | 是 | 1 至 64 个字符。 |
api | 是 | 1,目前唯一支持的版本。 |
description | 否 | 不超过 500 个字符。 |
permissions | 是 | system、messages、tools、params、reply.text、reply.tool_calls 中的一项或多项,见权限。 |
requests | 否 | 插件处理的请求种类:conversation(对话)、embeddings(嵌入)、completions(旧版补全)中的一项或多项。缺省时只处理对话,见嵌入与旧版补全。 |
match | 否 | 适用范围:clients、models、upstreams 三个列表,每个最多 100 项,* 匹配任意一串字符;列表缺省或为空表示全部。对应「设置」页的「适用范围」。 |
on_error | 否 | "reject"(默认)或 "skip":插件出错时如何处理,见插件出错时。对应「设置」页的「出错时」。 |
reply | 否 | "block"(默认)或 "stream",决定 onReplyText 以何种方式接收文字。 |
settings | 否 | 最多 20 项,名称由字母、数字和 _ 组成,不以数字开头,每项包含 type(string、number 或 boolean)、label(纯文本,不超过 100 个字符)与 value,即当前的取值(缺省时为 ""、0 或 false)。取值在「设置」页填写,通过 ctx.settings 传给插件。字符串的取值可以有多行,不超过 10,000 个字符。 |
插件是否启用不在 manifest 中,它是应用中的「启用」开关。
manifest 是纯数据,因此 core 可以直接从源码中读出它,应用也无须运行代码就能改写它。它是一个对象字面量,取值只能是字符串、数字、true、false、null、列表和嵌套的对象;键写成名称或带引号的字符串,允许结尾的逗号。表达式、变量、函数调用、展开、计算出的键、getter,以及带 ${} 的模板字符串都不是数据:manifest 中用到它们的文件无法加载,声明之后又在代码中改动 manifest 的文件同样无法加载。
改写只替换 manifest 字面量,其余每个字节都保持原样。字面量按固定的格式写回,即本页示例所用的格式;其中的注释不会保留,关于设置项的说明应写在 manifest 上方。
文件在添加时和每次被 core 加载时都会检查:必须导出有效的 manifest(不能有上表以外的字段)和至少一个钩子。导出的每个钩子都要有对应的权限,申请的每项权限也都要有用到它的钩子,插件不会申请用不到的权限;onReplyTextEnd 还要求同时导出 onReplyText 并使用逐段模式。未通过检查的文件不会安装,错误信息尽可能给出行号和列号。
适用范围#
适用范围即 manifest 中的 match,在「设置」页的「适用范围」中编辑,编辑时会提示网关已知的客户端、模型和上游。
| 列表 | 匹配的对象 |
|---|---|
clients | 请求来自的客户端,即请求记录上的客户端,例如 claude-code。识别不出客户端的请求只与空列表匹配。 |
models | 发往上游的模型名,即路由规则改名之后的名字。 |
upstreams | 这一次发送的上游。 |
三个列表对请求钩子和回答钩子都适用,匹配时不区分大小写。请求钩子按每一次发送判断是否在适用范围内:发生故障转移时,限定了上游的插件只在发往这些上游时运行。一次发送要运行哪些请求钩子,在其中任何一个运行之前就已确定,前面的插件改了模型名,不影响后面运行哪些请求钩子;回答钩子按作答上游实际收到的模型名匹配。此外,插件只处理它在 requests 中声明的请求种类。
钩子#
| 钩子 | 权限 | 调用时机 |
|---|---|---|
onRequest(req, ctx) | system、messages、tools 或 params | 路由之后,每次把请求发往一个上游之前。 |
onReplyText(text, ctx) | reply.text | 处理回答中的文字。 |
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 计数不运行插件。
onReplyText 在整段模式(默认)下,每段文字到齐后调用一次,参数是整段文字,调用结束后文字才交给客户端。逐段模式下随流式输出逐段调用,返回值是此刻要发出的文字;返回 "" 表示先扣住,这段文字结束时 onReplyTextEnd 的返回值把扣住的内容放出。非流式的回答一次传入全部文字,逐段模式下随后再调用 onReplyTextEnd。不返回表示文字不变。
onToolCall 接收 { id, name, input }。流式输出的参数先收集完整,处理后按客户端的格式发出。不返回表示保持原样;返回一个对象替换这个调用;返回 null 删除它;返回一个数组则替换为多个调用。缺少 id 的由 core 生成。
回答钩子只在对话的回答上运行。思考内容不交给插件,原样交给客户端。
钩子可以是 async 函数,也可以返回 Promise:Promise 在同一次调用内、同样的限额之下落定;始终不落定的 Promise 按出错处理。
请求视图#
type RequestView = {
format: "anthropic" | "openai_chat" | "openai_responses" | "gemini"; // 只读
model: string; // 只读;修改模型要通过 params.model
system?: string; // 授予 "system" 时提供;没有系统提示词时为 ""
messages?: Message[]; // 授予 "messages" 时提供
tools?: Tool[]; // 授予 "tools" 时提供
params?: Params; // 授予 "params" 时提供
};
type Message = { key?: string; role: "user" | "assistant" | "tool" | "system"; parts: Part[] };
type Part =
| { key?: string; type: "text"; text: string }
| { key?: string; type: "thinking"; text: string } // 只读
| { key?: string; type: "tool_call"; id: string; name: string; input: unknown } // input 可改
| { key?: string; type: "tool_result"; call_id: string; text: string; is_error: boolean } // text 可改
| { key?: string; type: "image"; media_type: string | null } // 只读,不含图片数据
| { key?: string; type: "other"; label: string }; // 只读
type Tool = { key?: string; name: string; description: string; input_schema: unknown };
type Params = { model: string; max_tokens?: number; temperature?: number; top_p?: number; stop?: string[] };
model 与 params.model 是这一次将要发往上游的模型名,与 ctx.model 相同。system 是对话开头的系统指令:Anthropic Messages 的 system 字段、Chat Completions 开头的 system 或 developer 消息、Responses 的 instructions、Gemini 的 systemInstruction。设为 "" 即删除。只含工具结果的消息角色为 tool,对话中途出现的 system 或 developer 消息角色为 system。
core 为每条消息、每个片段和每个工具分配一个 key。改动规则如下:
- 要修改的消息、片段或工具保留原来的
key,新增的不带key。出现未知或重复的key按出错处理。 - 消息可以删除、修改或新增。新增的消息角色只能是
user、assistant或system,且只含文字片段。保留下来的消息保持原有的先后顺序和角色。 - 保留下来的消息里,片段可以删除,可编辑的字段可以修改,也可以新增文字片段。
type、id、name与call_id不可修改,思考、图片和其他片段整体不可修改。 - 工具可以删除或新增,
description与input_schema可以修改,工具名称不能重复。视图只列出客户端自己定义的工具;由接口提供定义的工具(例如网页搜索)不在视图中,保持原样,新增的工具也不能与它们重名。 max_tokens、temperature、top_p与stop可以修改或删除,params.model可以修改。修改params.model会改变这一次发往上游的模型名,见插件在哪一步运行。
违反规则的结果,或者带有未授权部分的结果,都按出错处理。
嵌入与旧版补全#
在 requests 中声明了它们的插件,还处理嵌入(OpenAI 的 /v1/embeddings,Gemini 的 :embedContent 与 :batchEmbedContents)和旧版补全(OpenAI 的 /v1/completions)。这两种请求的视图结构相同,只是没有系统提示词和工具:
type InputsView = {
format: "openai_embeddings" | "openai_completions" | "gemini_embed"; // 只读
model: string; // 只读;修改模型要通过 params.model
messages?: Message[]; // 授予 "messages" 时提供:每项输入一条 user 消息
params?: Params; // 授予 "params" 时提供:模型;补全另有 max_tokens、temperature、top_p、stop
};
- 每项输入是一条
user消息。OpenAI 的input(嵌入)或prompt(补全)是数组时,每个元素一条,是单个字符串时就是一条;Gemini 的每个 content 一条,content 的每个部分对应一个片段。 - 只有文字片段的文字可以修改。以 token ID 给出的输入是只读的
other片段,label为tokens;其他不是文字的部分同样只读。 - 消息和片段不能新增、删除或调换顺序,因为回答按输入逐项对应。
params中有模型,补全另有max_tokens、temperature、top_p与stop。suffix、dimensions等其他字段不提供给插件,保持原样。
其余与对话相同:占位符、路由之后按每一次发送运行、修改模型只改变这一次发往上游的模型名、运行记录与试运行。改动过的请求按各项输入的文字,再经过一次内容过滤。这两种请求不运行回答钩子:嵌入的回答不含文字,旧版补全的回答原样转发。
messages 与 params 适用于全部三种请求;system、tools、reply.text 与 reply.tool_calls 只适用于对话。出现以下情况时插件无法加载:requests 为空、含有未知的种类或同一种类写了两次;声明的某种请求没有任何一项权限用得上(嵌入和旧版补全需要 messages 或 params);或者某项权限不适用于声明的任何一种请求,例如申请了 system 却没有声明 conversation。
插件没有声明的请求种类不在它的适用范围内:这类请求原样通过,不留记录;无论出错时的处理方式如何,即使插件文件已更改或加载失败,也不会因它被拒绝。
ctx#
type Ctx = {
client: string | null; // 请求记录上的客户端,例如 "claude-code"
model: string; // 发往上游的模型名,即路由规则或前面的插件改名之后的名字
requested_model: string; // 客户端请求的模型
format: "anthropic" | "openai_chat" | "openai_responses" | "gemini"
| "openai_embeddings" | "openai_completions" | "gemini_embed"; // 客户端请求的格式
upstream: string; // 这一次发送的上游,或作答的上游
settings: Record<string, string | number | boolean>;
};
ctx 已冻结,不能修改。
可用的 JavaScript#
插件可以使用 JavaScript 标准内置对象,例如 JSON、RegExp、Map、Date 与 Math;console.log、console.info、console.warn 与 console.error 写入插件日志;reject 只在 onRequest 中有效。没有 fetch、require 和定时器,import 与 import() 加载不了任何模块,也无法访问文件、网络、环境变量和进程。沙箱里的时钟是 UTC。插件存进变量的内容,不会留到它所处理的这一次发送或这个回答之后。
权限#
权限同时决定插件看得到什么和能改什么。未授权的部分不会交给插件;返回的结果改动了未授权的部分,按出错处理。
| 权限 | 可以看到并修改 | 应用中的提示 |
|---|---|---|
system | 系统提示词 | |
messages | 对话消息:文字、工具结果和此前工具调用的参数;思考内容只读,图片只提供类型。嵌入与旧版补全:每项输入的文字 | 可以往对话中加入指令 |
tools | 工具定义 | 会改变模型能使用的工具 |
params | 模型、max_tokens、temperature、top_p、stop;嵌入只有模型 | 可能改变发往上游的模型,以及因此产生的费用 |
reply.text | 回答中的文字 | |
reply.tool_calls | 回答中的工具调用:修改、删除、新增 | 高风险,以红字提示:插件可以改动客户端要执行的操作。改动后的工具调用仍经过工具调用审查;安装这类插件、打开它、更改它的代码和确认它的文件变更,都要在系统对话框中确认。 |
插件出错时#
插件抛出错误、超出限额、返回无效的结果,或者改动了未授权的部分,都算出错;插件处理的那种请求,请求体无法读取时也算出错。出错时的处理方式由 manifest 的 on_error 决定,即「设置」页的「出错时」:
- 拒绝这次请求(默认):请求被拒绝,或回答在此处结束,并附上注明插件名称的错误。被拒绝的请求不会转到别的上游重试。
- 跳过此插件:这次请求(或这个回答余下的部分)不经过该插件,按未安装它的情况继续。
插件无法运行时(文件已更改而尚未确认,或者插件加载失败),按同一设置处理。这时出错时的处理方式和适用范围取自确认过的文件,即使代码无法运行,core 也能把它们当作数据读出;因此这样的插件与正常的插件一样,按每一次发送的请求种类、客户端、模型和上游判断是否在适用范围内,只拒绝范围内的发送。加载失败的插件按只处理对话对待,完全读不出 manifest 的插件拒绝所有对话。每次出错都记录在请求上。
限额#
| 钩子 | CPU 时间 | 内存 | 输出 |
|---|---|---|---|
| 请求钩子 | 每次 200 毫秒,含模块顶层代码 | 128 MiB | 不超过请求视图大小的两倍加 1 MiB |
| 回答钩子 | 每次 20 毫秒,整个回答累计 2 秒 | 64 MiB | 不超过传入的文字或工具调用大小的两倍加 1 MiB;onReplyTextEnd 放出的文字不超过 1 MiB |
每次调用最多写 100 行日志,超过 4 KiB 的行截短。插件文件不超过 1 MiB。超出限额的调用(包括写第 101 行日志)立即中止,按出错处理。
整个网关同时运行的回答钩子实例最多 32 个。带回答钩子的插件每处理一个回答占用一个,回答结束时释放。没有空余时,按该插件出错时的处理方式:「拒绝这次请求」在回答的任何内容发出之前拒绝请求,「跳过此插件」让这次回答不经过该插件。两种情况都记为一次出错,并发送提醒。
安全模型#
- 沙箱里什么都没有。 插件在 QuickJS 中运行,QuickJS 编译成 WebAssembly,由 core 内部的 Wasmtime 执行。插件代码从不在应用窗口中运行,也从不以原生代码运行。沙箱里没有网络、文件、环境变量和进程;即使 JavaScript 引擎本身有漏洞,也只能破坏沙箱自己的内存,碰不到 core 进程中的密钥与令牌。
- 不留任何数据。 每次调用请求钩子都使用新实例。每个回答中,一个插件使用一个实例,由它在这个回答上的各个钩子共用,回答结束即丢弃。插件之间不共享任何东西。
- 只看到占位符。 出站脱敏规则能识别的密钥,在交给插件之前替换为占位符,插件处理之后再换回;请求与回答两个方向都是如此,与这项防护处于哪一档无关。回答一侧同样重要:回答会进入对话历史,随下一次请求再次发往上游;插件如果看得到真实的密钥,就能把它编码后藏进回答里带出去。
- 防护照常生效。 请求钩子在路由之后运行,被它改动的请求在出站脱敏之前再经过内容过滤;回答钩子在工具调用审查之前运行。插件写入的内容和其他内容一样受检,插件既改变不了请求的路由,也不能换用密钥不可用的模型。
- 只运行确认过的代码。 插件文件与确认过的 SHA-256 哈希一致才会运行,在应用中保存时,文件与哈希一同更新。能改动工具调用的插件,安装、打开、更改代码和确认文件变更,都要在页面之外的系统对话框中确认,见在系统对话框中确认。
- 改动都有记录。 每次运行都记录在请求上;被改动的请求另存一份发往作答上游的版本,和所有存下的请求一样替换掉了密钥;「流量」页为被插件改动的请求加上标记。
- 用量有上限。 每次调用都有 CPU 时间、内存、输出和日志量的上限,同时存在的回答实例也有数量上限。插件在单独的线程池中运行,运行缓慢的插件不会拖住网关本身的工作。
- 只显示纯文本。 插件的名称、说明、设置项标签、日志与错误信息,在应用中一律按纯文本显示。
沙箱无法防范的情况#
插件本来就有权改动内容,沙箱无法判断一处改动是否正当。拥有 system 或 messages 权限的插件可以往提示词里写入指令;拥有 reply.text 的插件可以把回答改得误导人;拥有 params 的插件可以在密钥的可用模型中换用更贵的模型;拥有 reply.tool_calls 的插件可以改动客户端要执行的操作,而工具调用审查只能拦下其规则覆盖的写法。出站脱敏规则识别不了的密钥,插件看得到;这类密钥本来也会原样发往上游,可以在安全页添加自定义规则覆盖它的格式。
降低这些风险的办法:只授予插件所需的权限,安装前通读代码,并在请求详情中对照经过插件前后的请求。
应用自带的插件#
应用自带两个插件。它们和其他插件列在同一张表里,出厂均为关闭,打开和设置的方式与其他插件相同;应用按界面语言显示它们的名称和设置项标签。其中「WSL 路径转换」能改动工具调用,打开它要在系统对话框中确认,修改它的设置项、适用范围和出错时的处理方式则无须确认。所有插件都关闭时,core 不启动沙箱,保持关闭的插件不占内存。这些插件随 core 发布,远程 core 上同样有。两个插件都只处理对话,代码在 ThinkWatch Core 仓库的 crates/tw-gateway/src/plugin/defaults 目录中。
指定回答语言(reply-language)#
在系统提示词末尾附上一句固定的要求:用选定的语言回答,用户明确要求其他语言时除外。这句要求每次都相同,上游的提示词缓存照常命中。
- 权限:
system。 - 设置:
language,回答语言,出厂为简体中文。只接受语言名称(字母、空格、括号和连字符,不超过 40 个字符),设置写不进其他指令;填入其他内容时,插件在每个请求上出错。
出厂时的 manifest:
export const manifest = {
name: "Answer in a chosen language",
api: 1,
description: "Adds a fixed line to the end of the system prompt that asks the model to answer in the language set here.",
permissions: ["system"],
on_error: "reject",
settings: {
language: { type: "string", label: "Answer language", value: "简体中文" },
},
};
WSL 路径转换(wsl-paths)#
在 WSL 中运行的客户端打不开 C:\Users\… 这样的路径,在 Windows 上运行的客户端同样打不开 /mnt/c/Users/…。插件把工具调用中整个取值为盘符路径的参数,统一改写为客户端一侧能打开的写法;回答中的工具调用和对话历史中此前的工具调用都会改写,模型看到的始终是同一种写法。命令行中夹带的路径、对话文字和工具结果不做改动。
- 权限:
messages、reply.tool_calls。 - 设置:
windows_client,客户端是否运行在 Windows 上,出厂为false,即客户端运行在 WSL 中。转换方式写死在代码里,设置无法表达其他改写。
出厂时的 manifest:
export const manifest = {
name: "Convert WSL and Windows paths",
api: 1,
description: "Rewrites drive paths in tool-call arguments to the form the client can open (WSL /mnt/c/… or Windows C:\\…), in answers and in the conversation history.",
permissions: ["messages", "reply.tool_calls"],
on_error: "reject",
settings: {
windows_client: {
type: "boolean",
label: "The client runs on Windows (otherwise WSL)",
value: false,
},
},
};
删除的自带插件不会再次添加;代码被修改过的自带插件保持原样,在「设置」页所做的更改不算修改代码。除此之外,应用更新带来自带插件的新版本时,插件随之更新,保留开关状态、出错时的处理方式、适用范围,以及新版本仍然声明且类型不变的设置项的取值;新版本申请了更多权限或更多请求种类时,插件改为关闭。
升级须知#
升级到加入插件的版本后,首次启动会清空请求记录,包括保存的请求与回答报文。配置、密钥与上游不受影响,两个自带插件以关闭状态加入列表。