OpenClaw 学习系列之三:必知的 TypeScript 模式
TS模式
读 OpenClaw 代码前必知的 TypeScript 模式
本文不是 TypeScript 教程,而是聚焦于 OpenClaw 代码库中实际使用的模式。
掌握这些模式后,你打开任何源文件都不会感到陌生。
1. ESM 模块和动态导入
OpenClaw 使用 TypeScript ESM(ECMAScript Modules),导入路径带 .js 后缀:
// 静态导入 — 模块在文件加载时就被引入
import { resolveAgentRoute } from "../routing/resolve-route.js";
// 动态导入 — 模块在运行时按需加载
const mod = await import("./send-runtime/telegram.js");
为什么带 .js? TypeScript 编译后输出的是 .js 文件,ESM 要求导入路径指向实际文件。
为什么要动态导入? OpenClaw 支持几十个渠道,如果启动时全部加载会很慢。动态导入允许"用到时才加载"。
懒加载工具函数
src/shared/lazy-runtime.ts 提供了通用的懒加载封装:
// 真实代码
export function createLazyRuntimeSurface<TModule, TSurface>(
importer: () => Promise<TModule>, // 动态 import 函数
select: (module: TModule) => TSurface, // 从模块中取出需要的部分
): () => Promise<TSurface> {
let cached: Promise<TSurface> | null = null;
return () => {
cached ??= importer().then(select); // ??= 表示"如果为 null 才赋值"
return cached;
};
}
它的使用场景在 src/cli/deps.ts:
// 每个渠道的 sender 都是懒加载的
export function createDefaultDeps(): CliDeps {
return {
whatsapp: createLazySender(
"whatsapp",
() => import("./send-runtime/whatsapp.js"),
),
telegram: createLazySender(
"telegram",
() => import("./send-runtime/telegram.js"),
),
// ... 其他渠道
};
}
规则:同一个模块不能同时用 import ... from 和 await import() 引入(会导致构建警告 [INEFFECTIVE_DYNAMIC_IMPORT])。如果需要懒加载,通过一个 .runtime.ts 边界文件隔离。
2. 依赖注入:纯对象传参,没有 DI 容器
OpenClaw 不使用装饰器、不使用 DI 容器(如 InversifyJS)。依赖注入通过最简单的方式实现:把依赖打包成一个对象,通过函数参数传进去。
核心示例:GatewayRequestContext
Gateway 的所有方法处理器共享一个 context 对象,它携带了服务器的全部运行时依赖:
// src/gateway/server-methods/types.ts
export type GatewayRequestContext = {
deps: ReturnType<typeof createDefaultDeps>; // 渠道发送器
cron: CronService; // 定时任务服务
broadcast: GatewayBroadcastFn; // WebSocket 广播
logGateway: SubsystemLogger; // 日志器
refreshHealthSnapshot: () => Promise<HealthSummary>; // 健康检查
// ... 更多字段
};
// 每个处理器通过参数拿到这个 context
export type GatewayRequestHandler = (opts: {
req: RequestFrame;
client: GatewayClient | null;
respond: RespondFn;
context: GatewayRequestContext; // 所有依赖都在这里
}) => Promise<void> | void;
好处:
- 类型安全 — 编辑器可以自动补全所有可用的依赖
- 测试友好 — 测试时传入 mock 对象即可
- 无魔法 — 不需要理解 DI 容器的注册/解析机制
同样的模式出现在各处
// 路由解析函数 — 通过参数接收所有需要的信息
export function resolveAgentRoute(input: {
cfg: OpenClawConfig;
channel: string;
accountId?: string | null;
peer?: RoutePeer | null;
// ...
}): ResolvedAgentRoute { ... }
// 自动回复分发 — 通过参数传入依赖
export async function dispatchInboundMessage(params: {
ctx: FinalizedMsgContext;
cfg: OpenClawConfig;
dispatcher: ReplyDispatcher;
replyResolver?: typeof import("./reply.js").getReplyFromConfig;
}): Promise<DispatchInboundResult> { ... }
3. 适配器组合模式(Composition over Inheritance)
OpenClaw 不使用类继承来实现渠道差异化,而是用可选适配器组合。
ChannelPlugin 接口
每个渠道(Telegram、Discord 等)导出一个对象,满足 ChannelPlugin 类型:
// src/channels/plugins/types.plugin.ts(简化)
export type ChannelPlugin = {
id: ChannelId; // "telegram" | "discord" | ...
meta: ChannelMeta; // 名称、图标等展示信息
capabilities: ChannelCapabilities; // 能力声明(支持图片?支持线程?)
// 以下全是可选适配器 — 实现哪些就有哪些能力
config?: ChannelConfigAdapter; // 配置读写
setup?: ChannelSetupAdapter; // 初始化向导
pairing?: ChannelPairingAdapter; // 设备配对
security?: ChannelSecurityAdapter; // 访问控制
outbound?: ChannelOutboundAdapter; // 发送消息
status?: ChannelStatusAdapter; // 状态检查
gateway?: ChannelGatewayAdapter; // 网关生命周期
messaging?: ChannelMessagingAdapter; // 接收消息
streaming?: ChannelStreamingAdapter; // 流式输出
threading?: ChannelThreadingAdapter; // 线程管理
// ... 还有十几个可选适配器
};
每个适配器是一组方法
// src/channels/plugins/types.adapters.ts(简化)
export type ChannelConfigAdapter<ResolvedAccount> = {
listAccountIds: (cfg: OpenClawConfig) => string[];
resolveAccount: (cfg: OpenClawConfig, accountId?: string) => ResolvedAccount;
isEnabled?: (account: ResolvedAccount) => boolean;
isConfigured?: (account: ResolvedAccount) => boolean | Promise<boolean>;
// ... 更多可选方法
};
理解要点:
- 不是
class TelegramChannel extends BaseChannel,而是const telegram: ChannelPlugin = { ... } - 每个适配器都是可选的 — 一个渠道不支持线程就不提供
threading适配器 - 适配器的方法也多是可选的 — 渐进式实现
4. 事件系统
OpenClaw 有一个轻量级事件总线,用于 Agent 运行时的事件通知。
// src/infra/agent-events.ts
// 事件类型
export type AgentEventStream = "lifecycle" | "tool" | "assistant" | "error";
export type AgentEventPayload = {
runId: string; // 本次运行的唯一 ID
seq: number; // 序列号
stream: AgentEventStream; // 事件流类型
ts: number; // 时间戳
data: Record<string, unknown>; // 事件数据
sessionKey?: string; // 关联的会话
};
// 发布事件
export function emitAgentEvent(event: Omit<AgentEventPayload, "seq" | "ts">) {
const enriched = { ...event, seq: nextSeq(), ts: Date.now() };
for (const listener of listeners) {
listener(enriched);
}
}
// 订阅事件(返回取消函数)
export function onAgentEvent(listener: (evt: AgentEventPayload) => void) {
listeners.add(listener);
return () => listeners.delete(listener);
}
用途:
- WebSocket 实时推送(Web UI 看到"正在思考…")
- 日志记录
- 工具执行监控
5. 方法处理器注册
Gateway 的 RPC 方法用对象字面量注册,不是路由框架:
// src/gateway/server-methods/chat.ts
export const chatHandlers: GatewayRequestHandlers = {
send: async (opts) => { /* 发送消息 */ },
history: async (opts) => { /* 获取历史 */ },
abort: async (opts) => { /* 取消生成 */ },
};
// 类似的还有 configHandlers, channelHandlers, skillsHandlers 等
// 它们在 server.impl.ts 中被合并成一个大的 handlers map
客户端通过 WebSocket 发送 { method: "chat.send", params: {...} } 这样的 JSON-RPC 请求,Gateway 根据 method 名查找对应的处理器。
6. 配置类型系统
配置定义分散在多个文件中,通过 src/config/types.ts 统一导出:
// src/config/types.ts — 只做 re-export
export * from "./types.base.js";
export * from "./types.agents.js";
export * from "./types.channels.js";
export * from "./types.auth.js";
// ... 20+ 个模块
大量使用字符串字面量联合类型:
// src/config/types.base.ts
export type DmPolicy = "pairing" | "allowlist" | "open" | "disabled";
export type GroupPolicy = "open" | "disabled" | "allowlist";
export type SessionScope = "per-sender" | "global";
export type DmScope = "main" | "per-peer" | "per-channel-peer" | "per-account-channel-peer";
配置用 JSON5 格式(支持注释和尾逗号),运行时用 Zod schema 校验。
7. Session Key:数据驱动的会话标识
会话通过 Session Key 唯一标识,格式为:
agent:<agentId>:<channel>:<accountId>:<scope>:<peerId>
示例:agent:main:telegram:bot1:direct:user123
// src/routing/session-key.ts
export function buildAgentPeerSessionKey(params: {
agentId: string;
channel: string;
accountId?: string | null;
peerKind: string;
peerId: string | null;
dmScope?: DmScope;
}): string { ... }
// 从 session key 反解 agent ID
export function resolveAgentIdFromSessionKey(sessionKey: string): string { ... }
8. 异步资源管理
OpenClaw 用 try-finally 模式确保异步资源(如 dispatcher)被正确释放:
// src/auto-reply/dispatch.ts
export async function withReplyDispatcher<T>(params: {
dispatcher: ReplyDispatcher;
run: () => Promise<T>;
onSettled?: () => void | Promise<void>;
}): Promise<T> {
try {
return await params.run();
} finally {
params.dispatcher.markComplete();
try {
await params.dispatcher.waitForIdle();
} finally {
await params.onSettled?.();
}
}
}
这个模式在整个代码库中反复出现:wrap 一个异步操作,确保不管成功还是失败都会执行清理。
9. 插件注册 API
插件通过 openclaw.plugin.json 声明元数据,通过注册函数暴露能力:
// src/plugins/types.ts(简化)
export type OpenClawPluginApi = {
id: string;
name: string;
config: OpenClawConfig;
runtime: PluginRuntime;
logger: PluginLogger;
// 注册各种能力
registerTool: (tool, opts?) => void;
registerChannel: (channel: ChannelPlugin) => void;
registerProvider: (provider: ProviderPlugin) => void;
registerHook: (events, handler) => void;
registerHttpRoute: (params) => void;
registerGatewayMethod: (method, handler) => void;
registerCli: (registrar) => void;
registerService: (service) => void;
// ... 更多
};
插件存放在 extensions/ 目录,每个插件是独立的 npm 包。
10. 路由解析与 matchedBy
路由决策的结果带有 matchedBy 字段,记录哪条规则匹配了:
// src/routing/resolve-route.ts
export type ResolvedAgentRoute = {
agentId: string;
sessionKey: string;
matchedBy:
| "binding.peer" // 直接对话绑定
| "binding.peer.parent" // 父级对话继承
| "binding.guild+roles" // Discord 服务器 + 角色
| "binding.guild" // Discord 服务器
| "binding.team" // Slack/Teams 团队
| "binding.account" // 渠道账号级别
| "binding.channel" // 渠道级别
| "default"; // 默认 agent
};
优先级从上到下递减。这个设计让路由决策可追踪——出问题时看 matchedBy 就知道是哪条规则生效了。
11. 统一消息上下文(MsgContext)与类型约束(Type Narrowing)
各个异构渠道(如 WhatsApp、Telegram 等)输入的消息,在框架内部都被标准化处理为统一的领域模型结构 MsgContext,用于屏蔽不同平台的 API 差异:
// src/auto-reply/templating.ts
export type MsgContext = {
Body?: string;
BodyForAgent?: string;
InboundHistory?: Array<{ sender: string; body: string; timestamp?: number }>;
From?: string;
SessionKey?: string;
AccountId?: string;
// ... 其他属性(涵盖所有可能的渠道特殊字段)
};
系统在路由和拦截(防抖、权限鉴定)之后,会将其转为更严格的 FinalizedMsgContext。这在代码中体现了优秀的 TypeScript 类型收窄(Type Narrowing) 工程实践:
export type FinalizedMsgContext = Omit<MsgContext, "CommandAuthorized"> & {
/**
* Always set by finalizeInboundContext().
* Default-deny: missing/undefined becomes false.
*/
CommandAuthorized: boolean; // 漏斗型数据加工:从可选状态变为了严格的必填属性
};
这种强类型实践能够让核心调度器(如 Dispatcher 和 Agent)基于最稳固的数据契约继续流转,从而避免潜在的出错。
小结
| 模式 | 关键文件 | 一句话 |
|---|---|---|
| 懒加载 | src/shared/lazy-runtime.ts |
??= 缓存 + 动态 import |
| DI 纯对象 | src/gateway/server-methods/types.ts |
依赖打成 struct 传参 |
| 适配器组合 | src/channels/plugins/types.plugin.ts |
可选接口组合代替继承 |
| 事件总线 | src/infra/agent-events.ts |
emit/on 订阅模式 |
| 方法注册 | src/gateway/server-methods/*.ts |
对象字面量做 handler map |
| 配置类型 | src/config/types.ts |
分模块定义 + 统一 re-export |
| Session Key | src/routing/session-key.ts |
agent:id:channel:... 格式 |
| 异步管理 | src/auto-reply/dispatch.ts |
try-finally wrap 模式 |
| 插件 API | src/plugins/types.ts |
注册函数暴露能力 |
| 路由追踪 | src/routing/resolve-route.ts |
matchedBy 标记匹配规则 |
| 统一消息上下文 | src/auto-reply/templating.ts |
MsgContext 抹平平台差异,通过 Omit 缩小类型 |
掌握以上 11 个模式,你就能顺畅阅读 OpenClaw 代码库的绝大部分文件了。
更多推荐

所有评论(0)