iii 服务扩展指南:以 Worker 为单元构建系统能力,用 Trigger 与 Function 组合任意服务
【免费下载链接】iiiEffortlessly compose, extend, and observe every service in real-time for the first time ever.项目地址: https://gitcode.com/GitHub_Trending/mo/iii
iii 的核心设计原则是:系统中的每一个新能力,都是通过创建一个 Worker 来加入的——队列、调度器、HTTP 边缘、浏览器标签页、Agent、CRM 集成、沙箱,无一例外。本指南基于docs/0-19-0/creating-workers/index.mdx.skill.md及其同节文档,讲解 Worker / Trigger / Function 三大核心概念、Worker 的脚手架、连接、生命周期与清单(manifest)配置,以及自定义 Trigger 类型的完整流程。读完本文,你将掌握"如何为 iii 系统新增一种能力"的标准路径:写一个 Worker,注册 Trigger 与 Function,让 Engine 完成路由,而 Engine 本身永远不需要修改。
Worker 就是服务:iii 的能力单元模型
在 iii 中,Worker 是能力的单元,也是服务的本体。当你需要新的行为时,正确做法是编写一个新的 Worker(或从 registry 安装一个已有的 Worker),而不是去给 Engine 打补丁、加插件或 fork 它。
Engine 是一个固定的协调者:
- 它负责在 Worker 之间路由调用(invocation);
- 它维护着一张"实时注册表"(live registry),记录每个 Worker 当前提供了什么。
所有让 iii 系统"变得有用"的逻辑都运行在 Worker 里。这也是为什么"如何给 iii 加 X?"这类问题几乎总有一个标准答案:添加一个 Worker。如果某项能力缺失,缺口由 Worker 填补,而不是由修改 iii 本身来填补。
关于 Worker、Trigger、Function 与 Engine 的完整心智模型,参见 docs/0-19-0/understanding-iii/index.mdx;而"如何动手构建 Worker"则继续阅读本节的 Workers、Triggers 与 Functions 三篇文档。
四个基本构件
理解 iii 只需要四个概念,其余一切皆是变体:
| 构件 | 角色 | 关键事实 |
|---|---|---|
| Worker | 承载工作的进程 | 能连上 Engine 并注册 Trigger 与 Function 的"任何东西",可运行在笔记本、容器、浏览器标签页或 microVM 上,任何能打开 WebSocket 的语言都可以 |
| Trigger | 触发工作的原因 | 有一个 type(http、cron、队列消息、状态变更、另一个 Function 调用trigger)、一份 config(路径、调度、队列)和一个它要调用的function_id |
| Function | 工作本身 | Worker 内部的具名处理器,接收 payload、返回结果;标识符遵循service::name约定,跨语言、跨重启保持稳定 |
| Engine | 协调者 | 接受 Worker 连接、维护可用 Function 与 Trigger 的实时注册表,把调用路由到当前提供目标 Function 的那个 Worker |
Worker 隔离与"任何语言、任何运行时"
Worker 被设计为相互独立的进程:一个 Worker 崩溃不会影响其他 Worker。Engine 与每个 Worker 通过独立的 WebSocket 连接,只把调用路由到当前已连接的 Worker;某个 Worker 崩溃、重启或网络分区时,它的 Function 与 Trigger 会在断开连接时从路由表中移除,其余 Worker 继续提供服务。
Worker 的契约足够小——只要能使用 WebSocket 和 JSON,就能实现。Python Worker 与 TypeScript Worker 可以是不同语言、不同运行时、甚至不同机器上的独立进程,互不感知对方上下文,一切由 Engine 协调。这就是"任何语言、任何运行时"在实践中的含义。
一、构建 Worker:脚手架、连接与生命周期
1. 脚手架一个新 Worker
iii worker init可以从零创建一个独立 Worker,命令会写入一个语言专属的项目目录(包含已安装的 iii SDK、一份iii.worker.yaml清单,以及可替换的示例 Function 与 Trigger 注册代码):
# 交互式:提示选择语言 iii worker init my-worker # 完全脚本化:传 --language 跳过提示 iii worker init my-worker --language typescript支持的--language取值:typescript(ts)、javascript(js)、python(py)、rust(rs)。位置参数NAME是目标目录名,可用--directory覆盖。注意:
- 对已包含 iii Worker(存在
.iii/worker.ini)的目录重复执行iii worker init不会做任何改动; - 默认对非空目录会失败,需加
--allow-non-empty才能在任意非空目录中脚手架。
如果不想从零编写,而是安装 registry 中已有的 Worker,则使用iii worker add(详见 docs/0-19-0/using-iii/workers.mdx)。
2. 连接 Engine:唯一的耦合点是连接字符串
Worker 通过 WebSocket 连接 Engine。约定是使用环境变量III_URL设置 Engine 地址,也可以显式传给register_worker。连接字符串是 Worker 与所加入 iii 实例之间的唯一耦合,因此 Worker 进程可以部署在网络可达的任何位置。
从源码看,Node SDK(sdk/packages/node/iii/src/iii.ts)的地址解析优先级为:显式地址 >III_URL环境变量 > 默认值ws://127.0.0.1:49134(DEFAULT_ENGINE_URL刻意使用 IPv4 回环地址,避免localhost解析到::1而 Engine 只监听 IPv4 时连不上)。此外,SDK 会优先采用III_WORKER_NAME环境变量(由 supervisor 为 Compose 托管 Worker 注入)作为 Worker 名,其次才用代码内显式名称或hostname:pid兜底。
// Node / TypeScript import { registerWorker } from "iii-sdk"; const url = process.env.III_URL; if (!url) throw new Error("III_URL must be set"); const worker = registerWorker(url, { workerName: "my-worker", workerDescription: "One-line summary of what this worker does", });# Python import os from iii import register_worker, InitOptions worker = register_worker( os.environ.get("III_URL"), InitOptions( worker_name="my-worker", worker_description="One-line summary of what this worker does", ), )// Rust use iii_sdk::{InitOptions, WorkerMetadata, register_worker}; let url = std::env::var("III_URL").expect("III_URL must be set"); let worker = register_worker( &url, InitOptions { metadata: Some(WorkerMetadata { name: "my-worker".into(), description: Some("One-line summary of what this worker does".into()), ..Default::default() }), ..Default::default() }, );3. Worker 生命周期状态机
Worker 连接后经过一组有限状态:connecting → connected → available / busy → disconnected。
connecting:WebSocket 握手阶段;connected:Worker 已加入 Engine 的注册表;available/busy:描述 Worker 当前是否正在处理调用;disconnected:WebSocket 关闭后的终态。
Engine 会跟踪这些状态转移,并通过 discovery 函数向其他 Worker 与工具暴露,让系统其余部分可以做出反应。
4. 检查实时注册表
想查看当前连接了哪些 Worker,可以调用engine::*::list系列 Function 获取注册表快照:
| Function | 返回内容 |
|---|---|
engine::workers::list | 每个已连接 Worker 及其指标 |
engine::functions::list | 每个已注册 Function,可用include_internal过滤 |
engine::triggers::list | 每个已注册 Trigger,可用include_internal过滤 |
engine::trigger-types::list | 每个已公布的 Trigger type 及其 config / call schema |
这些 Function 由 Engine 内置的engine_fnWorker 提供(见 engine/src/workers/engine_fn/mod.rs 与 engine/src/workers/registry.rs)。调用示例:
// engine::functions::list const { functions } = await worker.trigger({ function_id: "engine::functions::list", payload: { include_internal: false }, });5. 处理断开与优雅关闭
当 Worker 的 WebSocket 关闭时,Engine 自动清理:它的 Function 与 Trigger 离开实时注册表,正在处理中的调用被取消。in-flight 请求会收到invocation_stopped错误——请像处理"取消"一样捕获它;在拥有该 Function 的 Worker 重连之前重试都会失败。推荐的恢复方式是订阅 Engine 的 discovery 事件:
| Trigger | 触发时机 |
|---|---|
engine::workers-available | 有 Worker 连接或断开 |
engine::functions-available | 有 Function 被注册或注销 |
worker.registerFunction( "discovery::on-functions", async (data: { event: string; functions: { function_id: string }[] }) => { // `functions` 是变更后的完整快照 const ids = data.functions.map((f) => f.function_id); }, ); worker.registerTrigger({ type: "engine::functions-available", function_id: "discovery::on-functions", config: {}, });优雅关闭则调用 SDK 的shutdown:Engine 移除该 Worker 的 Function 与 Trigger、发出engine::workers-available的worker_disconnected事件,并用invocation_stopped取消针对它们的 in-flight 调用。若进程被强制退出,Engine 在发现 socket 断开后也会达到同样的状态,但优雅关闭让这一过程确定且更快。这在一次性 / 临时 Worker(Kubernetes Job、serverless 容器、定时脚本)中尤其有用:连接、注册、干完活、shutdown()退出。
二、Worker 清单:iii.worker.yaml 完整字段
iii.worker.yaml位于 Worker 根目录,告诉 iii 如何配置运行时、安装依赖、启动进程并透传配置。Engine 在从config.yaml启动托管 Worker 以及iii worker start/stop/restart时读取它(详见 docs/0-19-0/creating-workers/worker-manifest.mdx)。
注意:直接运行 Worker 进程(例如
node ./myworker/src/index.js)不需要iii.worker.yaml。用 SDK 连接 Engine 的 Worker,无论是否由 iii 启动,行为完全一致——清单只是"如何启动"的元数据。
name: math-worker description: Evaluate math expressions over iii functions. runtime: # 作为 Worker rootfs 的基础 OCI 镜像,可覆盖以锁定版本 base_image: docker.io/iiidev/python:latest scripts: install: "pip install -e ." start: "watchfiles 'python src/math_worker.py'"字段速查:
name(必填):必须满足 registry 的 Worker 命名规则;Worker 不能把自己列为依赖。runtime.base_image(可选):覆盖默认 OCI rootfs。必须是合法 OCI 引用(字母数字加. _ - / : @ +,最长 512 字符);非法引用会被丢弃并告警,回退到默认镜像。例如oven/bun:1或ghcr.io/astral-sh/uv:bookworm-slim。scripts:生命周期脚本。setup在沙箱供给时运行一次(装系统级依赖),install安装项目依赖,start启动 Worker 进程。省略scripts时,Engine 会依据runtime.kind与runtime.package_manager推断install与start。常用示例:
scripts: setup: "apt-get update && apt-get install -y build-essential" install: "npm install" start: "npx tsx watch src/index.ts" # Node 开发热重载env:注入 Worker 进程的环境变量映射(键值均为字符串)。III_URL与III_ENGINE_URL会被静默过滤——连接 URL 由 Engine 自行设置。dependencies:<worker-name>: <semver 范围>映射,按 registry 解析。每条范围须为合法 semver 需求(如^1.2、~0.5.0、>=2 <3);重复键是错误;不能依赖自身;prerelease 范围语法上可接受,但默认 resolver 只提供稳定版本,所以最终会以version_not_found失败。
dependencies: iii-http: "^0.19" iii-state: "^0.19"resources(可选):沙箱的 CPU 与内存请求,超出上限会被钳制(cap 内)。cpus默认2、上限4;memory默认2048MiB、上限4096MiB。对 bundle 类 Worker,超限时安装会发出W182 BundleResourceClamped告警。
resources: cpus: 2 memory: 2048三、编写 Function:Worker 贡献的能力
Worker 通过注册 Function 向系统贡献能力。每个 Function 有一个service::name形式的id、一个接收 payload 并返回结果的 handler,以及可选的请求/响应 JSON Schema(详见 docs/0-19-0/creating-workers/functions.mdx)。
注册 Function
// Node / TypeScript worker.registerFunction("math::add", async (payload: { a: number; b: number }) => { return { c: payload.a + payload.b }; });# Python def add_handler(payload: dict) -> dict: return {"c": payload["a"] + payload["b"]} worker.register_function("math::add", add_handler)// Rust worker.register_function(RegisterFunction::new("math::add", |input: AddInput| { Ok(serde_json::json!({ "c": input.a + input.b })) }));附加请求 / 响应 Schema
Schema 与 Function 一并存储,会出现在 iii console、iii trigger --help等地方。注意:运行时校验目前尚不支持——附加的 Schema 仅作契约文档(供调用方、Agent 与 console 阅读),Engine 不会拒绝不符合 Schema 的 payload 或返回值。Node 传入原生 JSON Schema 对象;Python 可直接传 dict;Rust 则通过schemars::JsonSchema从入参/出参结构体自动生成。
HTTP 可调用 Function:把外部端点注册为普通 Function
除了进程内 handler,你还可以注册一个外部 HTTP 端点作为 Function:Engine 在该 Function 被调用时发起 HTTP 请求,Worker 只需声明端点。这适合把现有 API Gateway、webhook、serverless 平台(Lambda、Azure Functions、Google Cloud Functions)或任意第三方 API 暴露为常规 iii Function——注册后它和任何其他 Function 一样,可被worker.trigger、iii trigger以及任意 Trigger type(queue、cron、state、http)调用。
HttpInvocationConfig字段:
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
url | string | (必填) | Engine 在调用时请求的端点 |
method | "GET" \| "POST" \| "PUT" \| "PATCH" \| "DELETE" | "POST" | HTTP 方法 |
timeout_ms | number | 30000 | 单请求超时(毫秒) |
headers | Record<string, string> | {} | 每次调用附加的请求头 |
auth | HttpAuthConfig | (无) | bearer、hmac或api_key之一,配合token_key/secret_key/value_key |
安全要点:token_key、secret_key、value_key指定的是环境变量的名字而非密钥本身。Engine 在注册时从其自身进程环境解析,密钥只留在 Engine 主机上,绝不会通过 SDK 的 WebSocket 传输。
错误处理上,Engine 把调用 payload 作为 JSON 请求体发送,任何非 2xx 响应或网络错误都被视为调用失败并回传给调用方。HTTP 调用的 Function 会出现在engine::functions::list中,与进程内 handler 一样可通过 console 发现。
返回值、错误与注销
Function 要么返回值(由 handler 负责匹配文档化的 response schema),要么返回错误。handler 内抛出的错误会作为 invocation error 传给调用方,并附带 Worker 的堆栈:Node 转发error.stack,Python 转发traceback.format_exc(),Rust 转发底层错误的堆栈。Engine 不会吞掉它们。可以用这个区别表达"预期失败"(返回结构化错误值)与"非预期失败"(throw / raise / 返回Err)。
registerFunction返回一个带unregister()方法的句柄,可在运行时把 Function 从 Engine 移除;Worker 断开时其全部 Function 会被自动移除,挂起的调用报错退出。
四、编写 Trigger:绑定事件源,或发布自己的 Trigger 类型
Trigger 告诉 iii "何时调用哪个 Function"。一个 Trigger 由三部分组成:type(事件种类,如http、cron)、config(该类型的具体细节,如 HTTP path 或 cron 表达式)、function_id(要调用的 Function)。还可以指定可选的condition_function_id:Trigger 触发时,Engine 先用相同 payload 调用条件函数,返回真值才执行 handler,否则跳过调用——"何时做"(Trigger)与"做什么"(Function)由此保持分离(详见 docs/0-19-0/creating-workers/triggers.mdx)。
绑定既有 Trigger 类型
大多数时候,Worker 是 Trigger 的消费者:把函数绑定到其他 Worker 已发布的类型上,如http(来自 iii-http,把函数暴露为端点)、cron(来自 iii-cron,按计划执行)、队列消息(iii-queue)、state(iii-state,响应数据变更)。绑定使用worker.registerTrigger({ type, function_id, config }):
// Node / TypeScript worker.registerTrigger({ type: "http", function_id: "math::add", config: { api_path: "/math/add", http_method: "POST" }, });注册时发布该 Trigger type 的 Worker 必须在线,否则注册失败。config的形状由各 Trigger type 自行定义,记录在发布该类型的 Worker 文档中。
给 Trigger 附加元数据
每次绑定都可带一个可选的metadataJSON 对象(由消费者在注册时设置)。Engine 原样存储,并在两处呈现:
- 发布方 Worker 的
TriggerHandler.registerTrigger(config)回调通过config.metadata看到它——发布方可据此做优先级提示、审计标签、路由键等记账; engine::triggers::list会在每条TriggerInfo上返回它,console 与做 discovery 的 Worker 都能读取。
注意区分:metadata由消费者在每个绑定上设置(自由标签袋);schemas由发布方声明 Trigger type 时设置(描述消费者交互的 JSON 形状,如http类型的绑定 config 是{ api_path, http_method },调用 payload 是{ method, headers, query_params, body })。
发布自己的 Trigger 类型:mini-http 完整示例
当你的 Worker 想成为事件发布方(HTTP 请求、webhook、文件变更、数据库更新)时,需要声明自己的 Trigger type。一个 Trigger type 由两部分组成:
- 一个字符串
id(消费者绑定时的type: "mini-http"); - 一份由你的 Worker 在进程内维护的 per-binding 路由表。Engine 的注册表只是规范地记录绑定(
engine::triggers::list返回的正是它),但 Engine 不负责分发——它把网络上任何消费者 Worker 的 bind/unbind 以回调形式转发给发布方 Worker,由发布方决定如何处理。
启动时用worker.registerTriggerType({ id, description }, handler)声明。你需要实现的TriggerHandler接口暴露两个回调:registerTrigger(config)(有消费者绑定时运行,config携带绑定实例的id、消费者的function_id和符合该类型形状的config,存进路由表)与unregisterTrigger(config)(解绑时从路由表删除)。
下面是一个迷你版 iii-http 的完整示例:声明mini-http类型,维护bindings表,并在收到 HTTP 请求时查找匹配绑定:
// Node / TypeScript import { registerWorker } from "iii-sdk"; import type { TriggerConfig, TriggerHandler } from "iii-sdk"; const url = process.env.III_URL; if (!url) throw new Error("III_URL must be set"); const worker = registerWorker(url); type MiniHttpConfig = { api_path: string; // 前导斜杠,如 "/orders" http_method?: "GET" | "POST" | "PUT" | "DELETE"; }; const bindings = new Map<string, TriggerConfig<MiniHttpConfig>>(); const httpHandler: TriggerHandler<MiniHttpConfig> = { async registerTrigger(config) { bindings.set(config.id, config); }, async unregisterTrigger(config) { bindings.delete(config.id); }, }; worker.registerTriggerType( { id: "mini-http", description: "Routes HTTP requests to bound functions" }, httpHandler, );Python 版使用worker.register_trigger_type(RegisterTriggerTypeInput(id="mini-http", description=...), HttpHandler());Rust 版使用RegisterTriggerType::new("mini-http", "Routes HTTP requests to bound functions", HttpHandler::default())。
分发事件:没有特殊的 "fire" API
当底层事件源送来内容(一个入站 HTTP 请求、一次 cron 滴答、一次 webhook 命中)时,发布方 Worker 从bindings表里查匹配项,然后用普通的worker.trigger(...)调用每个匹配函数——没有任何特殊 API:
// 在 Worker 的 HTTP listener 内部,method+path 匹配到 bindings 表项之后: const binding = bindings.get(matchedTriggerId); await worker.trigger({ function_id: binding.function_id, payload: { method, headers, body }, });每次分发时,Engine 会评估消费者的config与可选的condition_function_id,再把匹配的调用路由到绑定函数,并把结果返回给调用方。
附加 Schema 与注销 Trigger Type
Trigger type 可携带两个可选 JSON Schema:trigger_request_format(消费者绑定时传入的 per-bindingconfig的 schema)与call_request_format(触发时你交付给绑定函数的 payload 的 schema)。它们会喂给 iii console、Agent 可读的 skills 和engine::trigger-types::list输出,让消费者知道该发什么、会收到什么。运行时校验同样尚未支持,Schema 只是契约文档。
需要在不重启 Worker 的情况下下线某个类型(底层资源进入维护模式、feature flag 关闭、滚动更换 schema)时,可调用worker.unregisterTriggerType(...)(Node / Python 传完整输入对象但只用其id字段,Rust 只传id字符串;Node 的registerTriggerType还会返回带.unregister()快捷方法的TriggerTypeRef)。Worker 断开时其公布的所有 Trigger type 会自动移除,因此这一步只在 Worker 保持连接时需要。
五、源码印证:SDK 与 Engine 的落地实现
以上 API 并非纸面设计,仓库中均有对应实现:
- SDK 连接与地址解析:Node SDK 的
registerWorker与DEFAULT_ENGINE_URL实现在 sdk/packages/node/iii/src/iii.ts(第 87–117 行附近的resolveWorkerName/resolveAddress,地址优先级为显式参数 >III_URL>ws://127.0.0.1:49134)。同一文件还定义了 Worker 注册时的命名空间冲突处理:WORKER_NAMESPACE_CONFLICT(同名 Worker 已在线上,连接被关闭,致命)与FUNCTION_NAMESPACE_CONFLICT(同命名空间内函数 id 冲突,连接保持,非致命)。 - Engine 注册表:
engine::workers::list、engine::functions::list、engine::triggers::list、engine::trigger-types::list等 discovery Function 由 engine/src/workers/engine_fn/mod.rs 提供,注册表核心逻辑在 engine/src/workers/registry.rs。 - Worker 清单解析:
iii.worker.yaml的解析与校验在 iii-compose crate 的 crates/iii-compose/src/manifest.rs 中体现,Engine 侧对scripts.start、base_image等字段的处理可在 engine/src 的 Worker 管理相关模块中找到。
结语:从概览到实战的完整路径
回顾本节的完整脉络:Worker 是能力单元(workers.mdx)解决"如何写一个能连上 Engine 并部署的服务";Trigger(triggers.mdx)解决"什么事件触发这些 Function 运行";Function(functions.mdx)解决"Worker 贡献哪些可调用能力";配套的 worker-manifest.mdx 与 workers-registry.mdx 分别覆盖清单字段与发布/安装到 registry 的机制。
实践要点回顾:
- 新能力 = 新 Worker:脚手架用
iii worker init,安装现成的用iii worker add; - 连接只需一个 WebSocket 地址(
III_URL),Worker 可在任何语言、任何位置运行; registerFunction让函数可被worker.trigger/iii trigger直接调用(每个注册函数天然获得直接调用能力);- 一个 Function 可绑定多个 Trigger:同一个函数可同时被 HTTP 请求、cron 调度与队列消息触发,函数代码不变;
- 需要新事件源时,用
registerTriggerType发布自己的 Trigger type,Engine 只转发绑定回调,路由由你的 Worker 在进程内维护; - 注册表是可观测的:
engine::*::list系列函数与engine::*‑available事件让系统拓扑对每个 Worker 透明。
基于此,任何"如何给 iii 加 X?"的问题,答案都指向同一条路径:写一个 Worker。
【免费下载链接】iiiEffortlessly compose, extend, and observe every service in real-time for the first time ever.项目地址: https://gitcode.com/GitHub_Trending/mo/iii
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考