Connector 插件——可安装、消费方无关

更新于 · 在 sijie.xyz 查看原条目 ↗

Connector 不是 MCP——它讲的是分类契约(category contract),而不是 MCP 的线协议;这两条插件轴仅在 connector-deps(confusables,含 action 与 sync 模式的区分)处相交。

术语约定:

  • Consumer(消费方)= agent、平台功能、未来的 IM 网关 / job-loop
  • Category Contract(分类契约)= 平台自有的接口(如 CalendarContract、MailContract)
  • Connector Binding(connector 绑定)= 供应商操作到分类契约的声明式映射(YAML)
  • Protocol kind(协议型)= 内置的 Go 实现(SMTP/CalDAV,外加没有契约的 Telegram token 保险箱;backend/internal/connector/ 里不存在 IMAP 或 LDAP 实现)
  • OpenAPI kind(OpenAPI 型)= 作者提供的 OpenAPI spec + binding(面向单个 SaaS 的 HTTP)

定义:带凭证的外部集成

一个 connector 是一个带凭证的外部集成,与消费方无关,结构上完全类似 WordPress 插件。任何人都可以编写一个;owner 把它安装进自己的实例。Hub 是中立的基座;我们不打包 Nango 的目录。

关键性质:消费方(agent、平台功能、未来的网关)永远不持有凭证。凭证始终封存在 connector 这一层。

两种类型(透明可互换)

一个分类(如日历、邮件)可以由两种类型中的任意一种来满足:

  • openapi——基于 HTTP 的 SaaS。作者提供 OpenAPI 3.0 spec + binding YAML。凭证表单从 securitySchemes 派生(oauth2、apiKey、http basic、bearer);存在多种时由 owner 在 UI 中选择。
  • protocol——标准协议(SMTP、CalDAV;Telegram 自 2026-09-04 起,8c819f26d —— protocol_telegram.go,分类 im,只存 token,由 im-bridge 消费而非经分类契约)。内置 Go 实现;凭证描述符固定。一份实现覆盖长尾。

两者拥有相同的插件形状和部署路径。

三层架构

① Category Contract(平台自有,固定)
定义一个分类必须支持哪些操作。例如:

  • CalendarContract:list_busy(range) → bool 网格,create_event(title, when, attendees) → event,cancel_event(id)
  • MailContract:send(to, subject, body, attachments) → message ID

② Connector Binding(声明式 YAML)
把契约映射到供应商的操作上。对 OpenAPI:契约方法 → operationId,请求与响应各自带 JSONata 变换。对 protocol:直接实现。

③ Generic runtime(契约调用 → 结果)
OpenAPI:调用 → 注入 token → HTTP 调用 → 归一化响应。Protocol:直接调用协议客户端。

这三层都不依赖任何具体供应商。内核不含任何供应商专属代码(google-calendar / smtp 这类名字从不出现在 host 里)。

已锁定的决定(design lock)

  • 映射语言: 只用 JSONata(先例是 AWS Step Functions)。
  • OpenAPI 版本: 只支持 3.0。
  • 多重鉴权: 当供应商定义了多个 securityScheme 时,owner 在连接时于 UI 中选一个。
  • 内置件形状: 内置 connector(随仓库发布)与上传的 connector 形状完全相同。

代码现状

文件布局:

  • backend/internal/connector/ —— Hub、分类槽位、OpenAPI 的 spec/binding/runtime/ingest、protocol_* 实现、builtins/data
  • connector.Service(service.go + svc_*.go;connectorsvc 包已于 2026-07-26 合并进来,1bc9ba8b0)—— 凭证(经 cryptobox 的 AES-GCM)、connect、oauth、activate、disconnect
  • capreg/depresolver.go —— manifest 中的 Requires:["calendar"] → 具名依赖供应商;不满足时 → 能力隐藏,fail-closed
  • 按调用类别(call-class)设置的重试策略
  • 插件收到的是一个调用用的 HANDLE,从不是原始凭证

凭证处理:
静态加密存储,只在 connector 包内部解密。通过 AuthInjector 闭包按请求注入到 *http.Request 中。包外的调用方只能看到一个 Connected 布尔值加结果数据。

类视图——契约在上,类型在下

classDiagram
  class CalendarProxy {
    <<interface - connector/contract, consumer-facing>>
    +Connected(ctx, ownerID) (bool, error)
    +FreeBusy(ctx, ownerID, FreeBusyReq) ([]BusyInterval, error)
    +InsertEvent(ctx, ownerID, *InsertEventReq) (InsertedEvent, error)
    +DeleteEvent(ctx, ownerID, eventID, attendeeEmail) error
  }
  class MailProxy {
    <<interface - connector/contract, consumer-facing>>
    +Connected(ctx, ownerID) (bool, error)
    +Send(ctx, ownerID, MailMessage) (MailReceipt, error)
  }
  class MailMessage {
    To, Subject string
    Body, HTML string
  }
  class Connector {
    <<interface - hub-facing>>
    +Name() string
    +Kind() string
    +Connected(ctx, ownerID) (bool, error)
  }
  class Hub {
    -conns map[string]Connector
    +Register(c) / Upsert(c)
    +Resolve(name) (Connector, bool)
  }
  class openapiRuntime {
    -spec *Spec
    -binding *Binding - JSONata
    -doer Doer
    -baseURL string
    +Call(ctx, op, input, dst, AuthInjector) error
  }
  class AuthInjector {
    <<func type>>
    func(req *http.Request) error
    creds sealed - built inside connector pkg
  }
  class protocolImpl {
    built-in Go clients
    SMTP, CalDAV
  }
  CalendarProxy <|.. openapiRuntime : adapter
  CalendarProxy <|.. protocolImpl : adapter
  MailProxy <|.. openapiRuntime : adapter
  MailProxy <|.. protocolImpl : adapter
  MailProxy ..> MailMessage
  Hub o-- Connector : by name
  openapiRuntime ..> AuthInjector : per call

刻意拆成两个朝向:消费方手上拿的是 contract 层的 proxy(backend/internal/connector/contract/ 里的 CalendarProxy/MailProxy——分类动词,哪里都看不到供应商;Send 返回带供应商消息 id 的 MailReceipt);Hub 追踪的是 Connector(name/kind/connected——只管生命周期)。到底是哪种类型在满足某个 proxy,对消费方不可见。这已经是 Bridge/Strategy 的形状;按 judgment-audit 的规则,不需要再叠加别的模式。

红色契约测试套件(已建成:36789537d、2026-09-07 时 66 个 connector-*.spec.ts)

每个区域都由可执行的红色测试钉住:

  • Ingest —— spec 解析、binding 校验
  • Cred-form derivation —— securitySchemes → UI 表单 schema
  • JSONata binding —— 对请求与响应做变换
  • Connect flow —— OAuth 刷新、凭证存储
  • Protocol SMTP —— 内置的 SMTP 客户端(mailer_smtp.go;没有 IMAP 客户端)
  • Consumption loop —— 消费方调用契约方法
  • Upload mgmt —— owner 上传、生命周期、删除
  • Security —— SSRF 拒绝内网服务器、凭证不泄漏、按 owner 隔离(与 connector-egress-guard 有重叠)

由红色测试钉住的设计决定:

  • 每个分类槽位同一时刻只有一个生效的 connector
  • 面向 agent-tool 的暴露按操作逐一 opt-in(op_<operationId>,逐操作 ACL)
  • 断开连接(disconnect)保留凭证
  • 拒绝外部 $ref(不允许跨文件的 schema 组合)

两种模式:action(proxy)与 sync(ingest)

Action(proxy)——同步消费。消费方调用 connector;它代理到供应商并返回结果。这是主路径(agent 工具、平台功能)。

Sync(ingest)——异步摄取。connector 按计划(或由事件触发)拉取数据。Obsidian vault 同步已经是一个 sync 模式的 connector(NewSyncConnector + SyncIngester),corpus 支柱与这根支柱相接的那道缝已经合上。

状态(2026 年 7 月,2026-09-07 于 36789537d 复核):已落地

proxy 层更早以前就已落地;如今可安装、与消费方无关的设计也落地了——owner 上传流程是真实代码(connection_repo_uploaded.go 里的 Repo.SaveUploaded/UpdateUploaded,svc_manage.go 里的 Service.UpdateUploaded),红色契约测试套件已长成 66 个 e2e spec 文件(deps/retry/security/upload/ingest/binding/matrix),早先 13 个 test.fixme 的尾巴现已全部转成了实测,TODO(impl) 的 mock 基础设施缺口已于 2026-07-03 关闭(059dc5c13)——剩 0 个;落地之后的打磨还在继续(assemble 重设计去掉了供应商下拉框;通用 connector 形状重构;credform 从 authform 派生而来;OAuth 流程带 PKCE,e23c0c9f4,2026-08-20)。sync 模式(那道 Obsidian 缝)已于 2026-07-08 落地(d51805372,backend/internal/connector/sync.go)。

相关笔记