Worker

适用版本:StablEdge 桌面端 0.4.4,Worker 协议 2.0

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"]。

上例中的导入路径只是示意:Worker 用到的每个文件都必须放在 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 传递。

生命周期

  1. 宿主创建隔离进程,发送 worker.initialize。
  2. Worker 回显身份、协议版本和业务方法清单。
  3. 宿主校验 protocol_version、plugin_id、worker_id、methods、features 五个字段后进入就绪状态。
  4. 调用按各方法的时限执行。
  5. 界面调用 worker.cancel 时,宿主中止该调用并终止整个 Worker 进程;下一次调用会重新启动进程。
  6. 宿主目前不发送 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.statWorker → 宿主fs.source.read5 秒否
host.fs.readWorker → 宿主fs.source.read30 秒是
host.net.fetchWorker → 宿主net.fetch120 秒是
host.blob.readWorker → 宿主storage.blob10 秒否
host.blob.deleteWorker → 宿主storage.blob5 秒否
host.schedule.upsertWorker → 宿主—5 秒否
host.schedule.removeWorker → 宿主—5 秒否
host.schedule.pauseWorker → 宿主—5 秒否
host.service.invokeWorker → 宿主service.invoke30 秒是

通知:$/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_ms1–300000方法时限。
methods[].cancellable布尔是否可取消。
methods[].idempotent布尔是否幂等。
methods[].max_payload_bytes1–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 包发布。

本页目录