ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

iii 适配器模式实战:把任意第三方服务包装成 iii Worker 并暴露为函数

iii 适配器模式实战:把任意第三方服务包装成 iii Worker 并暴露为函数 iii 适配器模式实战把任意第三方服务包装成 iii Worker 并暴露为函数【免费下载链接】iiiEffortlessly compose, extend, and observe every service in real-time for the first time ever.项目地址: https://gitcode.com/GitHub_Trending/mo/iii本篇技术指南聚焦 iii 项目中的Adapter Pattern适配器模式当你想把一个已经存在的服务第三方 API、某个语言库、内部微服务接入 iii 系统而不重写它时如何用一层薄 worker把该服务的能力翻译成 iii 函数调用形态。读完本文你将掌握适配器的适用场景与三步式结构、函数 ID 命名约定、错误映射策略、认证与密钥的安全处理方式并能把适配器注册、部署、调用到真实的 iii 项目里。本文以 docs/0-18-0/patterns/adapter-pattern.mdx 为骨架结合 SDK 与引擎源码engine/src/invocation/auth.rs、engine/src/invocation/http_invoker.rs、sdk/packages/node/helpers/src/http/index.ts做纵深展开。什么是适配器模式在 iii 的世界里系统的能力以Worker为单位组织每个 Worker 向引擎注册若干函数Functions系统内任何调用方都可以通过function_id直接寻址并调用它们而无需关心目标函数运行在哪台机器、用哪种语言实现详见 Creating Workers / Workers。但现实世界很少从零开始你可能已经有一个线上运行的第三方 APIStripe、OpenAI、地图服务、一个内部微服务、或者一个积累了很久的工具库。适配器模式就是为这种存量资产设计的接入方式将一个已有的服务第三方 API、库、内部微服务包装进一个 iii 系统而不重写它——用一个薄 Worker 把服务的能力暴露为 iii 函数。Worker 负责在已有服务的接口形态与iii 函数调用形态之间做翻译。这里的关键词是thin薄适配器不做业务逻辑只做三件事——连上引擎、注册函数、把函数调用转发给背后真实的服务。何时使用这个模式适配器模式不是万能的它的适用条件非常明确你已有一个可以用的服务希望 iii 内的其他 Worker、CLI、Trigger 能直接使用它你不想或不能重写这个服务——可能是第三方托管的 API你拿不到源码也可能是内部系统重写成本过高你希望调用方用统一的方式寻址通过函数 IDservice::name形态调用而不是各自拿着 HTTP URL、库专属调用接口各写各的。当以上条件都满足时适配器就是最合适的接入形态。它让存量服务看起来和其他 iii Worker 没有任何区别因此可以立刻享受 iii 的统一调用面worker.trigger、iii trigger、以及 http / cron / queue / state 等各类 Trigger 绑定见 Using iii / Triggers。适配器的结构原文档给出了适配器的三步式标准结构一个薄 iii Worker需要做到连接引擎通过 SDK 的registerWorker()建立与引擎的 WebSocket 连接每个要暴露的操作注册一个函数把服务的每个能力映射为一个function_id在每个 handler 内部调用已有服务并把结果作为函数响应返回handler 是翻译层负责参数转换、调用真实服务、结果回传。第一步连接引擎Worker 与引擎之间唯一的耦合就是连接串。惯例是通过III_URL环境变量传入引擎地址也可以显式传给register_worker详见 Creating Workers / Workers — Connecting to the engine// 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-adapter, workerDescription: Thin adapter over the ACME pricing API, });# Python import os from iii import register_worker, InitOptions worker register_worker( os.environ.get(III_URL), InitOptions( worker_namemy-adapter, worker_descriptionThin adapter over the ACME pricing API, ), )// Rust use iii_sdk::runtime::WorkerMetadata; use iii_sdk::{InitOptions, 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-adapter.into(), description: Some(Thin adapter over the ACME pricing API.into()), ..Default::default() }), ..Default::default() }, );说明iii worker init可以脚手架出一个带iii.worker.yaml清单和示例函数注册的新 Worker但它不是必需的。任何使用 iii SDK、调用registerWorker()并连接到 iii 实例的代码都是 Worker——你可以手写适配器进程跑在裸机、容器或你自己的虚拟化环境里Creating Workers / Workers。第二步为每个操作注册一个函数注册函数使用worker.registerFunction(id, handler)。id遵循service::name形态handler 接收调用的 payload 并返回结果详见 Creating Workers / Functions。// Node / TypeScript worker.registerFunction(acme::get-price, async (payload: { sku: string }) { // 第三步调用已有服务 return { price: await acmeClient.getPrice(payload.sku) }; });# Python def get_price(payload: dict) - dict: return {price: acme_client.get_price(payload[sku])} worker.register_function(acme::get-price, get_price)// Rust use iii_sdk::{RegisterFunction, register_worker}; use schemars::JsonSchema; use serde::Deserialize; #[derive(Deserialize, JsonSchema)] struct GetPriceInput { sku: String } worker.register_function(RegisterFunction::new( acme::get-price, |input: GetPriceInput| - Resultserde_json::Value, String { Ok(serde_json::json!({ price: acme_client.get_price(input.sku)? })) }, ));第三步handler 内调用服务并返回结果这是适配器的翻译层所在接收 iii 的函数调用 payload → 转换成目标服务需要的调用参数 → 调用服务 → 把服务返回的结果整理成函数的响应。翻译层越薄越好复杂的业务逻辑应留在被包装的服务里。函数 ID 约定service::name原文档的 TODO 明确提到要补充函数 ID 约定。在 iii 中函数 ID 的统一形态是service::name见 Using iii / Functionsservice段标识提供者或领域通常是 Worker 名称或被包装服务的名称例如acme、stripe、slackname段标识具体操作例如get-price、create-checkout、send-message。采用这种约定的好处调用方不需要知道底层是 HTTP 端点、SDK 调用还是进程内代码只需按function_id寻址。这也是整个系统统一的寻址语言——engine::functions::list、iii trigger、Trigger 绑定的function_id字段都使用同一套 ID。两种适配实现引擎转发 vs 进程内调用原文档描述的是在 handler 里调用已有服务的通用形态。结合 Creating Workers / Functions适配器有两种落地方案按场景选择方案 AHTTP 转发函数零手写调用代码如果被包装的服务本身就是一个 HTTP API最典型的情况可以直接把外部 HTTP 端点注册为函数由引擎在函数被调用时代为发起 HTTP 请求。你的 Worker 只声明端点不写任何 HTTP 客户端代码见 Creating Workers / Functions — HTTP-invokable functions// Node / TypeScript worker.registerFunction( acme::get-price, { url: https://api.acme.example.com/v1/pricing, method: POST, timeout_ms: 5000, headers: { X-Service: iii-adapter }, auth: { type: bearer, token_key: ACME_API_TOKEN }, }, { description: Query the ACME pricing API }, );# Python from iii import HttpInvocationConfig, register_worker from iii.iii_types import HttpAuthBearer worker.register_function( acme::get-price, HttpInvocationConfig( urlhttps://api.acme.example.com/v1/pricing, methodPOST, timeout_ms5000, headers{X-Service: iii-adapter}, authHttpAuthBearer(token_keyACME_API_TOKEN), ), descriptionQuery the ACME pricing API, )// Rust use std::collections::HashMap; use iii_sdk::{HttpAuthConfig, HttpInvocationConfig, HttpMethod, RegisterFunctionMessage, register_worker}; let mut headers HashMap::new(); headers.insert(X-Service.into(), iii-adapter.into()); worker.register_function(( RegisterFunctionMessage::with_id(acme::get-price.into()) .with_description(Query the ACME pricing API.into()), HttpInvocationConfig { url: https://api.acme.example.com/v1/pricing.into(), method: HttpMethod::Post, timeout_ms: Some(5000), headers, auth: Some(HttpAuthConfig::Bearer { token_key: ACME_API_TOKEN.into() }), }, ));HttpInvocationConfig的字段在 sdk/packages/node/helpers/src/http/index.ts 中有精确的类型定义完整字段如下字段类型默认值说明urlstring必填函数被调用时引擎请求的端点methodGET \| POST \| PUT \| PATCH \| DELETEPOSTHTTP 方法timeout_msnumber30000单次请求超时毫秒headersRecordstring, string{}每次调用附加的请求头authHttpAuthConfig无认证配置bearer/hmac/api_key配合环境变量名指定密钥引擎侧的默认超时在 engine/src/invocation/http_invoker.rs 中得到印证HttpInvokerConfig::default()中default_timeout_ms: 30000。同一文件还揭示了 HTTP 转发的错误语义响应状态码 400 视为成功is_non_error_status按数值比较1xx/2xx/3xx 均不视为错误4xx/5xx 与网络错误则作为调用失败回传给调用方。被调用的函数仍然会出现在engine::functions::list中与进程内 handler 一样可被 console 与 agent 发现Creating Workers / Functions。方案 B进程内 handler 包装SDK 客户端形态如果被包装的服务不是 HTTP API而是库、数据库驱动、消息客户端等就在 handler 内部直接用其原生 SDK 调用这正是原文档描述的通用形态。此时适配器进程需要持有服务的客户端并管理其生命周期handler 作为翻译层做参数转换与结果整理。两种方案可以混合一个适配器里HTTP 型操作交给引擎转发库型操作走进程内 handler。错误映射把服务错误翻译成 iii 函数错误原文档 TODO 明确提到错误映射。iii 函数的错误语义在 Creating Workers / Functions — Return values and errors 中有清晰定义正常路径handler 返回一个值值的形状应匹配函数注册时声明的response_format异常路径handler 内抛出的错误会作为 invocation error 传播回调用方并附上栈信息——Node 转发error.stackPython 转发traceback.format_exc()Rust 转发底层错误的栈。引擎不会吞掉这些错误。映射策略因此很明确可预期的失败如业务校验失败、上游 4xx返回结构化错误值例如{ ok: false, error: { code: not_found, message: ... } }让调用方拿到稳定的错误契约不可预期的失败网络中断、上游 5xx、内部异常抛出/返回 Err让错误携带栈信息传播出去便于排查。对应地在方案 AHTTP 转发中非 2xx 会被当作 invocation failure 回传适配器本身不需要额外处理在方案 B中你需要主动把服务异常翻译成上面两种语义之一。认证与密钥用环境变量名代替明文密钥原文档 TODO 特别提到如何处理认证/密钥这是适配器最容易踩坑的地方。iii 的答案是配置里只写环境变量的名字不写密钥本身。HttpAuthConfig在 engine/src/invocation/auth.rs 中定义为三种形态的枚举serde(tag type)与 sdk/packages/node/helpers/src/http/index.ts 的 TypeScript 类型一一对应type字段语义bearertoken_keyBearer Token 认证token_key是环境变量名hmacsecret_keyHMAC 签名校验secret_key是环境变量名api_keyheadervalue_key自定义请求头携带 API Keyvalue_key是环境变量名引擎的注册与调用流程保证了密钥不会泄露注册时校验存在性HttpAuthConfig::validate()检查所引用的环境变量是否存在缺失时返回missing_env_var错误提示先设置该变量engine/src/invocation/auth.rs调用时解析值resolve_auth_ref()在引擎进程内从自己的环境变量读取真实密钥拼装成HttpAuthBearer/HMAC/ApiKey后随 HTTP 请求发出engine/src/invocation/auth.rs密钥永不经过 SDK WebSockettoken_key、secret_key、value_key都只是变量名因此密钥本身只存在于引擎宿主机环境里不会随函数注册消息在网络上传送。引擎对该行为的测试覆盖在 engine/src/invocation/auth.rs包括三种认证方式成功/缺失环境变量的validate用例以及resolve_auth_ref正确解析/缺失时报secret_not_found、token_not_found、api_key_not_found的用例。安全提示如果你的适配器 Worker 会暴露给不受信任的调用方浏览器、第三方进程不要让它直连默认的 49134 可信监听端口。应通过iii-worker-manager的 RBAC 监听器接入用你编写的auth函数逐连接鉴权并用expose_functions白名单支持match(...)glob 与metadata:选择器限制其可见函数面——详见 Creating Workers / Access Control。为适配器补充契约与元数据请求/响应 Schema注册函数时可以附加 JSON Schema 描述请求与响应形态request_format/response_format。这些 schema 随函数存储出现在 iii console、iii trigger id --help以及 agent 可读的 skills 中Creating Workers / Functions — Attach request and response schemas。注意运行时校验目前尚未支持schema 仅作为契约文档引擎不会拒绝不匹配的 payload 或返回值。这并不影响适配器的可用性但对调用方尤其是 agent是重要的契约来源。metadatametadata是注册函数时附带的一段任意 JSON引擎不解释它只随函数存储供系统其他部分使用Creating Workers / Functions — Attach metadata。对适配器而言很有价值的两个用法标记适配器暴露的公共函数{ public: true }配合 worker-manager 的 RBACmetadata:选择器做暴露控制Creating Workers / Access Control给函数打领域标签{ tier: premium }、{ owner: payments-team }供 console 与发现工具使用。绑定 Trigger适配器注册的函数可以像任何普通函数一样被绑定到 Triggerworker.registerTrigger({ type, function_id, config })。例如把适配器的函数同时暴露成 HTTP 端点和每周定时任务Using iii / Triggers// 同一个函数HTTP POST cron 定时两种事件源都能触发 worker.registerTrigger({ type: http, function_id: acme::get-price, config: { api_path: /acme/price, http_method: POST }, }); worker.registerTrigger({ type: cron, function_id: acme::get-price, config: { expression: 0 0 9 * * 1 }, // 每周一 09:00 });还可以用condition_function_id给 Trigger 加条件门控Trigger 触发时引擎先调用条件函数只有返回真值才执行目标函数Using iii / Triggers — Gate a trigger with a condition。部署适配器清单与运行用iii.worker.yaml管理适配器可选如果希望 iii 内置虚拟化来替你启动适配器就在适配器目录根部放一个iii.worker.yaml清单声明运行时镜像、依赖安装与启动脚本完整字段见 Creating Workers / Worker manifestname: acme-adapter description: Thin adapter over the ACME pricing API. runtime: base_image: docker.io/iiidev/python:latest scripts: install: pip install -e . start: watchfiles python src/adapter.py env: LOG_LEVEL: info # 注意III_URL / III_ENGINE_URL 会被引擎过滤由引擎自行设置连接地址env中注入的环境变量会进入 Worker 进程——如果你的适配器走方案 B进程内调用可把服务密钥放在这里仅当适配器进程本身是可信的。iii.worker.yaml只在你让 iii 替你运行 Worker 时才需要。自己跑进程如node ./adapter/src/index.js时完全不需要清单任何调用registerWorker()并连接的进程都是 Worker行为与 iii 启动的 Worker 完全一致Creating Workers / Worker manifest。加入项目本地开发的适配器iii worker add ./workers/acme_adapter本地目录已发布到 registry 的适配器iii worker add acme-adapter或固定版本iii worker add acme-adapter1.2.0从 OCI 镜像仓库拉取iii worker add ghcr.io/org/acme-adapter:tag。安装会写入项目的config.yaml并自动启动iii.lock记录解析版本保证跨机器可复现Using iii / Workers。调用适配器适配器上线后调用方通过函数 ID 以三种方式使用它进程内调用任何 Worker 代码里worker.trigger({ function_id: acme::get-price, payload: {...} })默认同步等待返回也可以传TriggerAction.Void()即发即忘或TriggerAction.Enqueue({ queue })走队列带重试Using iii / Functions — Triggering functionsCLI 调用iii trigger acme::get-price skuSKU-001。由于请求/响应 schema 的存在iii trigger acme::get-price --help还能直接打印该函数的参数与说明是开发期调试适配器的利器事件驱动把适配器函数绑定到 http / cron / queue / state 等 Trigger见上文。源码佐证与延伸阅读适配器模式的实现事实在仓库中有多处直接证据函数注册、Schema、metadata、HTTP 转发函数与错误语义docs/creating-workers/functions.mdx连接引擎、Worker 生命周期与断开处理docs/creating-workers/workers.mdxTrigger 绑定、条件门控、注册时机无关性docs/using-iii/triggers.mdx、docs/creating-workers/triggers.mdxHTTP 转发配置的精确类型定义sdk/packages/node/helpers/src/http/index.tsHttpInvocationConfig、HttpAuthConfig引擎侧认证解析与校验含测试engine/src/invocation/auth.rs引擎侧 HTTP 调用器默认超时、状态码语义、URL 校验engine/src/invocation/http_invoker.rs适配器接入 RBAC 监听器的配置样例docs/creating-workers/worker-manager.mdx。一句话总结适配器模式 连接引擎 一操作一函数 handler 内调用存量服务。它让第三方 API、老库、内部微服务以service::name的身份平等加入 iii 系统调用方无需关心底层接口差异错误按预期失败返回结构化值、意外失败抛出来映射密钥通过环境变量名引用、由引擎解析做到不落明文、不上网络。【免费下载链接】iiiEffortlessly compose, extend, and observe every service in real-time for the first time ever.项目地址: https://gitcode.com/GitHub_Trending/mo/iii创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进