Clawdbot 源码解读 7: 扩展机制

23 阅读11分钟

前言

在上一篇中,我们梳理了 Agent 作用域、会话管理、Pi Agent 运行时与工具注册。要让 Clawdbot 支持新的消息渠道(如 LINE、Microsoft Teams)、新的模型提供商认证或新的 Agent 能力,需要依赖 插件(extensions) 与可选的 技能(skills)。本文是《Clawdbot 源码解读》系列的第七篇,我们将深入 扩展系统与实战:插件系统架构(发现、清单、加载、配置、CLI 注册)、技能系统与「插件 vs 技能」的区别、扩展开发最佳实践,以及从需求到部署完整开发一个渠道插件的流程与常见陷阱。

学习目标

  • 理解插件发现顺序(config paths、workspace、global、bundled)与清单(clawdbot.plugin.json)的约束
  • 掌握插件加载流程:清单校验、配置校验、jiti 加载、register(api) 与各类 api.register*
  • 区分「插件」与「技能」:插件是运行时模块(渠道/工具/CLI/服务),技能是 AgentSkills 兼容的说明目录(SKILL.md)
  • 能够从零实现一个渠道插件(清单、入口、ChannelPlugin 适配器、认证/收发/状态)并完成安装与配置

前置知识

  • 已阅读系列前六篇(架构、CLI、配置、Gateway、渠道与路由、Agent 与工具)
  • 对第五篇中的 ChannelPlugin、适配器(config、gateway、outbound、pairing、status)有印象

一、核心概念

1.1 插件系统架构概览

Clawdbot 的插件是 TypeScript 模块,在运行时由 jiti 加载(支持 .ts/.mts 等),与 Gateway 同进程运行,视为受信任代码。插件系统由以下几部分组成:

  • 发现(Discovery):按固定顺序扫描「插件候选」路径,得到 PluginCandidate[](source、rootDir、origin、package 信息等)。不执行插件代码。
  • 清单(Manifest):每个插件根目录必须提供 clawdbot.plugin.json,包含 idconfigSchema(JSON Schema)、可选的 kindchannelsprovidersskillsnamedescriptionversionuiHints。配置校验 仅依赖清单与 schema,不加载插件实现。
  • 配置状态(Config state)plugins.enabledplugins.allow/plugins.denyplugins.load.pathsplugins.slots.memoryplugins.entries.<id>.enabled/config 决定哪些插件被启用以及传入的配置对象。
  • 加载器(Loader):在发现与清单校验通过后,用 jiti 加载插件入口(如 index.ts),解析 defaultregister/activate,调用 register(api)api 提供 registerChannelregisterToolregisterGatewayMethodregisterCliregisterCommandregisterHookregisterServiceregisterProviderregisterHttpRoute 等。同一 id 多份实现时,按优先级只启用一份(后者标记为 overridden)。
  • CLI 注册:插件可通过 api.registerCli(registrar) 向 Commander 注册子命令(如 clawdbot voice-call …),或通过 api.registerCommand(def) 注册「自动回复命令」(不经过 LLM,直接执行 handler)。

发现顺序(见 src/plugins/discovery.ts):
1)plugins.load.paths 中的路径(文件或目录);
2)工作区扩展 <workspace>/.clawdbot/extensions
3)全局扩展 ~/.clawdbot/extensions
4)内置目录(bundled,如仓库内 extensions/)。同一 id 先出现的优先,后出现的标记为 disabled(overridden)。

1.2 技能系统与「插件 vs 技能」的区别

  • 技能(Skill):来自 AgentSkills 兼容的 技能目录,内含 SKILL.md(YAML frontmatter + 说明),用于教 Agent 如何使用工具、何时调用。技能 是运行时模块,只是被 Agent 系统读取并拼进 system prompt / 工具说明。

    • 来源:bundled(随安装包)、managed~/.clawdbot/skills)、workspace<workspace>/skills),以及配置中的 skills.load.extraDirs
    • 插件可通过清单中的 skills 数组声明「本插件自带的技能目录」(相对插件根目录)。这些目录在插件启用后由 resolvePluginSkillDirs 解析,并入技能加载路径,参与与 workspace/managed/bundled 的优先级规则。
  • 插件 vs 技能

    • 插件:扩展运行时能力(新渠道、新工具、新 CLI、新 Gateway 方法、新服务、新提供商认证)。必须实现 register(api) 并满足清单 + configSchema。
    • 技能:扩展 Agent 的「说明书」与工具使用策略,不提供新代码入口;插件可以 顺带 提供技能目录(如 extensions/open-prose/skills),由清单的 skills 字段声明。

1.3 扩展开发最佳实践与检查清单

  • 清单:始终提供 clawdbot.plugin.json,且 configSchema 为合法 JSON Schema(可为空对象 { "type": "object", "additionalProperties": false })。
  • 依赖:插件依赖放在插件自己的 package.jsondependencies;避免在根 package.json 增加仅插件需要的依赖。不要使用 workspace:*dependencies 里引用 monorepo 包(npm install 会出问题);可将 clawdbot 放在 devDependenciespeerDependencies,运行时通过 jiti alias 解析 clawdbot/plugin-sdk
  • 安装:官方安装流程为 clawdbot plugins install <npm-spec>,会执行 npm install --omit=dev 在插件目录;因此 运行时依赖 必须在 dependencies
  • 渠道插件:实现完整的 ChannelPlugin(meta、config/configSchema、pairing、outbound、gateway、status 等),并在 register(api)api.registerChannel({ plugin });清单中 channels 数组声明本插件注册的 channel id。
  • 配置:用户配置写在 plugins.entries.<id>.config,与 configSchema 一致;启用/禁用由 plugins.entries.<id>.enabledplugins.allow/plugins.deny 控制。
  • 测试与发布:为插件编写单元测试、在本地用 plugins.load.paths 指向插件目录验证,再发布 npm 或提供安装文档。

二、代码解析

2.1 发现与清单

发现src/plugins/discovery.ts):
discoverClawdbotPlugins({ workspaceDir, extraPaths }) 会:

  • extraPaths(即 plugins.load.paths)中的每一项调用 discoverFromPath(支持单文件或目录);
  • 若为目录,优先看 package.jsonclawdbot.extensions 数组,有则把这些入口当作候选;否则找目录下的 index.ts/index.js 等;若为多子目录则递归 discoverInDirectory
  • 再扫描 workspaceDir/.clawdbot/extensions~/.clawdbot/extensions、以及 resolveBundledPluginsDir() 指向的 bundled 目录。
  • 每个候选得到 PluginCandidateidHintsource(入口文件绝对路径)、rootDir(插件根目录,用于找清单)、originconfig|workspace|global|bundled)、packageName/packageVersion 等。

清单src/plugins/manifest.ts):

  • resolvePluginManifestPath(rootDir)path.join(rootDir, "clawdbot.plugin.json")
  • loadPluginManifest(rootDir):读 JSON,校验必填字段 idconfigSchema(且为对象),解析 kindchannelsprovidersskillsnamedescriptionversionuiHints,返回 PluginManifestLoadResult

清单注册表src/plugins/manifest-registry.ts):

  • loadPluginManifestRegistry({ config, workspaceDir, cache, candidates, diagnostics }):若未传 candidates 则先调用 discoverClawdbotPlugins;对每个 candidate 在其 rootDir 上调用 loadPluginManifest;通过则得到 PluginManifestRecord(id、name、description、version、kind、channels、providers、skills、origin、rootDir、source、configSchema、schemaCacheKey、configUiHints)。
  • 用于「仅校验配置」或供加载器决定要加载哪些插件;重复 id 会记一条诊断,后加载的同 id 在 loader 里会被标记为 overridden。

2.2 加载与注册

加载器src/plugins/loader.ts):

  • loadClawdbotPlugins(options)
    1. normalizePluginsConfig(cfg.plugins) → 得到 enabledallow/denyloadPathsslots.memoryentries
    2. discoverClawdbotPlugins + loadPluginManifestRegistry,得到候选与清单记录。
    3. 解析 plugin-sdk 路径,createJiti 时设置 alias: { "clawdbot/plugin-sdk": pluginSdkAlias },便于插件里 import ... from "clawdbot/plugin-sdk"
    4. 按候选顺序遍历:若清单缺失或 id 重复则跳过或标记 disabled;resolveEnableState(id, origin, normalized) 决定是否启用;resolveMemorySlotDecision 处理 memory 类插件的唯一启用;validatePluginConfig 用清单的 configSchema 校验 entries[id].config
    5. 通过后 jiti(candidate.source) 加载模块,resolvePluginModuleExport(mod)defaultregister/activate;调用 register(api)apicreateApi(record, { config, pluginConfig }) 创建。
    6. 插件在 register(api) 里调用 api.registerChannel(...)api.registerTool(...) 等,这些会写入 PluginRegistry(channels、tools、gatewayHandlers、cliRegistrars、commands、hooks、services、providers、httpRoutes 等)。
    7. 结果缓存在 registryCache(可选),并 setActivePluginRegistryinitializeGlobalHookRunner(registry)

Registry 与 createApisrc/plugins/registry.ts):

  • createPluginRegistry({ logger, coreGatewayHandlers, runtime }) 返回 registrycreateApi(record, { config, pluginConfig })
  • createApi 返回的 ClawdbotPluginApi 包含:idnameversiondescriptionsourceconfigpluginConfigruntimelogger,以及:
    • registerChannel(registration):把 ChannelPlugin 压入 registry.channels,并记录 record.channelIds
    • registerTool(tool, opts):支持单工具或工厂函数;名字写入 record.toolNames,工厂/工具压入 registry.tools
    • registerGatewayMethod(method, handler):若与 core 或已有方法冲突则报错,否则写入 registry.gatewayHandlers
    • registerCli(registrar):压入 registry.cliRegistrars,后续由 CLI 程序在构建时调用。
    • registerCommand(def):压入 registry.commands,用于自动回复层的「插件命令」(先于 Agent 执行)。
    • registerHookregisterHttpRouteregisterServiceregisterProviderresolvePathon(hookName, handler) 等同理。
  • 渠道注册时要求 plugin.id 非空;重复的 channel id 不会在这里去重,但同一插件 id 只能注册一次渠道(由前面「同 id 仅启用一份」保证)。

2.3 Plugin SDK 与渠道插件接口

Plugin SDKsrc/plugin-sdk/index.ts):

  • 对外 réexport 渠道开发所需类型与 helpers:ChannelPluginChannelMetaChannelConfigAdapterChannelOutboundAdapterChannelPairingAdapterChannelStatusAdapterbuildChannelConfigSchemaemptyPluginConfigSchema、各渠道的 ConfigSchema(如 LineConfigSchema)、账号解析、normalize、onboarding、allowlist、ack reactions、typing、directory 等。
  • 插件只需 import { ... } from "clawdbot/plugin-sdk",无需直接依赖 clawdbot 的完整源码;运行时通过 loader 的 jiti alias 指向仓库内 src/plugin-sdk/index.ts(或 dist 产物)。

ChannelPlugin 结构src/channels/plugins/types.plugin.ts):

  • ChannelPlugin<ResolvedAccount> 包含:idmeta(ChannelMeta)、capabilitiesconfig(ChannelConfigAdapter)、configSchema(可选)、pairingsecuritygroupsoutboundstatusgatewayauthmessagingdirectoryresolveractions 等可选适配器。
  • 渠道插件至少实现:metaconfig(listAccountIds、resolveAccount、defaultAccountId、setAccountEnabled、deleteAccount、isConfigured、describeAccount 等)、outbound(发送)、gateway(startAccount/stopAccount,拉取或接收消息);若需配对/状态/目录则实现对应适配器。

三、实战:从零开发一个渠道插件(以 LINE 为例)

下面以仓库内 extensions/line 为例,走通「需求 → 清单 → 入口 → ChannelPlugin → 配置与部署」的完整流程。

3.1 需求与产物

  • 需求:支持 LINE Messaging API 作为消息渠道(收消息、发消息、多账号、配对、状态)。
  • 产物:一个 npm 包或本地目录,包含 clawdbot.plugin.json、入口 index.ts、以及实现 ChannelPlugin 的模块(如 src/channel.ts)、运行时桥接(src/runtime.ts)、可选 CLI/卡片命令(src/card-command.ts)等。

3.2 清单

在插件根目录创建 clawdbot.plugin.json

{
  "id": "line",
  "channels": ["line"],
  "configSchema": {
    "type": "object",
    "additionalProperties": false,
    "properties": {}
  }
}
  • id:与配置中 plugins.entries.line 及渠道 channels.line 对应。
  • channels:声明本插件会注册的 channel id,便于配置校验与文档。
  • configSchema:LINE 的完整配置由 SDK 的 LineConfigSchema 在运行时通过 buildChannelConfigSchema(LineConfigSchema) 提供;清单里可用空 schema 或与 channels.line 结构一致的 schema。
  • 若插件还提供技能目录,可加 "skills": ["skills"](相对插件根目录)。

3.3 入口与注册

index.ts(简化自 extensions/line/index.ts):

import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
import { emptyPluginConfigSchema } from "clawdbot/plugin-sdk";

import { linePlugin } from "./src/channel.js";
import { registerLineCardCommand } from "./src/card-command.js";
import { setLineRuntime } from "./src/runtime.js";

const plugin = {
  id: "line",
  name: "LINE",
  description: "LINE Messaging API channel plugin",
  configSchema: emptyPluginConfigSchema(),
  register(api: ClawdbotPluginApi) {
    setLineRuntime(api.runtime);
    api.registerChannel({ plugin: linePlugin });
    registerLineCardCommand(api);
  },
};

export default plugin;
  • register(api) 中:先保存 api.runtime 供 channel 实现里解析账号、发消息等使用;再 api.registerChannel({ plugin: linePlugin }) 注册渠道;若有自定义卡片命令则 registerLineCardCommand(api)(内部可能调用 api.registerCommand 或 CLI)。
  • 配置校验不执行插件代码,仅用清单的 configSchema;若清单里用了空 schema,实际渠道配置结构由 buildChannelConfigSchema(LineConfigSchema) 在类型侧约束。

3.4 ChannelPlugin 结构要点

linePluginextensions/line/src/channel.ts)需实现:

  • meta:id、label、selectionLabel、detailLabel、docsPath、blurb、systemImage、quickstartAllowFrom 等,供 Control UI / 向导展示。
  • pairing:idLabel、normalizeAllowEntry、notifyApproval(审核通过后通过 LINE API 推一条通知)。
  • capabilities:chatTypes(direct、group)、reactions、threads、media、nativeCommands、blockStreaming。
  • config:listAccountIds、resolveAccount、defaultAccountId、setAccountEnabled、deleteAccount、isConfigured、describeAccount、resolveAllowFrom、formatAllowFrom 等,与 channels.line 配置结构对应。
  • configSchemabuildChannelConfigSchema(LineConfigSchema),与核心配置 Zod 一致。
  • reload:configPrefixes 如 ["channels.line"],便于配置热重载。
  • outbound:发送文本、附件等,内部调用 LINE Messaging API(需 channelAccessToken)。
  • gateway:startAccount(启动 webhook 或轮询)、stopAccount;与 ChannelManager 生命周期一致。
  • status(可选):collectStatusIssues,用于 clawdbot channels status --probe

认证与收发:

  • 认证:LINE 使用 Channel Access Token(与 channel secret);可放在 channels.line.channelAccessToken 或通过 token 文件/环境变量读取。
  • 收发:入站由 webhook 或 gateway 的 HTTP 处理接收,normalize 成内部消息格式后进 auto-reply;出站通过 outbound 调用 LINE Push/Reply API。
  • 状态:status 适配器可检查 token 是否有效、webhook 是否可达等。

3.5 配置与部署

  • 启用插件:在 clawdbot.jsonplugins.entries.line.enabled: true(或通过 plugins.allow 包含 line);若从 npm 安装则先 clawdbot plugins install @clawdbot/line(若包存在),再在 plugins.entries.line.configchannels.line 下配置账号与 token。
  • 渠道配置channels.line 下配置默认账号、多账号、allowFrom、webhook 等,与 LineConfigSchema 一致。
  • 本地开发:在仓库内可直接把 extensions/line 作为 bundled 被发现;或在外层通过 plugins.load.paths: ["/path/to/line"] 指向插件根目录,重启 Gateway 后生效。

四、架构图解

graph TB
  subgraph 发现
    A[plugins.load.paths] --> D[PluginCandidate]
    B[workspace/.clawdbot/extensions] --> D
    C[~/.clawdbot/extensions] --> D
    E[bundled extensions/] --> D
  end
  D --> F[loadPluginManifestRegistry]
  F --> G[PluginManifestRecord]
  G --> H[resolveEnableState / MemorySlot]
  H --> I[jiti 加载入口]
  I --> J[register api]
  J --> K[PluginRegistry]
  K --> L[channels / tools / gatewayHandlers / cliRegistrars / commands ...]
sequenceDiagram
  participant Boot as Gateway/CLI 启动
  participant Loader as loadClawdbotPlugins
  participant Discovery as discoverClawdbotPlugins
  participant Manifest as loadPluginManifestRegistry
  participant Jiti as jiti(入口)
  participant Register as register(api)
  participant Registry as PluginRegistry

  Boot->>Loader: loadClawdbotPlugins
  Loader->>Discovery: discoverClawdbotPlugins
  Discovery-->>Loader: candidates
  Loader->>Manifest: loadPluginManifestRegistry(candidates)
  Manifest-->>Loader: manifest records
  loop 每个启用候选
    Loader->>Loader: validatePluginConfig(schema, entries[id].config)
    Loader->>Jiti: jiti(candidate.source)
    Jiti-->>Loader: module
    Loader->>Register: register(createApi(record, config))
    Register->>Registry: api.registerChannel / registerTool / ...
  end
  Loader-->>Boot: PluginRegistry

五、常见陷阱与解决方案

陷阱现象解决方案
清单缺少 configSchema插件报错 "missing config schema"、Doctor 报错clawdbot.plugin.json 中提供 configSchema,至少为 { "type": "object", "additionalProperties": false }
插件 id 与配置不一致配置不生效或重复 id 被禁用清单 idplugins.entries.<id>channels.<id> 保持一致;同 id 只保留一份来源
运行时依赖在 devDependenciesclawdbot plugins install 后运行报缺模块将运行时依赖写在 dependencies;install 使用 --omit=dev
workspace:* 在 dependenciesnpm install 失败或解析错误插件不要用 workspace:* 依赖根包;用 devDependencies/peerDependencies + jiti alias
渠道未实现 gateway.startAccount渠道显示但收不到消息实现 gateway 适配器,在 startAccount 中启动 webhook 或轮询,并调 dock 注册
清单 channels 未声明配置里写 channels.xxx 被校验报未知 key在清单中 channels: ["xxx"] 声明本插件注册的 channel id
技能目录未在清单声明插件自带的 skills 未被加载clawdbot.plugin.json 中加 skills: ["相对路径"],且路径存在且含 SKILL.md

六、总结

本文介绍了 Clawdbot 的 插件与技能 扩展机制:

  • 插件:发现(config / workspace / global / bundled)→ 清单校验(id、configSchema 必填)→ 配置启用与 memory slot → jiti 加载 → register(api) → 注册渠道/工具/Gateway/CLI/命令/钩子/服务/提供商。
  • 技能:AgentSkills 兼容目录(SKILL.md),来源为 bundled、managed、workspace、extraDirs;插件可通过清单 skills 数组附带技能目录。
  • 渠道插件:实现 ChannelPlugin(meta、config、pairing、outbound、gateway、status 等),在入口里 api.registerChannel({ plugin });清单中声明 channelsconfigSchema
  • 实战:以 LINE 为例,从清单、入口、ChannelPlugin 结构到认证/收发/状态与配置部署,并给出常见陷阱与对策。

参考资源

  • 项目仓库:github.com/clawdbot/cl…
  • 官方文档:docs.clawd.bot
  • 插件总览:docs/plugin.md;清单规范:docs/plugins/manifest.md;技能:docs/tools/skills.md
  • 发现与清单:src/plugins/discovery.tssrc/plugins/manifest.tssrc/plugins/manifest-registry.ts
  • 加载与注册:src/plugins/loader.tssrc/plugins/registry.tssrc/plugins/config-state.ts
  • Plugin SDK:src/plugin-sdk/index.ts;渠道类型:src/channels/plugins/types.plugin.ts
  • 插件技能目录解析:src/agents/skills/plugin-skills.ts
  • LINE 扩展示例:extensions/line/clawdbot.plugin.jsonindex.tssrc/channel.ts