前言
在上一篇中,我们梳理了 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,包含id、configSchema(JSON Schema)、可选的kind、channels、providers、skills、name、description、version、uiHints。配置校验 仅依赖清单与 schema,不加载插件实现。 - 配置状态(Config state):
plugins.enabled、plugins.allow/plugins.deny、plugins.load.paths、plugins.slots.memory、plugins.entries.<id>.enabled/config决定哪些插件被启用以及传入的配置对象。 - 加载器(Loader):在发现与清单校验通过后,用 jiti 加载插件入口(如
index.ts),解析default或register/activate,调用register(api);api 提供registerChannel、registerTool、registerGatewayMethod、registerCli、registerCommand、registerHook、registerService、registerProvider、registerHttpRoute等。同一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 的优先级规则。
- 来源:bundled(随安装包)、managed(
-
插件 vs 技能:
- 插件:扩展运行时能力(新渠道、新工具、新 CLI、新 Gateway 方法、新服务、新提供商认证)。必须实现
register(api)并满足清单 + configSchema。 - 技能:扩展 Agent 的「说明书」与工具使用策略,不提供新代码入口;插件可以 顺带 提供技能目录(如
extensions/open-prose/skills),由清单的skills字段声明。
- 插件:扩展运行时能力(新渠道、新工具、新 CLI、新 Gateway 方法、新服务、新提供商认证)。必须实现
1.3 扩展开发最佳实践与检查清单
- 清单:始终提供
clawdbot.plugin.json,且configSchema为合法 JSON Schema(可为空对象{ "type": "object", "additionalProperties": false })。 - 依赖:插件依赖放在插件自己的
package.json的dependencies;避免在根package.json增加仅插件需要的依赖。不要使用workspace:*在dependencies里引用 monorepo 包(npm install 会出问题);可将clawdbot放在devDependencies或peerDependencies,运行时通过 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>.enabled或plugins.allow/plugins.deny控制。 - 测试与发布:为插件编写单元测试、在本地用
plugins.load.paths指向插件目录验证,再发布 npm 或提供安装文档。
二、代码解析
2.1 发现与清单
发现(src/plugins/discovery.ts):
discoverClawdbotPlugins({ workspaceDir, extraPaths }) 会:
- 对
extraPaths(即plugins.load.paths)中的每一项调用discoverFromPath(支持单文件或目录); - 若为目录,优先看
package.json的clawdbot.extensions数组,有则把这些入口当作候选;否则找目录下的index.ts/index.js等;若为多子目录则递归discoverInDirectory; - 再扫描
workspaceDir/.clawdbot/extensions、~/.clawdbot/extensions、以及 resolveBundledPluginsDir() 指向的 bundled 目录。 - 每个候选得到 PluginCandidate:
idHint、source(入口文件绝对路径)、rootDir(插件根目录,用于找清单)、origin(config|workspace|global|bundled)、packageName/packageVersion等。
清单(src/plugins/manifest.ts):
- resolvePluginManifestPath(rootDir):
path.join(rootDir, "clawdbot.plugin.json")。 - loadPluginManifest(rootDir):读 JSON,校验必填字段
id、configSchema(且为对象),解析kind、channels、providers、skills、name、description、version、uiHints,返回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):
- normalizePluginsConfig(cfg.plugins) → 得到
enabled、allow/deny、loadPaths、slots.memory、entries。 - discoverClawdbotPlugins + loadPluginManifestRegistry,得到候选与清单记录。
- 解析 plugin-sdk 路径,createJiti 时设置
alias: { "clawdbot/plugin-sdk": pluginSdkAlias },便于插件里import ... from "clawdbot/plugin-sdk"。 - 按候选顺序遍历:若清单缺失或 id 重复则跳过或标记 disabled;resolveEnableState(id, origin, normalized) 决定是否启用;resolveMemorySlotDecision 处理 memory 类插件的唯一启用;validatePluginConfig 用清单的 configSchema 校验
entries[id].config。 - 通过后 jiti(candidate.source) 加载模块,resolvePluginModuleExport(mod) 取
default或register/activate;调用 register(api),api 由 createApi(record, { config, pluginConfig }) 创建。 - 插件在
register(api)里调用api.registerChannel(...)、api.registerTool(...)等,这些会写入 PluginRegistry(channels、tools、gatewayHandlers、cliRegistrars、commands、hooks、services、providers、httpRoutes 等)。 - 结果缓存在 registryCache(可选),并 setActivePluginRegistry、initializeGlobalHookRunner(registry)。
- normalizePluginsConfig(cfg.plugins) → 得到
Registry 与 createApi(src/plugins/registry.ts):
- createPluginRegistry({ logger, coreGatewayHandlers, runtime }) 返回
registry和 createApi(record, { config, pluginConfig })。 - createApi 返回的 ClawdbotPluginApi 包含:
id、name、version、description、source、config、pluginConfig、runtime、logger,以及:- 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 执行)。 - registerHook、registerHttpRoute、registerService、registerProvider、resolvePath、on(hookName, handler) 等同理。
- registerChannel(registration):把
- 渠道注册时要求
plugin.id非空;重复的 channel id 不会在这里去重,但同一插件 id 只能注册一次渠道(由前面「同 id 仅启用一份」保证)。
2.3 Plugin SDK 与渠道插件接口
Plugin SDK(src/plugin-sdk/index.ts):
- 对外 réexport 渠道开发所需类型与 helpers:ChannelPlugin、ChannelMeta、ChannelConfigAdapter、ChannelOutboundAdapter、ChannelPairingAdapter、ChannelStatusAdapter、buildChannelConfigSchema、emptyPluginConfigSchema、各渠道的 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> 包含:
id、meta(ChannelMeta)、capabilities、config(ChannelConfigAdapter)、configSchema(可选)、pairing、security、groups、outbound、status、gateway、auth、messaging、directory、resolver、actions等可选适配器。 - 渠道插件至少实现:meta、config(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 结构要点
linePlugin(extensions/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 配置结构对应。
- configSchema:buildChannelConfigSchema(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.json中plugins.entries.line.enabled: true(或通过plugins.allow包含line);若从 npm 安装则先clawdbot plugins install @clawdbot/line(若包存在),再在plugins.entries.line.config或 channels.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 被禁用 | 清单 id 与 plugins.entries.<id>、channels.<id> 保持一致;同 id 只保留一份来源 |
| 运行时依赖在 devDependencies | clawdbot plugins install 后运行报缺模块 | 将运行时依赖写在 dependencies;install 使用 --omit=dev |
| workspace:* 在 dependencies | npm 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 });清单中声明 channels 与 configSchema。
- 实战:以 LINE 为例,从清单、入口、ChannelPlugin 结构到认证/收发/状态与配置部署,并给出常见陷阱与对策。
参考资源
- 项目仓库:github.com/clawdbot/cl…
- 官方文档:docs.clawd.bot
- 插件总览:
docs/plugin.md;清单规范:docs/plugins/manifest.md;技能:docs/tools/skills.md - 发现与清单:
src/plugins/discovery.ts、src/plugins/manifest.ts、src/plugins/manifest-registry.ts - 加载与注册:
src/plugins/loader.ts、src/plugins/registry.ts、src/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.json、index.ts、src/channel.ts)