构建扩展
后端进程
启动一个托管的长期运行子进程,并通过类型化 RPC 与其通信。
有时一个命令不够——你需要一个持久进程:一个 PTY、一个 SSH 连接池、一个原生扫描器、一个长期运行的服务器。spawnBackend 从你的扩展启动一个托管的 Node/Deno/Bun 进程,并为你提供一个类型化的 RPC 句柄。这也是扩展既可以作为 CLI 也可以作为插件发布的接缝。
两个部分
前端 —— spawnBackend
import { spawnBackend } from "@kunkunsh/sdk";
interface TerminalBackend {
open(cmd: string): Promise<string>; // 返回会话 id
write(id: string, data: string): Promise<void>;
}
const conn = await spawnBackend<TerminalBackend>({
scriptPath: "$EXTENSION/dist/backend.js",
runtime: "node", // "auto" | "node" | "bun" | "deno"
});
const sessionId = await conn.api.open("bash");
await conn.api.write(sessionId, "ls\n");
await conn.destroy(); // 终止进程spawnBackend 返回 { api, backendId, destroy } —— api 是一个基于 kkrpc 的类型化代理。
后端 —— exposeBackend
// src/backend.ts (打包到 dist/backend.js)
import { exposeBackend } from "@kunkunsh/sdk/backend";
import { spawn } from "node:child_process";
const facade: TerminalBackend = {
async open(cmd) { /* 启动 pty,返回 id */ return "s1"; },
async write(id, data) { /* 写入 pty */ },
};
exposeBackend<TerminalBackend>(facade);你也可以向 spawnBackend 传递 localAPI,这样后端可以回调到前端(双向 kkrpc)。
清单权限
声明允许启动的具体脚本及其运行时:
{
"permissions": [
{ "permission": "backend", "allow": [{ "script": "$EXTENSION/dist/backend.js", "runtime": "node" }] }
]
}能力(capabilities)
额外的进程权限在每个 backend scope 的 capabilities 下声明。每一项都遵循同一形状——省略 = 无,true/"all" = 全量,数组 = 限定列表:
{
"permission": "backend",
"allow": [{
"script": "$EXTENSION/dist/backend.js",
"runtime": "auto",
"capabilities": {
"subprocess": "all", // 或 ["ps", "sysctl"]
"env": true, // 或 ["HTTP_PROXY"]
"sys": ["loadavg", "cpus", "systemMemoryInfo"],
"nativeAddons": ["node-pty/**/*.node"],
"ffi": ["native/libscanner.dylib"]
}
}]
}| 能力 | 形式 | 授予内容 |
|---|---|---|
subprocess | "all" | string[] | 子进程。程序列表可限定 Deno 的 --allow-run;Node 无法收窄(列表 ⇒ 仅 Deno)。 |
env | true | string[] | 环境访问。true 枚举(宿主已净化、不含机密的)env;列表还会把这些 key 从宿主 env 透传进来。被会展开 process.env 的依赖所需(如 systeminformation)。 |
sys | true | string[] | 系统信息(Deno --allow-sys 种类:loadavg、cpus、systemMemoryInfo、networkInterfaces、hostname、osUptime 等)。 |
workers | boolean | Worker 线程。 |
nativeAddons | string[](glob) | backend 加载的打包 .node 产物。 |
ffi | string[] | 通过 Deno.dlopen() 加载的原生库。 |
unsandboxed | boolean | 逃生舱——无进程沙箱。最后手段。 |
运行时与沙箱
某运行时只有在能至少按声明那样窄地强制每一项能力时才合格;否则会被拒绝,auto 转向下一个候选。
| 运行时 | 说明 |
|---|---|
| Deno | 最佳沙箱——限定的 --allow-read/write、按域 --allow-net、--allow-run/--allow-env/--allow-sys(全量或限定)。auto 优先选择。 |
| Node v25+ | Node 权限模型。全量 env: true/sys: true 隐式满足(Node 不拦截);限定的 env/sys/subprocess、域限定 net、FFI 除非 capabilities.unsandboxed 否则被拒绝。 |
| Bun | 无沙箱——仅允许使用 capabilities.unsandboxed。 |
"auto" 按顺序选择:Deno → Node。因此声明限定的 sys/env/subprocess 列表会把 auto 导向 Deno(Node 无法强制更窄的授予)。进程沙箱将你的清单范围作为运行时标志应用。
原生插件不能内联
.node 插件不能打包到 JS 文件中。请将插件的加载程序 + 平台 .node 文件与 dist/backend.js 放在一起,并通过 capabilities.nativeAddons 允许它们。或者惰性加载原生功能,使扩展即使没有该功能也能正常工作。
构建后端
将后端入口打包为独立的 Node/Deno 脚本:
await Bun.build({
entrypoints: ["./src/backend.ts"],
outdir: "./dist",
target: "node", format: "esm",
// 保持原生插件包外部,并将其文件复制到 dist/
});实际示例
wterm-terminal-demo—— 由基于spawnBackend的 Node PTY 支持的自定义视图kkterminal/space-lens—— 与 CLI 共享的生产级后端;参见 CLI + 插件