iii Adapter Pattern 实战:用薄 Worker 将存量服务接入 iii 函数网格
2026/9/14 5:46:19 网站建设 项目流程

iii Adapter Pattern 实战:用薄 Worker 将存量服务接入 iii 函数网格

【免费下载链接】iiiEffortlessly compose, extend, and observe every service in real-time for the first time ever.项目地址: https://gitcode.com/GitHub_Trending/mo/iii

Adapter Pattern(适配器模式)是 iii 中一种将存量系统(第三方 API、内部微服务、遗留库)接入 iii 平台而无需重写的标准做法:用一个薄 Worker 包装既有服务,将其能力以service::name形式的 iii 函数暴露给整个网格。读完本文你将掌握适配器 Worker 的结构、注册与调用链路、错误映射与密钥处理的实现要点,并能够参考仓库源码写出可运行的适配器。

模式是什么:包装,而非重写

iii 的架构是一个 WebSocket 路由的 Worker 网格:单个引擎进程(默认端口49134)维护所有已连接 Worker、每个 Worker 暴露的函数以及绑定到函数的触发器的实时注册表。Worker 是独立的进程,通过 WebSocket 连接引擎并注册Functionsservice::name处理器)和Triggers(触发这些函数的事件);Worker 之间没有直连流量,所有调用都经由引擎路由——即caller → engine → handler,函数 ID 是任意两个 Worker 之间唯一的契约(见 engine 内建iii-engine-functionsWorker 的架构说明)。

Adapter Pattern 正是建立在这个模型之上:当你希望把一个既有服务引入 iii 系统而不重写它时,就把它包在一个薄 Worker 里,让该 Worker 把服务的接口翻译成 iii 的函数调用形态(function-call shape)。调用方因此可以像对待网格里任何其他 Worker 一样对待它——按函数 ID 寻址,而不是按 HTTP 端点或库特有的调用形态。

何时使用该模式

当你有一个希望从 iii 使用、但不想重写的既有服务,并且希望调用方以与其他 Worker 相同的方式(通过函数 ID,而不是 HTTP 或库特有的调用形态)来寻址它时,就使用此模式。

适用场景包括:

  • 第三方 API(如支付、AI、消息推送服务);
  • 内部自研微服务,已有 REST/gRPC/私有协议接口;
  • 仓库内已有的库或工具,需要暴露为网格可调用能力;
  • 需要在不改动调用方的前提下,为某个服务增加可观测性、触发绑定、队列化等 iii 能力。

结构:三步薄 Worker

一个 Adapter 就是一个薄 iii Worker,它:

  1. 连接引擎——通过 SDK 建立与引擎的 WebSocket 连接;
  2. 为每个要暴露的操作注册一个函数——每个函数对应存量服务的一个操作;
  3. 在每个处理器内部调用存量服务,并把结果作为函数的响应返回——翻译层就在这一步。

这与仓库中 functions.mdx 文档 描述的"编写函数"模型完全一致:Worker 通过注册函数向系统贡献能力,每个函数有一个service::name形式的 ID、一个接收 payload 并返回结果的 handler,以及可选的描述请求/响应形态的 JSON Schema。

连接引擎:registerWorker 与地址解析

适配器 Worker 的第一步是与引擎建立连接。以 Node/TypeScript SDK 为例:

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);

从 SDK 源码看,registerWorker(address?, options?)返回一个已连接的 Worker 客户端,WebSocket 连接会自动建立。地址解析遵循明确的优先级(resolveAddress):

  1. 显式传入的address参数(优先级最高);
  2. 环境变量III_URL——通常由孵化该进程的监督者(iii compose、容器运行时、systemd)设置,与III_NAMESPACEIII_WORKER_NAME一并注入;
  3. 兜底默认值DEFAULT_ENGINE_URL = 'ws://127.0.0.1:49134'(定义于 iii.ts)。

注意:默认地址特意写成 IPv4 回环地址,因为localhost在主机仅监听 IPv4 时可能解析到::1

注册函数:service::name 命名与 JSON Schema

连接建立后,为存量服务的每个操作注册一个函数。函数 ID 采用service::name形式——这既是调用方的寻址契约,也是触发器绑定时的function_id。在 Node SDK 的registerFunction中,函数 ID 不能为空且不能重复注册(重复会抛出function id already registered错误)。

建议的命名约定:service段使用被包装服务的域名化缩写(例如stripeopenaiorders),name段使用该服务的具体操作(例如create-paymentcompletionget-by-id)。这样整个网格的函数清单(可通过engine::functions::list查看)一目了然,也便于用prefix过滤出某个适配器的全部函数。

附带请求/响应 Schema

可以在注册时为请求和响应附带 JSON Schema,让契约随函数一起文档化——这些 Schema 会与函数一同存储,并在 iii console、iii trigger --help等界面呈现(见 functions.mdx 的 Schema 章节)。需要明确的是,当前引擎不做运行时校验:Schema 仅作为函数调用、Agent 与 console 的契约文档,引擎不会拒绝与 Schema 不匹配的 payload 或返回值。

worker.registerFunction( "orders::get", async (payload: { orderId: string }) => { // 调用存量订单服务…… return { orderId: payload.orderId, status: "shipped" }; }, { request_format: { type: "object", properties: { orderId: { type: "string" } }, required: ["orderId"], }, response_format: { type: "object", properties: { orderId: { type: "string" }, status: { type: "string" }, }, required: ["orderId", "status"], }, }, );

一个完整的适配器示例

以下是一个包装内部订单 REST 微服务的示意适配器(API 签名均取自仓库 SDK 的真实实现)。它演示了三步结构的完整落地:连接引擎、按操作注册函数、在 handler 内调用存量服务并映射结果。

import { registerWorker, InvocationError } from "iii-sdk"; const url = process.env.III_URL; if (!url) throw new Error("III_URL must be set"); const worker = registerWorker(url, { workerName: "orders-adapter" }); // 存量服务客户端:可以是内部 REST 客户端、SDK 或数据库连接 const ordersService = createOrdersClient({ baseUrl: process.env.ORDERS_SERVICE_URL, // 由部署环境注入 apiKey: process.env.ORDERS_SERVICE_API_KEY, // 密钥走环境变量,不进代码 }); worker.registerFunction( "orders::get", async (payload: { orderId: string }) => { const res = await ordersService.get(payload.orderId); // 调用存量服务 if (!res.ok) { throw new InvocationError({ code: "UPSTREAM_ERROR", message: `orders service returned ${res.status}`, function_id: "orders::get", }); } return res.json(); // 返回标准 iii 函数响应 }, { description: "Fetch an order from the legacy orders service", request_format: { type: "object", properties: { orderId: { type: "string" } }, required: ["orderId"] }, response_format: { type: "object" }, }, );

Python 与 Rust 的写法可参考 functions.mdx 中的多语言示例:

import os from iii import register_worker, InitOptions worker = register_worker( os.environ.get("III_URL"), InitOptions(worker_name="orders-adapter"), ) def get_order_handler(payload: dict) -> dict: return orders_service.get(payload["orderId"]) # 调用存量服务 worker.register_function("orders::get", get_order_handler)

错误映射:从上游错误到 iii 函数错误

Adapter 的翻译层不仅要翻译成功响应,还要翻译失败。Node SDK 提供了统一的类型化错误InvocationError:它包装线上的ErrorBody形态({ code, message, stacktrace? })以及被调用的function_id,使调用方在所有失败模式(RBAC 拒绝 FORBIDDEN、handler 级失败、等待引擎响应的超时)下都能通过同一个错误类型区分问题,且能通过err.code快速识别失败类别。其 message 以code: message形式呈现,错误对象自带codefunction_idstacktrace字段,自我描述性强。

实践建议:

  • 上游服务返回业务错误时,映射为带语义化codeInvocationError(如UPSTREAM_ERRORNOT_FOUNDRATE_LIMITED),让调用方可以按 code 编程式处理;
  • 不要把上游的原始堆栈直接透传,而是保留 message 并适当记录日志;
  • 若适配器同时暴露多个操作,为每个 handler 的InvocationError带上对应的function_id,便于追踪是哪个适配操作失败。

另外,SDK 中还有RegistrationRejectedError:当引擎拒绝 Worker 的身份注册(例如同一(namespace, worker_name)已被另一个存活 Worker 占用)时抛出,SDK 会停止且不重连。命名空间冲突(FUNCTION_NAMESPACE_CONFLICT)则是非致命的,仅记录日志。这提醒我们:部署适配器时要注意 Worker 名称与命名空间的唯一性。

认证与密钥处理

原文档的 TODO 特别点名了如何在外包服务时处理认证/密钥。结合仓库约定,推荐做法是:

  1. 密钥一律走环境变量或部署平台提供的注入机制,绝不硬编码进源码。SDK 本身依赖III_URLIII_NAMESPACEIII_WORKER_NAME等环境变量完成引导(见resolveAddress),适配器可沿用同样的约定,读取ORDERS_SERVICE_API_KEY之类的变量来构造存量服务客户端;
  2. 上游认证失败应映射为明确的错误 code(如AUTH_FAILEDUNAUTHORIZED),而非让调用方看到裸的 HTTP 401;
  3. 不要在接受外部 payload 的 handler 中拼接敏感凭据到响应里;如需审计,记录到日志/可观测性系统而不是返回给调用方。

关于可观测性:SDK 的 handler 包装层会在调用前后记录iii.invocation.input/iii.invocation.output追踪事件(受III_DISABLE_TRACE_PAYLOADS环境变量控制,见 iii.ts 中的 handler 包装)。也就是说,适配器天然获得请求/响应 payload 的追踪能力,便于观察"调用方 → 引擎 → 适配器 → 存量服务"整条链路。

暴露适配器:用 Trigger 把函数接到事件上

适配器注册的函数默认只能被显式调用(worker.trigger/iii trigger/ SDK 调用)。若要让它响应事件,就绑定 Trigger。SDK 的registerTrigger将一个 trigger 配置绑定到已注册函数上,返回带unregister()的句柄:

// 暴露为 HTTP 端点 worker.registerTrigger({ type: "http", function_id: "orders::get", config: { api_path: "/orders/get", http_method: "GET" }, }); // 定时轮询存量服务并同步状态 worker.registerTrigger({ type: "cron", function_id: "orders::sync", config: { expression: "0 * * * * *" }, // 7 字段:sec min hr dom mon dow year });

仓库中的引擎内建 Worker README 归纳了常用 trigger 类型及其配置键(见 engine_fn README 的 Canonical trigger types 表):

typeProvider配置键
httphttp{ http_method, api_path }
croncron{ expression }— 7 字段:sec min hr dom mon dow year
queuequeue{ queue, retries }
statestate{ scope, condition_function_id }
streamiii-stream{ stream_name }

一个值得注意的命名空间语义:registerTrigger若未显式指定命名空间,则落在该 Worker 自己的命名空间,因为函数注册在 Worker 的命名空间下——命名空间不一致会导致 trigger 触发后解析不到函数(见 registerTrigger 的源码注释)。适配器与调用方在同一命名空间时通常无需显式指定。

通过引擎内建函数观察适配器

部署适配器后,可以用引擎自带的engine::*内省函数验证它是否正确入网(无需额外安装,内建于引擎,见 engine_fn README):

  • engine::functions::list { prefix: "orders::" }— 确认适配器注册的函数已出现在注册表;
  • engine::functions::info { function_id: "orders::get" }— 查看该函数的 Schema、属主 Worker 与已绑定的触发器;
  • engine::workers::list/engine::workers::info { name: "orders-adapter" }— 查看连接中的 Worker 及其完整暴露面。

小结与工程建议

Adapter Pattern 在 iii 中是一个"薄壳"模式:翻译层只做三件事——连接引擎、按操作注册函数、在 handler 里调用存量服务并映射结果。它把 HTTP 或库特有的调用形态统一收敛为service::name函数契约,让调用方与存量服务的耦合降到最低。工程落地时重点把握四条原则:

  1. 每个存量操作 = 一个函数,命名遵循service::name,并附带请求/响应 Schema 作为契约文档;
  2. 错误统一走类型化错误(如InvocationError),用语义化code区分失败类别;
  3. 密钥走环境变量,由部署环境注入,不在源码与响应中暴露;
  4. 用 Trigger 决定暴露方式(HTTP、定时、队列等),命名空间要与函数注册保持一致。

参考实现与文档入口:适配器骨架与多语言写法见 functions.mdx,引擎路由模型与内省函数见 engine_fn README,SDK 连接与注册 API 见 iii.ts、errors.ts。

【免费下载链接】iiiEffortlessly compose, extend, and observe every service in real-time for the first time ever.项目地址: https://gitcode.com/GitHub_Trending/mo/iii

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询