OpenClaw08_监听器
针对中文版本openClaw进行源码阅读,当前项目针对【TypeScript中监听器】逻辑进行解读
文章目录

1-参考地址
2-知识整理
- 1)OpenClaw源码-TypeScript中监听器-源码部分(心跳极值)
- 2)OpenClaw源码-TypeScript中监听器-简化版本

3-动手实操
1-TypeScript中监听器-源码部分
typescript
// 心跳指示器类型定义,用于 UI 层面展示不同的状态反馈
// ok: 正常,alert: 告警/发送中,error: 错误
export type HeartbeatIndicatorType = "ok" | "alert" | "error";
// 心跳事件数据负载结构体,封装了心跳请求与响应的核心信息
export type HeartbeatEventPayload = {
ts: number;
status: "sent" | "ok-empty" | "ok-token" | "skipped" | "failed";
to?: string;
preview?: string;
durationMs?: number;
hasMedia?: boolean;
reason?: string;
/** 心跳检测经由的通信通道名称。 */
channel?: string;
/** 标记消息是否被静默处理(未触发 ShowOk 提示)。 */
silent?: boolean;
/** 对应 UI 组件展示的指示器类型。 */
indicatorType?: HeartbeatIndicatorType;
/** 在多账户场景下区分归属的账户 ID。 */
accountId?: string;
};
// 解析心跳事件状态码,将其转换为对应的 UI 显示指示器类型
// 参数:status - 心跳事件的具体状态描述
// 返回值:对应的 IndicatorType 枚举值,如果是 skipped 则返回 undefined
export function resolveIndicatorType(
status: HeartbeatEventPayload["status"],
): HeartbeatIndicatorType | undefined {
switch (status) {
// 空包成功或 Token 验证成功后,视为正常状态
case "ok-empty":
case "ok-token":
return "ok";
// 发送中通常作为告警提示展示,告知用户正在处理
case "sent":
return "alert";
// 连接或请求失败时,标识为错误状态
case "failed":
return "error";
// 跳过的步骤不产生具体的 UI 指示
case "skipped":
return undefined;
}
}
// 全局变量:缓存最近一次发出的心跳事件数据,用于后续查询
let lastHeartbeat: HeartbeatEventPayload | null = null;
// 全局变量:维护一个集合,存储所有监听心跳事件的回调函数
const listeners = new Set<(evt: HeartbeatEventPayload) => void>();
// 发射心跳事件逻辑,包含自动填充时间和分发通知
// 参数:evt - 待发送的心跳基础数据对象(不含时间戳)
// 业务逻辑:生成完整时间戳,更新本地缓存,遍历并调用所有订阅者
export function emitHeartbeatEvent(evt: Omit<HeartbeatEventPayload, "ts">) {
// 将当前系统时间与传入事件合并,形成完整的事件负载
const enriched: HeartbeatEventPayload = { ts: Date.now(), ...evt };
// 保存最新的心跳快照到全局上下文
lastHeartbeat = enriched;
// 依次通知所有已注册的监听器
for (const listener of listeners) {
try {
listener(enriched);
} catch {
// 捕获单个监听器可能抛出的异常,确保不影响其他监听器的执行
/* ignore */
}
}
}
// 注册心跳事件监听器
// 参数:listener - 当事件发生时将被执行的回调函数
// 返回值:返回一个取消订阅函数,调用它可从监听列表中移除当前回调
export function onHeartbeatEvent(listener: (evt: HeartbeatEventPayload) => void): () => void {
listeners.add(listener);
// 返回清理函数,实现类似 React useEffect 的 cleanup 模式
return () => listeners.delete(listener);
}
// 获取系统内部记录的最后一个心跳事件快照
// 返回值:若曾有过心跳发射则返回 Payload 对象,否则返回 null
export function getLastHeartbeatEvent(): HeartbeatEventPayload | null {
return lastHeartbeat;
}
2-TypeScript中监听器-简化版本
typescript
// ============================================
// 极简事件总线 - 感受 TS 函数即值的语法
// ============================================
// 1. 【核心】定义一个"存函数的盒子"
// 注意:这里直接存的是 (n: number) => void 这个函数类型
const callbacks = new Set<(n: number) => void>();
// 2. 【订阅】把函数塞进盒子
function subscribe(fn: (n: number) => void): () => void {
callbacks.add(fn);
console.log(`📥 订阅成功,当前共 ${callbacks.size} 个监听者`);
// 返回"取消订阅"函数(也是一个函数!)
return () => {
callbacks.delete(fn);
console.log(`📤 取消订阅,剩余 ${callbacks.size} 个`);
};
}
// 3. 【广播】遍历盒子,挨个调用存的函数
function broadcast(value: number): void {
console.log(`\n📢 广播数字: ${value}`);
for (const fn of callbacks) {
fn(value); // ← 这就是 Java 做不到的直接"括号调用"!
}
}
// ============================================
// 测试代码
// ============================================
console.log("========== 测试开始 ==========\n");
// 测试1:创建3个不同的函数(像创建3个变量一样简单)
const listenerA = (n: number) => console.log(` [A] 收到: ${n},平方是 ${n * n}`);
const listenerB = (n: number) => console.log(` [B] 收到: ${n},翻倍是 ${n * 2}`);
const listenerC = (n: number) => console.log(` [C] 收到: ${n},我是来捣乱的 💥`);
// 测试2:订阅(把函数"值"存进 Set)
const unsubscribeA = subscribe(listenerA);
const unsubscribeB = subscribe(listenerB);
subscribe(listenerC); // 这个不保存取消函数,就永远无法移除了 😈
// 测试3:广播
broadcast(10);
// 测试4:取消 A 的订阅,再广播
console.log("\n--- 取消 A 的订阅 ---");
unsubscribeA();
broadcast(5);
// 测试5:取消 B 的订阅,再广播(只剩 C 了)
console.log("\n--- 取消 B 的订阅 ---");
unsubscribeB();
broadcast(99);
console.log("\n========== 测试结束 ==========");
- 日志打印
bash
[LOG]: "========== 测试开始 ==========
"
[LOG]: "📥 订阅成功,当前共 1 个监听者"
[LOG]: "📥 订阅成功,当前共 2 个监听者"
[LOG]: "📥 订阅成功,当前共 3 个监听者"
[LOG]: "
📢 广播数字: 10"
[LOG]: " [A] 收到: 10,平方是 100"
[LOG]: " [B] 收到: 10,翻倍是 20"
[LOG]: " [C] 收到: 10,我是来捣乱的 💥"
[LOG]: "
--- 取消 A 的订阅 ---"
[LOG]: "📤 取消订阅,剩余 2 个"
[LOG]: "
📢 广播数字: 5"
[LOG]: " [B] 收到: 5,翻倍是 10"
[LOG]: " [C] 收到: 5,我是来捣乱的 💥"
[LOG]: "
--- 取消 B 的订阅 ---"
[LOG]: "📤 取消订阅,剩余 1 个"
[LOG]: "
📢 广播数字: 99"
[LOG]: " [C] 收到: 99,我是来捣乱的 💥"
[LOG]: "
========== 测试结束 =========="
[LOG]: "========== 测试开始 =========="
[LOG]: "📥 订阅成功,当前共 1 个监听者"
[LOG]: "📥 订阅成功,当前共 2 个监听者"
[LOG]: "📥 订阅成功,当前共 3 个监听者"
[LOG]: "
📢 广播数字: 10"
[LOG]: " [A] 收到: 10,平方是 100"
[LOG]: " [B] 收到: 10,翻倍是 20"
[LOG]: " [C] 收到: 10,我是来捣乱的 💥"
[LOG]: "--- 取消 A 的订阅 ---"
[LOG]: "📤 取消订阅,剩余 2 个"
[LOG]: "
📢 广播数字: 5"
[LOG]: " [B] 收到: 5,翻倍是 10"
[LOG]: " [C] 收到: 5,我是来捣乱的 💥"
[LOG]: "--- 取消 B 的订阅 ---"
[LOG]: "📤 取消订阅,剩余 1 个"
[LOG]: "
📢 广播数字: 99"
[LOG]: " [C] 收到: 99,我是来捣乱的 💥"
[LOG]: "========== 测试结束 =========="
[LOG]: "========== 测试开始 =========="
[LOG]: "📥 订阅成功,当前共 1 个监听者"
[LOG]: "📥 订阅成功,当前共 2 个监听者"
[LOG]: "📥 订阅成功,当前共 3 个监听者"
[LOG]: "
📢 广播数字: 10"
[LOG]: " [A] 收到: 10,平方是 100"
[LOG]: " [B] 收到: 10,翻倍是 20"
[LOG]: " [C] 收到: 10,我是来捣乱的 💥"
[LOG]: "
--- 取消 A 的订阅 ---"
[LOG]: "📤 取消订阅,剩余 2 个"
[LOG]: "
📢 广播数字: 5"
[LOG]: " [B] 收到: 5,翻倍是 10"
[LOG]: " [C] 收到: 5,我是来捣乱的 💥"
[LOG]: "
--- 取消 B 的订阅 ---"
[LOG]: "📤 取消订阅,剩余 1 个"
[LOG]: "
📢 广播数字: 99"
[LOG]: " [C] 收到: 99,我是来捣乱的 💥"
[LOG]: "
========== 测试结束 =========="
[LOG]: "========== 测试开始 =========="
[LOG]: "📥 订阅成功,当前共 1 个监听者"
[LOG]: "📥 订阅成功,当前共 2 个监听者"
[LOG]: "📥 订阅成功,当前共 3 个监听者"
[LOG]: "
📢 广播数字: 10"
[LOG]: " [A] 收到: 10,平方是 100"
[LOG]: " [B] 收到: 10,翻倍是 20"
[LOG]: " [C] 收到: 10,我是来捣乱的 💥"
[LOG]: "
--- 取消 A 的订阅 ---"
[LOG]: "📤 取消订阅,剩余 2 个"
[LOG]: "
📢 广播数字: 5"
[LOG]: " [B] 收到: 5,翻倍是 10"
[LOG]: " [C] 收到: 5,我是来捣乱的 💥"
[LOG]: "
--- 取消 B 的订阅 ---"
[LOG]: "📤 取消订阅,剩余 1 个"
[LOG]: "
📢 广播数字: 99"
[LOG]: " [C] 收到: 99,我是来捣乱的 💥"
[LOG]: "
========== 测试结束 =========="
[LOG]: "========== 测试开始 =========="
[LOG]: "📥 订阅成功,当前共 1 个监听者"
[LOG]: "📥 订阅成功,当前共 2 个监听者"
[LOG]: "📥 订阅成功,当前共 3 个监听者"
[LOG]: "
📢 广播数字: 10"
[LOG]: " [A] 收到: 10,平方是 100"
[LOG]: " [B] 收到: 10,翻倍是 20"
[LOG]: " [C] 收到: 10,我是来捣乱的 💥"
[LOG]: "
--- 取消 A 的订阅 ---"
[LOG]: "📤 取消订阅,剩余 2 个"
[LOG]: "
📢 广播数字: 5"
[LOG]: " [B] 收到: 5,翻倍是 10"
[LOG]: " [C] 收到: 5,我是来捣乱的 💥"
[LOG]: "
--- 取消 B 的订阅 ---"
[LOG]: "📤 取消订阅,剩余 1 个"
[LOG]: "
📢 广播数字: 99"
[LOG]: " [C] 收到: 99,我是来捣乱的 💥"
[LOG]: "
========== 测试结束 =========="
[LOG]: "========== 测试开始 =========="
[LOG]: "📥 订阅成功,当前共 1 个监听者"
[LOG]: "📥 订阅成功,当前共 2 个监听者"
[LOG]: "📥 订阅成功,当前共 3 个监听者"
[LOG]: "📢 广播数字: 10"
[LOG]: " [A] 收到: 10,平方是 100"
[LOG]: " [B] 收到: 10,翻倍是 20"
[LOG]: " [C] 收到: 10,我是来捣乱的 💥"
[LOG]: "
--- 取消 A 的订阅 ---"
[LOG]: "📤 取消订阅,剩余 2 个"
[LOG]: "📢 广播数字: 5"
[LOG]: " [B] 收到: 5,翻倍是 10"
[LOG]: " [C] 收到: 5,我是来捣乱的 💥"
[LOG]: "
--- 取消 B 的订阅 ---"
[LOG]: "📤 取消订阅,剩余 1 个"
[LOG]: "📢 广播数字: 99"
[LOG]: " [C] 收到: 99,我是来捣乱的 💥"
[LOG]: "
========== 测试结束 =========="
如何测试
方式一:在线运行(最快,30秒)
- 打开 TypeScript Playground
- 把上面代码全选复制进去
- 按 Ctrl+Enter 或点击 Run 按钮
- 看右侧 Console 输出
方式二:本地运行
bash
# 1. 创建文件
mkdir ts-demo && cd ts-demo
cat > demo.ts << 'EOF'
[把上面代码粘贴到这里]
EOF
# 2. 初始化并运行
npm init -y
npm install ts-node typescript --save-dev
npx ts-node demo.ts