用 TypeScript 编写插件
以 TypeScript 的 AssemblyScript 兼容子集编写、经 asc 编译为 WASM 沙箱可加载单元——沙箱 ABI 与线性内存约定、@json 契约类型、出站脱敏实战与逐 chunk 处理、构建注册、限制与失败处置,面向 TS 团队的字符串治理场景
用 TypeScript 为灵渠平台编写插件,走的是 沙箱可加载单元 形态——以 TypeScript 的 AssemblyScript 兼容子集编写业务逻辑,经 asc 编译为 .wasm,跨边界以 JSON 交换、隔离执行、内存受限,单一产物跨环境加载。TypeScript 无法编译为原生 .so,因此本页所有路径都落在沙箱形态。若你的治理逻辑对延迟极敏感、或需要原生外部 IO,应改用原生形态,参见 用 Go 编写原生插件。
本页定位。 本页是 扩展点 SDK 参考 与 自定义插件开发 在 TypeScript 上的语言落地指南——只讲「照着 TS 写出可加载
.wasm插件」的具体路径,概念与契约(前置 / 后置对称 Hook、order、短路、逐 chunk、fail-open / fail-closed、能力边界)不再重述,需要时一句话回指。沙箱 ABI 与通用 host-guest 约定以 用 WebAssembly 编写插件 为权威,本页与其共用同一套约定。
1. 概述:为什么用 TypeScript 写插件
TypeScript 插件的目标读者,是已经用 TS 维护业务代码、希望复用同一套类型思维来写治理逻辑的团队。它的取舍很清晰:
| 维度 | TypeScript(沙箱 / WASM) | 原生(Go / .so) |
|---|---|---|
| 编写语言 | TS 的 AssemblyScript 兼容子集 | Go |
| 产物 | 单一 .wasm | 平台专属 .so |
| 跨环境移植 | 单产物处处可加载 | 须按目标环境分别构建 |
| 构建对齐 | 无要求 | 须与平台运行环境构建参数对齐 |
| 隔离性 | 沙箱隔离、内存受限 | 与平台同进程 |
| 执行开销 | 含 JSON 跨边界序列化开销 | 最低(同进程内调用) |
| 外部 IO | 无(沙箱内不可直接外联) | 可(同进程能力) |
一句话取舍:需要跨多套部署环境分发、希望沙箱隔离、逻辑以字符串 / JSON 处置为主(校验、脱敏、改写提示)——选 TypeScript;需要原生外部连接、对延迟极敏感、且部署环境可控——选 Go 原生形态。两种形态对外导出的符号语义一致,业务逻辑可在二者间平移。
2. 加载形态:沙箱可加载单元
TypeScript 插件落在 自定义插件开发 §4.1 所述的 沙箱可加载单元:
- 跨边界以结构化数据(JSON 字符串)交换,宿主与插件之间不共享内存对象图。
- 隔离执行、内存受限——插件在独立的线性内存里运行,无法触达宿主进程的其余部分。
- 单一产物跨环境加载——
.wasm不区分运行环境家族 / 处理器架构 / 构建工具版本,没有原生形态那种「构建参数对齐」要求。
这与原生形态正相反:原生单元随平台运行环境、同进程内直接调用,性能最优但要求与平台运行环境构建参数对齐(运行环境家族、处理器架构、构建工具版本一致)。如果这些对齐成本可接受且你需要原生能力,转到 用 Go 编写原生插件。
3. 工程脚手架
3.1 安装工具链与初始化
AssemblyScript 工具链与一个 AssemblyScript 兼容的 JSON 序列化库(如 json-as)即可起步:
# 安装编译器与 JSON 序列化库(二选一)
bun add -D assemblyscript json-as
npm install --save-dev assemblyscript json-as
# 初始化 AssemblyScript 脚手架(生成 assembly/ 目录与 asconfig.json)
bunx asinit .
npx asinit .3.2 目录与配置
asinit 生成的工程结构,调整为如下形态:
plugin-redact/
├── assembly/
│ └── index.ts # 插件入口:导出 ABI 函数与 Hook
├── build/
│ └── plugin.wasm # 构建产物(注册时引用)
├── asconfig.json # 编译目标与选项
└── package.json # 构建脚本asconfig.json 启用增量运行时(让 heap.alloc / heap.free 可用)并挂上 json-as 的编译期转换:
{
"targets": {
"release": {
"outFile": "build/plugin.wasm",
"optimizeLevel": 3,
"shrinkLevel": 1,
"noAssert": false
}
},
"options": {
"runtime": "incremental",
"exportRuntime": true,
"transform": ["json-as/transform"]
}
}package.json 的构建脚本:
{
"scripts": {
"asbuild": "asc assembly/index.ts --target release --transform json-as/transform -o build/plugin.wasm"
}
}不设置
bindings选项,asc仅产出裸.wasm、不生成宿主侧 JS /.d.ts胶水——平台按本页 ABI 直接加载该.wasm。transform: json-as/transform是json-as的硬性要求,缺它则@json装饰的类不会获得序列化能力。
4. 沙箱 ABI 与内存约定
平台以一套固定的 host-guest ABI 加载 .wasm。插件作者只需照表导出函数、遵守内存约定即可——这套约定与 用 WebAssembly 编写插件(沙箱可加载单元的通用 ABI 权威页)完全一致。
4.1 必须导出的函数
| 导出函数 | 签名 | 职责 |
|---|---|---|
malloc | malloc(size:u32)->u32 | 供宿主写入入参时申请线性内存 |
free | free(ptr:u32) 或 free(ptr:u32,size:u32) | 释放申请的内存 |
get_name | get_name()->u64 | 返回打包指针,指向插件名字符串 |
init | init(config_ptr:u32,config_len:u32)->i32 | 解析配置、一次性初始化,成功返回 0 |
pre_hook | pre_hook(input_ptr:u32,input_len:u32)->u64 | 前置 Hook,返回打包指针指向出参 JSON |
post_hook | post_hook(input_ptr:u32,input_len:u32)->u64 | 后置 Hook,同上 |
cleanup | cleanup()->i32 | 释放资源,成功返回 0 |
这组小写函数名即抽象契约符号 get_name / init / pre_hook / post_hook / cleanup 在沙箱 ABI 上的落地(见 自定义插件开发 §2),外加沙箱形态特有的 malloc / free 内存管理对。
4.2 线性内存与打包指针
复杂数据一律以 JSON 字符串跨边界。一次 Hook 调用的数据流:
宿主 malloc(len) ──▶ 写入入参 JSON ──▶ 传 ptr/len ──▶ 插件 readString 解析
│
宿主读取出参后 free(ptr) ◀── 返回打包指针 ◀── malloc+writeString 出参 JSON ◀── 处置返回值用打包 u64:高 32 位存指针、低 32 位存长度。宿主收到后拆包、按长度读取那段 JSON、再 free 掉这块缓冲:
打包返回值 u64
┌─────────────────┬─────────────────┐
│ 高 32 位 │ 低 32 位 │
│ 指针 ptr │ 长度 len │
└─────────────────┴─────────────────┘流式不另设导出函数。 按 扩展点 SDK §2.3,流式响应下
post_hook会被 多次调用(每个 chunk 一次,stream.is_stream/chunk_index/is_last标识进度),而不是另设一个单独的 chunk 处理导出。前置pre_hook在一次请求中仍只调用一次。
下图给出 host-guest 一次后置调用的完整往返:
4.3 readString / writeString 工具函数
字符串跨边界的两个核心工具,用 String.UTF8.encode / decode 配 load<u8> / store<u8> 实现:
// 高 32 位存指针、低 32 位存长度
function pack(ptr: u32, len: u32): u64 {
return ((ptr as u64) << 32) | (len as u64);
}
// 从线性内存的 ptr 处读取 len 字节,解码为字符串
function readString(ptr: u32, len: u32): string {
const buf = new ArrayBuffer(len);
const dest = changetype<usize>(buf);
for (let i: u32 = 0; i < len; i++) {
store<u8>(dest + i, load<u8>(ptr + i));
}
return String.UTF8.decode(buf, false);
}
// 把字符串编码为 UTF-8、写入新申请的缓冲、返回打包指针
function writeString(s: string): u64 {
const buf = String.UTF8.encode(s, false);
const len = buf.byteLength as u32;
const ptr = malloc(len);
const src = changetype<usize>(buf);
for (let i: u32 = 0; i < len; i++) {
store<u8>(ptr + i, load<u8>(src + i));
}
return pack(ptr, len);
}malloc / free 直接转交 AssemblyScript 增量运行时的堆:
export function malloc(size: u32): u32 {
return heap.alloc(size) as u32;
}
export function free(ptr: u32): void {
heap.free(ptr);
}5. 契约类型:用 @json 映射 Hook 入参出参
Hook 的入参出参以 @json 装饰的 class 映射,字段名 严格等于 扩展点 SDK §2 的契约 JSON——action / request / short_circuit_response / reject_reason / context,以及 response / usage / stream / block_reason 等。不需要解析的子树(如 messages、context、short_circuit_response)可用 JSON.Raw 原样透传,省去逐字段建模:
import { JSON } from "json-as";
// ── 前置 Hook 入参(节选关键字段) ──
@json
class RequestBody {
model: string = "";
messages: JSON.Raw = JSON.Raw.from("[]"); // 透传,不逐条建模
stream: bool = false;
}
@json
class Routing {
complexity: string = "";
candidate_model: string = "";
}
@json
class PreInput {
phase: string = "";
request_id: string = "";
api_key_id: string = "";
request: RequestBody = new RequestBody();
routing: Routing = new Routing();
context: JSON.Raw = JSON.Raw.from("{}");
}
// ── 前置 Hook 出参 ──
@json
class RejectReason {
code: string = "";
message: string = "";
}
@json
class PreOutput {
action: string = "continue"; // continue | rewrite | short_circuit | reject
request: JSON.Raw | null = null;
short_circuit_response: JSON.Raw | null = null;
reject_reason: RejectReason | null = null;
context: JSON.Raw = JSON.Raw.from("{}");
}
// ── 后置 Hook 入参(节选关键字段) ──
@json
class ResponseBody {
model: string = "";
content: string = "";
finish_reason: string = "";
}
@json
class StreamInfo {
is_stream: bool = false;
chunk_index: i32 = 0;
is_last: bool = false;
}
@json
class PostInput {
phase: string = "";
request_id: string = "";
api_key_id: string = "";
response: ResponseBody = new ResponseBody();
stream: StreamInfo = new StreamInfo();
context: JSON.Raw = JSON.Raw.from("{}");
}
// ── 后置 Hook 出参 ──
@json
class RedactedResponse {
content: string = "";
}
@json
class PostOutput {
action: string = "pass"; // pass | redact | block
response: RedactedResponse | null = null;
}
action取值的完整语义见 扩展点 SDK §2:前置continue/rewrite/short_circuit/reject,后置pass/redact/block。本页代码原样复用这些字符串,不另造取值。
6. 完整最小示例:直通插件
下面是一个完整、可编译的 assembly/index.ts——「直通」插件什么也不改,只跑通「加载 → 挂载 → 调用 → 卸载」全链路:pre_hook 放行并向后置写一个 context 标记,post_hook 逆序回收后放行。
import { JSON } from "json-as";
// ── 内存与字符串工具 ──
export function malloc(size: u32): u32 { return heap.alloc(size) as u32; }
export function free(ptr: u32): void { heap.free(ptr); }
function pack(ptr: u32, len: u32): u64 {
return ((ptr as u64) << 32) | (len as u64);
}
function readString(ptr: u32, len: u32): string {
const buf = new ArrayBuffer(len);
const dest = changetype<usize>(buf);
for (let i: u32 = 0; i < len; i++) store<u8>(dest + i, load<u8>(ptr + i));
return String.UTF8.decode(buf, false);
}
function writeString(s: string): u64 {
const buf = String.UTF8.encode(s, false);
const len = buf.byteLength as u32;
const ptr = malloc(len);
const src = changetype<usize>(buf);
for (let i: u32 = 0; i < len; i++) store<u8>(ptr + i, load<u8>(src + i));
return pack(ptr, len);
}
// ── 契约类型(字段名严格对齐 Hook 契约) ──
@json
class PreInput {
phase: string = "";
request_id: string = "";
api_key_id: string = "";
context: JSON.Raw = JSON.Raw.from("{}");
}
@json
class Marker {
seen_by: string = "直通插件";
request_id: string = "";
}
@json
class PreOutput {
action: string = "continue";
context: JSON.Raw = JSON.Raw.from("{}");
}
@json
class PostOutput {
action: string = "pass";
}
// ── 模块级状态(init 写入、cleanup 清空) ──
let pluginConfig: string = "";
// ── 生命周期与 Hook 导出 ──
export function get_name(): u64 {
return writeString("直通插件");
}
export function init(config_ptr: u32, config_len: u32): i32 {
pluginConfig = readString(config_ptr, config_len);
return 0; // 成功
}
export function pre_hook(input_ptr: u32, input_len: u32): u64 {
const evt = JSON.parse<PreInput>(readString(input_ptr, input_len));
const marker = new Marker();
marker.request_id = evt.request_id;
const out = new PreOutput();
out.action = "continue";
out.context = JSON.Raw.from(JSON.stringify<Marker>(marker)); // 传给配对后置
return writeString(JSON.stringify<PreOutput>(out));
}
export function post_hook(input_ptr: u32, input_len: u32): u64 {
// 直通:放行出站响应(逆序收尾,前置写入的 context 由宿主回传)
const out = new PostOutput();
out.action = "pass";
return writeString(JSON.stringify<PostOutput>(out));
}
export function cleanup(): i32 {
pluginConfig = "";
return 0; // 成功
}直通骨架的价值在于先跑通全链路,再逐步把
action从continue/pass改为rewrite/short_circuit/redact/block,填入真实治理逻辑——下一节就是这样的实战。
7. 实战示例:出站敏感信息脱敏插件
一个挂在 后置段 的治理插件:复核上游返回内容,对其中的邮箱、手机号、证件号等敏感片段就地脱敏,返回 action=redact 与脱敏后的 response.content。它只在治理面改写出站文本——不重新生成、不加工合成内容(能力边界见 §13)。
7.1 脱敏逻辑
正则能力由一个 AssemblyScript 兼容的正则库提供(AssemblyScript 标准库不内建完整 RegExp,见 §10)。脱敏用 redact 三遍替换实现:
import { JSON } from "json-as";
import { RegExp } from "assemblyscript-regex"; // AS 兼容正则库
// 三类敏感片段的脱敏模式
const EMAIL = new RegExp("[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}", "g");
const PHONE = new RegExp("1[3-9][0-9]{9}", "g");
const IDNO = new RegExp("[0-9]{17}[0-9Xx]", "g");
// 用全局正则就地替换所有匹配片段为掩码(exec 循环,不依赖 replace)
function maskAll(text: string, re: RegExp, mask: string): string {
let result = "";
let cursor = 0;
let m = re.exec(text);
while (m != null) {
const start = m.index;
const matched = m.matches[0];
result += text.substring(cursor, start);
result += mask;
cursor = start + matched.length;
m = re.exec(text); // 全局标志推进 lastIndex
}
result += text.substring(cursor);
return result;
}
function redact(text: string): string {
let out = maskAll(text, EMAIL, "[已脱敏·邮箱]");
out = maskAll(out, PHONE, "[已脱敏·手机号]");
out = maskAll(out, IDNO, "[已脱敏·证件号]");
return out;
}后置 Hook 主体——只有真正改动了内容才返回 redact,否则放行:
@json
class ResponseBody { model: string = ""; content: string = ""; finish_reason: string = ""; }
@json
class StreamInfo { is_stream: bool = false; chunk_index: i32 = 0; is_last: bool = false; }
@json
class PostInput {
phase: string = "";
request_id: string = "";
response: ResponseBody = new ResponseBody();
stream: StreamInfo = new StreamInfo();
context: JSON.Raw = JSON.Raw.from("{}");
}
@json
class RedactedResponse { content: string = ""; }
@json
class PostOutput { action: string = "pass"; response: RedactedResponse | null = null; }
export function post_hook(input_ptr: u32, input_len: u32): u64 {
const evt = JSON.parse<PostInput>(readString(input_ptr, input_len));
// 仅对出站内容做治理面脱敏,不重新生成内容。
// 流式下本函数被多次调用,每次只拿到当前 chunk,就地脱敏即可。
const cleaned = redact(evt.response.content);
const out = new PostOutput();
if (cleaned != evt.response.content) {
out.action = "redact";
const r = new RedactedResponse();
r.content = cleaned;
out.response = r;
} else {
out.action = "pass";
}
return writeString(JSON.stringify<PostOutput>(out));
}7.2 配对前置 Hook 与上下文
后置脱敏可配一个轻量前置 Hook:放行(continue)并向 context 写一个标记,标明本请求已纳入脱敏治理;该标记在配对后置 Hook 中由宿主原样回传(对称传递、逆序收尾,见 扩展点 SDK §1)。
@json
class PreInput { phase: string = ""; request_id: string = ""; context: JSON.Raw = JSON.Raw.from("{}"); }
@json
class GuardMarker { scanned: bool = true; request_id: string = ""; }
@json
class PreOutput { action: string = "continue"; context: JSON.Raw = JSON.Raw.from("{}"); }
export function pre_hook(input_ptr: u32, input_len: u32): u64 {
const evt = JSON.parse<PreInput>(readString(input_ptr, input_len));
const marker = new GuardMarker();
marker.request_id = evt.request_id;
const out = new PreOutput();
out.action = "continue";
out.context = JSON.Raw.from(JSON.stringify<GuardMarker>(marker));
return writeString(JSON.stringify<PreOutput>(out));
}7.3 流式逐 chunk 处理
按 扩展点 SDK §2.3,当 stream.is_stream 为 true 时,本后置 Hook 会被 逐 chunk 多次调用:每次入参 response.content 只是当前一块、chunk_index 递增、末块 is_last 为 true。脱敏天然适配这种逐块模型——每块就地脱敏后随即放行,违规片段不会先于护栏抵达客户端。
stream 字段 | 逐 chunk 时的用法 |
|---|---|
is_stream | 为 true 时进入逐块处置分支 |
chunk_index | 当前块序号,用于跨块计数 / 落账归并 |
is_last | 末块标识;计量类汇总通常在此块完成 |
跨块边界提醒。 单块就地脱敏对落在 chunk 边界处、被切成两半的敏感串可能漏判。若治理要求覆盖跨块片段,可在
context里缓存上一块的尾部若干字符与本块拼接后再匹配,并仅输出确认安全的前缀;对边界要求极严、或正则负担较重的场景,更适合改用 原生形态 配合完整正则引擎。request_id在整条流(含全部 chunk)中保持一致,可用作跨块状态的归并键。
8. 构建与产物
编译为发布版 .wasm:
# 经 package.json 脚本
bun run asbuild
# 或直接调用编译器
bunx asc assembly/index.ts --target release --transform json-as/transform -o build/plugin.wasm产物即 build/plugin.wasm,单文件、自包含。沙箱形态 无平台对齐要求:同一个 .wasm 可在不同运行环境家族 / 处理器架构 / 构建工具版本的平台实例上加载,无需为每种环境分别构建。如需进一步压缩体积,可对产物追加一次 wasm-opt(可选)。
9. 注册与加载
注册流程与 自定义插件开发 §4 完全一致,沙箱形态只是把 artifact_ref 指向 .wasm。
先确认实例支持动态加载:
curl https://<平台入口地址>/api/v1/deployment \
-H "Cookie: <控制台会话凭证>"{
"deploy_mode": "private",
"deploy_status": "ready",
"capabilities": {
"dynamic_plugins": true
}
}capabilities.dynamic_plugins 为 true 后,注册脱敏插件(后置段):
curl https://<平台入口地址>/api/v1/pipeline/extensions \
-X POST \
-H "Content-Type: application/json" \
-H "Cookie: <控制台会话凭证>" \
-d '{
"name": "出站脱敏插件",
"artifact_ref": "wasm://plugins/redact@1.2.0",
"node_id": "<安全护栏节点标识>",
"hook_type": "post",
"placement": "post",
"order": 10,
"enabled": true,
"config": {
"mask_email": true,
"mask_phone": true,
"mask_id": true
},
"fail_policy": "fail_closed"
}'| 字段 | 取值说明 |
|---|---|
name | 须与 get_name 返回一致 |
artifact_ref | 指向 .wasm 的加载位置 + 版本标识 |
hook_type / placement | 脱敏挂后置:均为 post(须匹配) |
order | 段位内位次,值小者先 |
config | 传给插件 init 的初始配置 |
fail_policy | 脱敏属安全护栏类,取 fail_closed |
读取已注册清单用 GET /api/v1/pipeline/extensions(见 扩展点 SDK §3)。热替换:更新 artifact_ref 的版本标识(如 redact@1.2.0 → redact@1.3.0)即触发平台重载该插件——先以新 .wasm 完成 init,再切换执行,旧实例 cleanup 收尾。
10. 限制
沙箱形态的能力边界主要来自 WASM 隔离与 AssemblyScript 子集:
- 内存受限、隔离执行:插件在独立线性内存运行,不能触达宿主进程其余部分。
- 无原生外部 IO:沙箱内不能直接访问文件、网络套接字、环境变量。需要外联能力(如调用外部服务、读本地配置文件)的治理逻辑,应改用 原生形态。
- AssemblyScript 是 TS 的子集:并非任意 TS / npm 运行时库都能编译。
AssemblyScript 可用 / 不可用速查:
| 类别 | 可用 | 不可用 |
|---|---|---|
| 语法 | class、函数、泛型、整数 / 浮点类型、bool | any、union 类型(受限)、动态原型扩展 |
| 标准库 | String、ArrayBuffer、TypedArray、Map、数学 / 字符串 / 数组操作 | 完整 JS RegExp 内建(需引入 AS 兼容正则库) |
| 依赖 | AS 兼容库(如 json-as、AS 正则库) | 依赖 Node / 浏览器 API 的普通 npm 运行时包 |
| 运行期 | 线性内存读写、heap.alloc / heap.free | eval、Proxy、动态反射、运行期装饰器元编程 |
| IO | 仅经 Hook 入参出参的 JSON 交换 | 文件 / 网络 / 环境变量等原生外部 IO |
经验法则:以字符串 / JSON 处置为主、纯计算、可自包含的治理逻辑,沙箱形态足够;一旦需要外联或重型库,转 原生形态。
11. 失败处置与启停
失败处置、启停、灰度、热替换均沿用 自定义插件开发 §5 的统一规则,沙箱形态无特殊性:
| 情形 | 处置 |
|---|---|
加载 / init 返回非 0 | 该插件不纳入编排;其余插件与内建管道照常运行 |
前置 Hook 返回 reject | 请求被结构化拒答,不再转发上游 |
| 前置 Hook 执行报错(含 wasm trap) | 按 fail_policy:fail_open 放行、fail_closed 拒绝;护栏类默认 fail_closed |
后置 Hook 返回 block | 出站响应被拦截;已发生的计量按实落账 |
| 后置 Hook 执行报错 | 不影响已完成的上游调用与计量;按 fail_policy 决定是否放行出站内容,护栏类默认拦截 |
| 短路路径上的后置异常 | 逆序收尾继续执行其余后置 Hook,单插件异常不阻断整条收尾链 |
- 启停:
enabled控制单个插件的执行状态,停用即运行时跳过——便于灰度与回退,无需注销。 - 版本热替换:更新
artifact_ref版本标识触发重载(见 §9)。 - 沙箱隔离的额外好处:插件内的 wasm trap 被限制在其线性内存范围内,按
fail_policy处置,不会连带拖垮宿主进程;安全相关的脱敏 / 拦截一律fail_closed——「宁可拒绝,不放行未经复核的内容」。
12. 常见问题
为什么 TS 要走 AssemblyScript?
因为沙箱形态的产物是 .wasm,而普通 TypeScript 依赖 Node / 浏览器运行时、无法直接编译为这种自包含的线性内存模块。AssemblyScript 是 TS 的静态类型子集,专门面向 WASM 目标编译,让你以熟悉的 TS 语法写出可加载的 .wasm。
能用普通 npm 包吗?
只能用 AssemblyScript 兼容的库(如 json-as、AS 正则库)。依赖 Node / 浏览器 API、或大量使用动态特性的普通 npm 运行时包不能编译进 .wasm(见 §10 速查表)。
.wasm 能跨环境吗?
能。沙箱形态单一产物处处可加载,没有原生形态的「构建参数对齐」要求——同一个 .wasm 在不同运行环境家族 / 处理器架构 / 构建工具版本的实例上都能加载。
流式下跨 chunk 的敏感串怎么处理?
单块就地脱敏会漏掉落在 chunk 边界处被切开的片段。可借 context 缓存上一块尾部字符与本块拼接后再匹配(见 §7.3);边界要求极严或正则负担重时,建议改用 原生形态。
何时该改用原生形态? 需要原生外部 IO、对延迟极敏感、或要用完整正则 / 重型库时——转 用 Go 编写原生插件。两种形态导出的符号语义一致,逻辑可平移。
13. 能力边界
TypeScript 插件与所有扩展点共享同一条硬性边界(见 扩展点 SDK §6):
插件只在控制面 / 治理面对请求与响应施加处置——校验、路由提示、缓存判定、脱敏、拦截、计量采集——不对结果内容做实质性的生成、加工或合成。内容生成始终由路由选定的上游模型完成。前置 Hook 的
rewrite与后置 Hook 的redact都限于治理面改写(如去除敏感片段、补充结构化路由提示),不构成对生成内容的实质性创作。
本页的脱敏插件正是这条边界的典型落地:它只把已有出站文本里的敏感片段替换为掩码,既不补写也不重写语义内容。注册一个试图替代上游做内容生成的 TS 插件,不符合扩展点 SDK 的能力定义。
相关链接
- 概念与编排:三类治理中间件、执行位次、条件分支、短路语义与流式逐 chunk 处理。
- 扩展点 SDK 参考:前置 / 后置对称 Hook 模型、Hook 签名与生命周期、注册启停、编排约束与失败处置、能力边界声明。
- 自定义插件开发:插件模型、生命周期符号、两种加载形态、构建与动态加载、注册启停。
- 用 Go 编写原生插件:原生可加载单元——同进程、性能最优、可原生外部 IO,须与平台运行环境构建参数对齐。
- 用 WebAssembly 编写插件:沙箱可加载单元的通用 host-guest ABI 与内存约定,以 Rust 为主示例(TS 与 Rust 共用同一套约定)。
- API 参考:管道编排与扩展点接口的完整端点契约。