流式进程与 sidecar
流式进程与 sidecar
Section titled “流式进程与 sidecar”「跑命令并持续消费输出」是插件最常见的高级需求。宿主提供两条路径:
流式进程(ctx.process / hosts.execStream,纯 JS 即可)和
sidecar(随包分发的原生二进制,需要原生能力时用)。
流式进程(ctx.process / hosts.execStream)
Section titled “流式进程(ctx.process / hosts.execStream)”hosts.exec / execLocal 是一次性的:等进程退出拿全部输出,撑不起
日志 follow、拉取进度、事件流。流式原语(本地进程与 SSH exec 通道同构)
返回同一个 ProcessHandle:
process: { spawn(cmd: string, args: string[], opts?: { cwd?: string; env?: Record<string, string>; }): Promise<ProcessHandle>;}hosts: { execStream(hostId: string, command: string): Promise<ProcessHandle>;}interface ProcessHandle { readonly id: string; write(data: string): Promise<void>; // 写入 stdin(本地进程 / SSH 通道同构) kill(): Promise<void>; // 本地 kill 子进程;SSH 关闭 exec 通道 onData(cb: (chunk: ProcChunk) => void): Disposer; onExit(cb: (info: { code: number | null; error?: string }) => void): Disposer;}interface ProcChunk { seq: number; data: string; stream: "stdout" | "stderr"; // stdout / stderr 分流}要点:
process.spawn以 argv 方式拉起本地进程(不做 shell 展开;需要 shell 语义时显式spawn("/bin/sh", ["-c", cmd]))。- chunk 带 stdout / stderr 分流——构建进度通常在 stderr,解析必须 分流。
kill()语义:本地终止子进程;SSH 关闭 exec 通道(远端进程收到 EOF / SIGPIPE)。- 能力门禁(L1):
spawn与execStream都要求用户授予process能力(未获授予即明确报错,插件其余功能可用)。 - 孤儿防护:插件去激活时,宿主强制回收其名下全部进程,无需插件 自行清理。
const handle = await ctx.process.spawn("docker", ["logs", "-f", id], { cwd: "/" });const offData = handle.onData(({ data, stream }) => { if (stream === "stderr") logPanel.append(data, "err");});const offExit = handle.onExit(({ code, error }) => { offData(); offExit(); if (code !== 0) ctx.ui.toast.error({ title: `进程退出 ${code ?? error}` });});取消语义:一次性命令的取消(如旧实现里的 *_stop(opId))在流式模型里
变成 handle.kill() + AbortController,模式一一对应。
sidecar 原生能力桥
Section titled “sidecar 原生能力桥”sidecar 让插件把「需要原生能力」的部分(系统调用、新协议、重计算)放进
一个随包分发的本地二进制,宿主以子进程托管,插件经 ctx.sidecar.call
调用。它是信任模型的升级而非替代:普通外部插件仍是纯 JS;声明 sidecar
的插件需要用户单独确认「允许运行原生代码」。
manifest 声明
Section titled “manifest 声明”{ "id": "sysinfo", "apiVersion": 3, "capabilities": ["sidecar"], // 声明 sidecar 隐含 process(L1) "sidecar": { "binaries": { "darwin-aarch64": "bin/sysinfo-darwin-aarch64", "darwin-x86_64": "bin/sysinfo-darwin-x86_64", "windows-x86_64": "bin/sysinfo-windows-x86_64.exe" }, "args": [] // 可选,固定启动参数 }}当前平台无对应二进制 → 插件可加载,但 ctx.sidecar.call 返回明确
错误(见下)。
生命周期与协议
Section titled “生命周期与协议”- 生命周期:宿主在插件激活时拉起子进程,去激活 / 卸载 / 应用退出时 回收。进程异常退出(退出码非 0)→ 标记该插件 sidecar 不可用,前端 toast 提示。
- 协议:stdio 上 NDJSON 编码的 JSON-RPC 2.0,一行一条请求 / 响应:
- 请求
{"jsonrpc":"2.0","id":<u64>,"method":"...","params":{...}} - 响应
{"jsonrpc":"2.0","id":<u64>,"result":...}或{"jsonrpc":"2.0","id":<u64>,"error":{"code":..,"message":".."}} - id 由宿主递增分配,按 id 关联应答;超时(默认 10s,call 可传) reject。
- 请求
- 健康信号:宿主以进程存活为健康信号——二进制侧不需要回应 keepalive,崩溃即整插件报错。
调用与错误分支
Section titled “调用与错误分支”sidecar: { call<T = unknown>(method: string, params?: unknown, timeoutMs?: number): Promise<T>;}// 返回二进制回应的 result(Rust 侧 JSON 反序列化后透传)const info = await ctx.sidecar.call("getSystemInfo", { extra: true }); // params 可选const slow = await ctx.sidecar.call("heavyJob", { n: 1e6 }, 30_000); // timeoutMs 可选,默认 10000错误分支(各自抛明确错误,插件应捕获并展示):
| 场景 | 行为 |
|---|---|
manifest 未声明 sidecar |
报「插件未声明 sidecar」 |
| 已声明但未获用户确认(L1 未授予) | 报「未获用户确认运行原生代码:请到设置 → 插件确认信任」 |
| 当前平台无对应二进制 | 报「当前平台无对应二进制(需要 <os>-<arch> 键)」 |
| 子进程未能启动 / 崩溃 | 整插件报错,call reject |
官方模板的 sidecar-sysinfo/ 是可运行的 sidecar 示例:极简二进制实现
getSystemInfo 方法,插件视图调用 ctx.sidecar.call("getSystemInfo")
并渲染。构建 / 安装说明见该模板 README。
何时用 sidecar,何时用 process
Section titled “何时用 sidecar,何时用 process”| 场景 | 选择 |
|---|---|
| 调用已存在的 CLI / 命令(docker、git、ssh …) | ctx.process.spawn / hosts.execStream |
| 需要系统调用、新协议、重计算,没有现成命令 | sidecar 二进制 |
| 只是想「拿个输出」 | hosts.exec / execLocal 即可,无需流式 |