Worker 是插件在独立进程中运行的后台逻辑,适合插件私有的解析、转换和计算。界面和 Agent 通过 operation 调用它;它通过宿主代理(Host Broker)访问文件、网络和服务,自己不直接获得这些能力。
两类 Worker
| 插件 Worker | 全局 Worker 服务 | |
|---|---|---|
| 分发方式 | 随 .seplugin 一起声明和安装 | 独立的 .seworker 包,单独签名、安装和管理生命周期 |
| 服务对象 | 只服务当前插件 | 可被多个插件按服务 id 和 API 主版本依赖 |
| 权限 | 通过宿主代理获得本插件授予的权限 | 不继承调用方插件的权限 |
| 适用场景 | 插件私有的解析、转换、后台计算 | 多个插件共享、版本独立的重型能力,例如 OCR、媒体处理 |
插件只能在 required_services 中声明对服务的依赖,不能静默下载、安装、升级或替换服务。
用官方 Node SDK 编写
官方 Node SDK 处理握手、健康检查、取消和宿主调用,你只需要定义业务 operation:
import { runNodeWorker } from "./sdk/worker/index.mjs"; // 随包放在 payload/worker/ 下
runNodeWorker({
operations: {
async "document.extract"(input, context) {
const response = await context.callHost("host.net.fetch", {
url: "https://api.example.com/v1/extract",
method: "POST",
body: JSON.stringify(input),
response_type: "json"
});
return { status: response.status, body: response.body };
}
},
health: () => ({ status: "ready" })
});
operation 处理函数收到的 context 包含:identity(plugin_id、plugin_version、worker_id)、signal(AbortSignal)和 callHost(method, params)。握手时 SDK 会把 operations 的键排序后作为 methods 上报,并固定声明 features: ["cancel", "host-broker"]。
payload/worker/ 下并进入 files。不用 SDK 时,可以按下文协议手写。协议 2.0
宿主与 Worker 之间通过标准输入输出交换 UTF-8 NDJSON 格式的 JSON-RPC 2.0 消息:一行一个完整的消息帧。
{"jsonrpc":"2.0","id":"req-1","method":"worker.invoke","params":{"operation":"document.extract","input":{"file_token":"token"}}}
{"jsonrpc":"2.0","id":"req-1","result":{"output":{"text":"example"}}}
消息上限
单个消息帧上限为 32 MiB。超限时的处理取决于方向:
- Worker 写出的帧超限:宿主结束 Worker 进程(
PLUGIN_WORKER_FRAME_TOO_LARGE); - Worker 读入的帧超限:只有本次调用失败,进程保留;
- 宿主对 Worker 调用的应答超限:返回 JSON-RPC 错误
-32004。
其他上限:host.fs.read 单块 1 MiB。Worker 调用宿主网络请求(host.net.fetch)时,内联返回(text / json / base64)上限 20 MiB,blob 上限 32 MiB。更大的数据请分块读取,或用文件令牌、Blob 传递。
生命周期
- 宿主创建隔离进程,发送
worker.initialize。 - Worker 回显身份、协议版本和业务方法清单。
- 宿主校验
protocol_version、plugin_id、worker_id、methods、features五个字段后进入就绪状态。 - 调用按各方法的时限执行。
- 界面调用
worker.cancel时,宿主中止该调用并终止整个 Worker 进程;下一次调用会重新启动进程。 - 宿主目前不发送
worker.shutdown,停止就是终止进程。临时资源要做到“进程被杀也不留脏状态”。
握手应答
手写 Worker 时,worker.initialize 的 result 必须同时包含以下五个字段,缺任何一个都会以 PLUGIN_WORKER_HANDSHAKE_INVALID 拒绝:
{
"protocol_version": "2.0",
"plugin_id": "<原样回显 params.plugin_id>",
"worker_id": "<原样回显 params.worker_id>",
"methods": ["document.extract"],
"features": []
}
features是必填数组,宿主不读取其中的值,手写时写[]即可。methods只列真实的业务 operation,不要把worker.health、worker.invoke等生命周期方法放进去。空数组表示这个 Worker 不接受任何业务调用。worker.health的result.status只能是ready或degraded,其他值会以PLUGIN_WORKER_HEALTH_INVALID拒绝。
方法
| 方法 | 方向 | 所需权限 | 时限 | 可取消 |
|---|---|---|---|---|
worker.initialize | 宿主 → Worker | — | 10 秒 | 否 |
worker.health | 宿主 → Worker | — | 5 秒 | 否 |
worker.invoke | 宿主 → Worker | — | 30 秒 | 是 |
worker.shutdown | 宿主 → Worker | — | 5 秒 | 否 |
host.fs.stat | Worker → 宿主 | fs.source.read | 5 秒 | 否 |
host.fs.read | Worker → 宿主 | fs.source.read | 30 秒 | 是 |
host.net.fetch | Worker → 宿主 | net.fetch | 120 秒 | 是 |
host.blob.read | Worker → 宿主 | storage.blob | 10 秒 | 否 |
host.blob.delete | Worker → 宿主 | storage.blob | 5 秒 | 否 |
host.schedule.upsert | Worker → 宿主 | — | 5 秒 | 否 |
host.schedule.remove | Worker → 宿主 | — | 5 秒 | 否 |
host.schedule.pause | Worker → 宿主 | — | 5 秒 | 否 |
host.service.invoke | Worker → 宿主 | service.invoke | 30 秒 | 是 |
通知:$/cancelRequest(双向)、worker.progress(Worker → 宿主)。注意宿主取消调用时的实际做法是终止进程,Worker 不会收到 $/cancelRequest,不需要实现协作式取消。
host.schedule.* 只能管理清单 triggers 中声明过的触发器,不能低于声明的最小间隔,否则报 PLUGIN_SCHEDULE_TRIGGER_NOT_DECLARED 或 PLUGIN_SCHEDULE_INTERVAL_INVALID。
JSON-RPC 错误码
| 名称 | 数值 | 可重试 |
|---|---|---|
WORKER_PARSE_ERROR | -32700 | 否 |
WORKER_INVALID_REQUEST | -32600 | 否 |
WORKER_METHOD_NOT_FOUND | -32601 | 否 |
WORKER_INVALID_PARAMS | -32602 | 否 |
WORKER_INTERNAL_ERROR | -32603 | 是 |
WORKER_PERMISSION_DENIED | -32001 | 否 |
WORKER_DEADLINE_EXCEEDED | -32002 | 是 |
WORKER_CANCELLED | -32003 | 是 |
WORKER_PAYLOAD_TOO_LARGE | -32004 | 否 |
WORKER_SERVICE_UNAVAILABLE | -32005 | 是 |
Worker 自己返回的 JSON-RPC 错误,在界面和 Agent 侧统一显示为 PLUGIN_WORKER_REMOTE_ERROR:<code>:<message>:<data>。业务分支请按数值或自定义的 data 判断,不要匹配 message。
实现规则
- stdout 只写协议帧,一行一个;业务日志一律写 stderr。不要输出横幅、进度条、调试打印或多行 JSON。stdout 出现非 JSON 行时,宿主返回
PLUGIN_WORKER_INVALID_RESPONSE。 - 原样回传请求 id;对未知方法返回稳定的错误。
- 启动阶段的异常写到 stderr:Worker 应答前退出时,宿主返回
PLUGIN_WORKER_EXITED,并附上 stderr 的最后几行。 - 不要在主线程做长时间阻塞的同步调用,重活放进
worker_threads。调用超时后宿主会补发一次worker.health:有应答报PLUGIN_WORKER_TIMEOUT,进程保留;无应答报PLUGIN_WORKER_UNRESPONSIVE,进程被终止。 - 文件正文和大块二进制用令牌或 Blob 传递,不要塞进 RPC 帧。
- 幂等方法不能因为重试而重复产生副作用。
- 不要在环境变量、日志、KV 或错误信息中保存令牌。
崩溃与隔离
崩溃和协议违规(非 JSON 输出、超大帧、非法应答等)计入崩溃次数:同一 Worker 在 5 分钟内退出 3 次就会被隔离,宿主不再启动它,调用直接返回 PLUGIN_WORKER_QUARANTINED。超时不计入。停用后再启用、卸载或覆盖安装插件会立即清零;开发时改完代码重新安装,或“停用 → 启用”一次即可。
执行策略
| 策略 | 清单声明 | 行为 |
|---|---|---|
沙箱(sandbox) | 默认 | 在系统沙箱中运行,隔离失败即拒绝启动。默认没有文件、网络、钥匙串或其他插件数据的访问权,唯一例外是插件私有数据目录。child_process 被禁用,不能启动子进程。 |
受信任(trusted) | "execution_policy": "trusted" | 安装时需要用户逐项确认。仍在同一个沙箱中运行,同样不能启动子进程,只放宽经批准的宿主代理能力。 |
本电脑模式(unrestricted) | "isolation": "dev_unrestricted" | 不进沙箱,以用户的系统账户权限作为普通进程运行,可以读写任意文件、联网和启动子进程。需要用户在插件详情或「设置 → 插件」中为这个 Worker 授予本电脑模式;安装或升级后授权会清空,需要重新授予。 |
- 声明为
sandboxed_plugin的 Worker 请求本电脑模式会被拒绝(PLUGIN_WORKER_ISOLATION_POLICY_MISMATCH)。 - Agent 以本电脑模式调用 Worker 时需要传
policy: "unrestricted",请在技能文件中写明,否则默认按沙箱调用。 - 只有确实依赖系统能力时才声明
dev_unrestricted,并在插件说明中写清原因和访问范围。能用宿主代理完成的事,不要要求本电脑模式。
私有数据目录
宿主通过环境变量 STABLEDGE_PLUGIN_DATA_DIR 给沙箱 Worker 提供一个插件独占的可读写目录,可以在其中使用 SQLite 等嵌入式数据库,这也是后台任务结果的主要存放位置。不要拼接或探测它的父目录和其他插件的目录;插件包目录本身只读。
网络
Worker 没有裸 socket,只能通过 host.net.fetch 联网。宿主对请求检查以下几项:
- 该 Worker 没有相应的
net.fetch权限时,返回PLUGIN_WORKER_PERMISSION_DENIED; - 协议必须是 http 或 https,否则返回
PLUGIN_HOST_NET_DENIED; - 默认只能访问公网,访问局域网和本机服务需要单独申请权限,安装时由用户单独确认,之后可以撤销;目标是局域网或本机地址、而 Worker 没有
net.local权限时,返回PLUGIN_HOST_NET_PRIVATE_TARGET,message为PLUGIN_HOST_NET_PRIVATE_TARGET:…。拦截范围和 DNS、重定向规则见存储、文件与网络 · 局域网与本机地址。
带 credential_refs 的请求必须使用 https,并且目标要在凭据的 allowed_origins 中,否则返回 PLUGIN_CREDENTIAL_ORIGIN_DENIED。宿主不跟随重定向;宿主注入的 Authorization、Cookie 不会返回给 Worker。
以上错误统一以 JSON-RPC 错误 -32010 返回,message 格式为 CODE:msg,请按冒号前的 CODE 分支。
请求参数支持 url、method、headers、body、body_base64、body_encoding、timeout_ms、max_bytes、response_type、file_name、credential_refs。credential_refs 引用清单中声明的凭据,由宿主注入。
后台调用
Worker 是被动的:只有宿主发来 worker.invoke 时才执行。Worker 自己用 setInterval 发出的宿主调用没有人接收,会一直挂起。定时任务、事件监听一律使用清单中的 triggers 和 event_channels,见 Manifest 参考。触发器调用时,input 中会带上宿主注入的 _trigger(id、kind、reason、scheduled_at、cursor);返回值中可以带 next_cursor 和 next_run_in_seconds / next_run_at。
依赖全局 Worker 服务
在清单中声明依赖,并让调用服务的 Worker 申请 service.invoke:
"required_services": [
{ "service_id": "com.stabledge.ocr", "version": "^1.0.0", "worker": "main", "optional": true }
],
"requested_permissions": [
{ "permission": "service.invoke", "worker": "main", "optional": false }
]
- 必需依赖缺失或版本不兼容时,插件处于依赖受阻状态;可选依赖缺失时,插件要走显式的本地降级路径。
- 主版本兼容的服务可以共享,主版本不兼容的服务可以并存;单例冲突会被明确诊断,不会静默选择。
- Worker 通过
host.service.invoke调用服务,单次时限 30 秒。
服务描述
宿主为每个已安装的服务维护一份描述,字段如下:
| 字段 | 约束 | 说明 |
|---|---|---|
service_id | ^[a-z][a-z0-9.-]{2,127}$ | 服务标识。 |
version | 语义化版本 | 服务版本。 |
api_major | ≥1 的整数 | API 主版本。 |
singleton | 布尔 | 是否只允许一个实例。 |
methods[].name | — | 方法名。 |
methods[].deadline_ms | 1–300000 | 方法时限。 |
methods[].cancellable | 布尔 | 是否可取消。 |
methods[].idempotent | 布尔 | 是否幂等。 |
methods[].max_payload_bytes | 1–2097152 | 单次载荷上限。 |
服务状态取值:stopped、starting、ready、degraded、quarantined。服务层错误码:SERVICE_INVALID_REQUEST、SERVICE_NOT_FOUND、SERVICE_VERSION_UNSATISFIED、SERVICE_SINGLETON_CONFLICT、SERVICE_PERMISSION_DENIED、SERVICE_UNAVAILABLE、SERVICE_DEADLINE_EXCEEDED。
全局 Worker 服务以独立的 .seworker 包发布。