OpenClaw08_监听器

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秒)

  1. 打开 TypeScript Playground
  2. 把上面代码全选复制进去
  3. Ctrl+Enter 或点击 Run 按钮
  4. 看右侧 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
相关推荐
Tianwen_Burning3 小时前
openclaw安装流程 +火山方舟
openclaw
熊猫钓鱼>_>3 小时前
从“流程固化“到“意图驱动“:大模型调智能体调Skill架构深度解析
ai·架构·大模型·llm·agent·skill·openclaw
xyz_CDragon4 小时前
OpenClaw Skills 完全指南:ClawHub 安装、安全避坑与自定义开发(2026)
人工智能·python·ai·skill·openclaw·clawhub
南师大蒜阿熏呀4 小时前
openclaw 多智能体协同简易版案例实战
ai·openclaw
AI航向标5 小时前
OpenClaw 完整本地部署安装(接入飞书)
人工智能·飞书·openclaw
zandy10115 小时前
【全新 3.0版本】openclaw github installation guide
github·openclaw·installation
AI航向标5 小时前
Openclaw一键本地部署接入豆包
人工智能·openclaw
七夜zippoe7 小时前
OpenClaw 技能开发实战:从零到一
运维·服务器·网络·openclaw·技能开发
竹之却7 小时前
【Agent-阿程】openclaw v2026.4.9更新内容介绍
开发语言·php·openclaw·openclaw 更新
陈一帆AI8 小时前
OpenClaw 4.5 重磅发布:安全硬化 + 生态重构,AI 助手迈入信任时代!
openclaw·龙虾·养虾