Sim 集成工具开发规范:从服务 API 到注册表全流程实战指南
【免费下载链接】simSim is the collaborative workspace to build, deploy, and monitor AI agents and workflows. Used by 100,000+ builders.项目地址: https://gitcode.com/GitHub_Trending/sim16/sim
导读
本文以 Sim 仓库中 apps/sim/tools/AGENTS.md 为骨架,系统讲解在 Sim 中为外部服务(Slack、Exa、Airtable 等数百个集成)开发、维护工具定义(Tool Definition)的完整规范:目录结构、工具 ID 命名、参数可见性分级、响应转换与注册表对齐。读完本文,你将掌握"以服务 API 文档为起点 → 按目录约定建模 → 定义参数可见性 → 转换响应 → 注册工具"的标准开发链路,并能对照仓库中的真实工具实现(如 Exa Search、Slack 消息读取)落地自己的集成工具。
一、适用范围与核心原则
apps/sim/tools/AGENTS.md开篇即划定边界:这些规则适用于apps/sim/tools/**下的所有集成工具定义(integration tool definitions)。所谓集成工具,指的是 Sim 工作流与 Copilot 中可被 LLM 调用的、面向外部 SaaS 服务的能力单元——从仓库 apps/sim/tools 的顶层目录可以看到,它涵盖了 a2a、affinity、airtable、github、gmail、slack、snowflake、stripe 等数百个服务,每个服务一个目录。
整套规范的核心原则可以概括为一句话:先读服务 API 文档,再动手写工具("Start from the service API docs before adding or changing a tool")。这意味着:
- 工具的参数、请求、响应结构必须以服务官方 API 为唯一事实来源;
- 任何对既有工具的修改,也应回到 API 文档核对字段含义与取值范围;
- 工具是 Sim 对外部世界的"翻译层",不是 API 的直通代理——它要负责把原始响应提炼成 LLM 与用户可消费的结构化输出。
这一原则在 Exa Search 工具 中体现得很典型:url指向https://api.exa.ai/search,body构造严格遵循 Exa API 的请求契约(如/search下内容选项嵌套在contents字段中,与/contents的顶层字段不同),注释明确写明了这一差异。
二、目录结构约定:一服务一目录,一动作一文件
2.1 标准文件布局
AGENTS.md 规定每个服务必须位于tools/{service}/下,并包含三个必备要素:
index.ts:作为该服务的统一导出入口,聚合该服务下的所有工具;types.ts:定义该服务的参数类型(XxxParams)与响应类型(XxxResponse),并承载可复用的输出属性常量;- 每个 action 一个文件:一个工具(一次 API 操作)对应一个独立文件。
以 Slack 服务 为例,其目录下有get_message.ts、list_channels.ts、schedule_message.ts、message.ts等几十个 action 文件,每个文件只负责一个工具定义;index.ts 则统一export出slackMessageTool、slackGetMessageTool、slackListChannelsTool等全部工具,并同时按名称导出,供 registry.ts 引用。
2.2 为什么这样拆分
从工具定义的类型结构(见 types.ts 中的ToolConfig)可以推断,每个工具定义体量不小:包含id/name/description/version、完整的params参数 schema、request请求构造、transformResponse响应转换、outputs输出 schema,以及可选的oauth、hosting、postProcess等配置。一动作一文件的好处是:
- 每个文件的职责单一,便于审阅与测试(仓库中大量存在
exa.test.ts、operations.test.ts等与工具文件一一对应的测试); - 增加新 API 操作时不触碰既有工具,降低回归风险;
- registry 的导入关系清晰可追踪。
三、工具 ID 规范:snake_case 与注册表精确对齐
3.1 硬性要求
AGENTS.md 明确两条 ID 规则:
- 工具 ID 必须使用
snake_case(如slack_get_message、exa_search); - 工具 ID 必须与 registry 中的键精确匹配("match registry keys exactly")。
3.2 ID 解析机制
tool-ids.ts 提供了 ID 解析的底层实现:
getToolIds()返回全部已注册 ID(从 generated/tool-ids 生成的静态列表导入,避免加载整个可执行注册表,注释说明这样可将体积从约 4 MB 降到约 100 KB);resolveToolId(toolName)支持版本化 ID 解析:当传入不带版本后缀的名称时,会通过getLatestByBaseName()映射到最新版本——例如notion_search会被解析为notion_search_v2;hasToolId(toolId)用于判断某 ID 是否为内置工具。
因此,在新增工具时,ID 的选择不仅影响展示,还直接参与版本解析逻辑。带_v{n}后缀的 ID 会被识别为版本化工具,且同名基础 ID 只会保留版本号最高者。
3.3 registry.ts 中的对齐
registry.ts(11434 行)是该规范落地的集中体现:文件按服务分组,通过import { xxxTool } from '@/tools/{service}'引入每个工具,再统一放入 registry 对象。例如 Airtable 的 9 个工具、Ashby 的 40+ 个工具都以airtableCreateRecordsTool、ashbySearchCandidatesTool的形式成组注册。新增工具时必须同时完成"导出(index.ts)→ 注册(registry.ts)"两步,且 ID 完全一致,否则工具将无法被运行时解析。
四、参数可见性分级:三种 visibility 的语义与使用场景
4.1 类型定义
AGENTS.md 规定的三条 visibility 规则,其完整枚举定义在 types.ts 中:
export type ParameterVisibility = | 'user-or-llm' // User can provide OR LLM must generate | 'user-only' // Only user can provide (required/optional determined by required field) | 'llm-only' // Only LLM provides (computed values) | 'hidden' // Not shown to user or LLM对应 AGENTS.md 的三条指令:
| visibility | 适用场景 | 说明 |
|---|---|---|
hidden | 系统注入的参数 | 典型如 OAuth access token——由授权流程自动注入,用户与 LLM 均不可见、不可填写 |
user-only | 凭据与账户特定值 | API key、bot token、账户相关参数,用户必须自行提供 |
user-or-llm | 普通操作参数 | 查询词、数量、日期等业务参数,用户可填、LLM 也可生成 |
(llm-only属于"仅 LLM 提供"的计算值,用于更细的边界控制。)
4.2 源码实例印证
hidden的典型用法——Slack Get Message 工具:
accessToken: { type: 'string', required: false, visibility: 'hidden', description: 'OAuth access token or bot token for Slack API', },该工具的oauth配置声明provider: 'slack',授权完成后 token 由系统注入到accessToken参数,并在请求头中作为 Bearer 使用:Authorization: \Bearer ${params.accessToken || params.botToken}``。用户与模型都看不到也不该触碰这个字段。
user-only的典型用法——Exa Search 工具:
apiKey: { type: 'string', required: true, visibility: 'user-only', description: 'Exa AI API Key', },apiKey只允许用户提供;同时 Exa 工具还声明了hosting配置(envKeyPrefix: 'EXA_API_KEY'、byokProviderId: 'exa'),意味着当用户未自带 key 时,Sim 可以注入平台托管的 API key(详见下文第五节)。
user-or-llm的典型用法——同文件中的query、numResults、type、includeDomains等操作参数,用户和 LLM 都有权提供,是绝大多数业务参数的标准选择。
4.3 为什么这样设计
可见性分级直接关系到安全与可用性边界。在 tools/index.ts 的执行逻辑中可以看到其深层作用:
- 只有
visibility: 'user-only'的参数才允许进行环境变量引用解析(resolveToolEnvReferences),且严格限定为"整值恰好是一条引用"({{NAME}}),从而保证 LLM 可写的 URL、header、body 等参数永远无法被用来提取密钥; hidden参数(如 accessToken)不会进入模型上下文,避免敏感凭据泄漏到提示词或日志。
五、托管 API Key(Hosting)配置
AGENTS.md 虽未展开,但仓库中大量工具(如 Exa、Serper、Firecrawl 等)都配置了hosting字段,这是理解"用户不带 key 也能用工具"的关键。配置结构定义在 types.ts 的ToolHostingConfig中:
hosting: { envKeyPrefix: 'EXA_API_KEY', // 环境变量前缀 apiKeyParam: 'apiKey', // 接收 key 的参数名 byokProviderId: 'exa', // BYOK provider ID pricing: { type: 'custom', getCost: ... }, // 计费模型 rateLimit: { mode: 'per_request', requestsPerMinute: 60 }, // 限流 }5.1 环境变量约定
ToolHostingConfig的文档注释明确了一套编号约定:
- 设置
{envKeyPrefix}_COUNT声明可用 key 的数量; - 依次提供
{envKeyPrefix}_1、{envKeyPrefix}_2…{envKeyPrefix}_N。
例如envKeyPrefix: 'EXA_API_KEY'且配置 5 个 key 时:
EXA_API_KEY_COUNT=5 EXA_API_KEY_1=sk-... EXA_API_KEY_2=sk-... EXA_API_KEY_3=sk-... EXA_API_KEY_4=sk-... EXA_API_KEY_5=sk-...单 key 部署时,未配置_COUNT的情况下也支持直接使用{envKeyPrefix}本身。扩容只需更新 COUNT 并新增环境变量,无需改代码。
5.2 运行时注入流程
从 tools/index.ts 的injectHostedKeyIfNeeded可以看到完整决策链:
- 工具未配置
hosting或非托管环境 → 不注入; tool.hosting.enabled谓词不满足 → 不注入;- 用户已提供
apiKeyParam→ 不注入(尊重自带 key); - 配置了
byokProviderId且工作区存在 BYOK key → 优先使用工作区自带 key(不计费); - 否则通过
getHostedKeyRateLimiter().acquireKey()从环境变量池中轮询分配一个托管 key,并标记__usingHostedKey; - 若工作区被限流则抛
HostedKeyRateLimitedError,无 key 可用则抛HostedKeyUnavailableError。
此外还有配套的成本核算:calculateToolCost支持per_request(固定每次调用费用)与custom(根据参数与响应动态计算)两种定价模型;assertBillableCost会拒绝NaN/Infinity/负数,避免污染计费账本。__costDollars这类以双下划线开头的内部字段会在返回用户前由stripInternalFields剥离(postProcessToolOutput)。
六、transformResponse:提炼有意义字段,而非转储原始 JSON
6.1 规范要求
AGENTS.md 的两条响应处理规则:
- 在
transformResponse中提取有意义字段,而不是把原始 JSON 原样丢给调用方; - 可空字段用
?? null,可选数组用?? []兜底。
6.2 实例一:Exa Search 的结构化提炼
Exa Search 的transformResponse将上游返回的results数组逐字段映射为稳定的、有语义的输出对象:
transformResponse: async (response: Response) => { const data = await response.json() return { success: true, output: { results: (data.results ?? []).map((result: any) => ({ id: result.id, title: result.title || '', url: result.url, publishedDate: result.publishedDate, author: result.author, summary: result.summary, favicon: result.favicon, image: result.image, text: result.text, highlights: result.highlights, highlightScores: result.highlightScores, subpages: result.subpages, entities: result.entities, extras: result.extras, score: result.score, })), requestId: data.requestId, structuredOutput: data.output?.content, grounding: data.output?.grounding, __costDollars: data.costDollars, }, } }值得注意的细节:data.results ?? []正是规范中"可选数组用?? []"的落地;而__costDollars作为内部计费字段,会在输出前被剥离(见上文 5.2)。
6.3 实例二:Slack Get Message 的字段归一化与错误翻译
Slack Get Message 的transformResponse展示了更丰富的处理手法:
错误码翻译——把 Slack 的 API 错误码转换为可诊断的中文/人话信息:
if (!data.ok) { if (data.error === 'missing_scope') { throw new Error('Missing required permissions. Please reconnect your Slack account with the necessary scopes (channels:history, groups:history, im:history, mpim:history).') } if (data.error === 'invalid_auth') { throw new Error('Invalid authentication. Please check your Slack credentials.') } ... }?? null兜底——响应对象中的每个可空字段都显式兜底:
const message = { type: msg.type ?? 'message', ts: msg.ts, text: msg.text ?? '', user: msg.user ?? null, bot_id: msg.bot_id ?? null, username: msg.username ?? null, ... reactions: msg.reactions ?? [], is_starred: msg.is_starred ?? false, pinned_to: msg.pinned_to ?? [], files: (msg.files ?? []).map(...), }同时它还做了一致性校验:请求指定timestamp后,若返回消息的ts与请求不符则抛错(Message not found at timestamp ...),防止 API 返回了相邻线程消息却被误当作目标消息。
6.4 outputs 声明
除了transformResponse,规范还要求用outputs字段声明结构化输出 schema(见 types.ts 的ToolOutputProperty)。Exa 工具为results数组的每个属性(id、title、url、publishedDate、score等)都写了类型与描述,Slack 工具则复用MESSAGE_OUTPUT_PROPERTIES常量(定义于 types.ts)保持多处输出定义一致。这样 LLM 与下游才能可靠地消费工具结果。
七、OAuth 与凭据处理
7.1 oauth 配置
需要用户授权的服务在工具定义中声明oauth块(结构见 types.ts 的OAuthConfig):
oauth: { required: true, // 该工具是否必须 OAuth 授权 provider: 'slack', // 授权服务 requiredScopes?: string[], // 需要的具体 scope(粒度校验) credentialKind?: 'oauth' | 'service-account', // 限定凭据类型 authoritativeParams?: [...], // 授权响应中必须覆盖同名调用参数的字段 }Slack 工具中required: true即意味着:未授权时工具不可用,运行时必须先完成 Slack 账号连接。
7.2 凭据选择的强制校验
从 tools/index.ts 的enforceCopilotCredentialSelection可以看出,Copilot 场景下工具执行有额外的凭据选择强制逻辑:若工具oauth.required为 true 且调用未显式传入credentialId/oauthCredential/credential,执行会直接报错,提示模型从environment/credentials.json读取确切的credentialId。这保证了"由谁提供凭据"始终是显式的。
八、注册与一致性:让工具真正可用
最后一步是保证工具进入运行时。依据 AGENTS.md 与 tool-ids.ts、registry.ts 的实现,完成一个工具需要串起以下环节:
- 阅读服务 API 文档,确定操作、参数与响应契约;
- 在
tools/{service}/下新建 action 文件(如search.ts),实现完整的ToolConfig:id(snake_case)、name、description、version、params(含 visibility)、request、transformResponse、outputs; - 在
tools/{service}/types.ts中补充XxxParams/XxxResponse类型; - 在
tools/{service}/index.ts中导出工具; - 在 tools/registry.ts 中注册,并确保注册键与导出的工具 ID 完全一致;
- 可选:为该工具补充单元测试(仓库中已有大量先例,如 exa.test.ts、list_channels.test.ts、operations.test.ts)。
从仓库现状看,这套规范已经支撑起了数百个服务的数千个工具定义(registry.ts 单文件即超过一万行),generated/tool-ids与 metadata.ts 等生成/元数据模块也依赖同一套 ID 体系——ID 一致性是整个工具系统的地基。
九、总结
apps/sim/tools/AGENTS.md虽然只有 13 行,却浓缩了 Sim 集成工具开发的核心纪律:
- 流程上:先 API 文档、后代码,工具是 API 的语义化翻译层;
- 结构上:一服务一目录、一动作一文件、
index.ts+types.ts兜底; - 命名上:snake_case、版本化 ID、与 registry 精确对齐;
- 边界上:
hidden/user-only/user-or-llm三级可见性 + OAuth/托管 key 凭据体系; - 输出上:提炼字段、
?? null/?? []兜底、显式 outputs schema。
对想要为 Sim 添加新集成的开发者而言,最稳妥的路径是:先找一个与目标服务形态最接近的既有工具(如 HTTP 类参考 Exa、OAuth 类参考 Slack),照着它的结构与 AGENTS.md 的规则逐步补齐,最后在 registry 中注册并通过测试验证。
延伸阅读:工具执行期逻辑可继续阅读 tools/index.ts(凭据注入、限流重试、成本核算)与 tools/utils.ts(参数合并与校验);参数解析细节可参考 params-resolver.ts 与 operation-input.ts;SDK/API 层调用方可在 lib/copilot 与 lib/internal/tool-operations 中继续追踪。
【免费下载链接】simSim is the collaborative workspace to build, deploy, and monitor AI agents and workflows. Used by 100,000+ builders.项目地址: https://gitcode.com/GitHub_Trending/sim16/sim
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考