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
相关推荐
不惑_1 天前
腾讯云WorkBuddy实战, 全场景智能体工作搭子,这只龙虾真能帮你干活吗
人工智能·云计算·腾讯云·openclaw
华科大胡子1 天前
用OpenCLAW重写CUDA内核
openclaw
2501_937154931 天前
酷秒神马 9.0(2026 新版)内核优化实测
源码·源代码管理·机顶盒
起个破名想半天了1 天前
OpenClaw保姆级配置教程(适用于Mac)
mac·openclaw·配置教程
七夜zippoe1 天前
OpenClaw Nodes 设备管理深度解析:AI Agent的跨设备协作能力
人工智能·ai·agent·openclaw·nodes
AC赳赳老秦2 天前
OpenClaw+MySQL 深度应用:自动生成建表语句、索引优化建议与数据迁移脚本
开发语言·数据库·人工智能·python·mysql·算法·openclaw
无心水2 天前
【Harness:落地实战】24、Harness CI/CD+GitOps深度实战:智能交付与渐进发布——企业级云原生DevOps全解析
人工智能·ci/cd·云原生·openclaw·harness·hermes·honcho
hhzz2 天前
OpenClaw中文案例精选:多智能体内容工厂
语言模型·多智能体·openclaw
rrokoko2 天前
“计算器” VB.NET源码
.net·源码·vb.net·计算器·计算器源码
rrokoko2 天前
“扫雷”游戏 VB.NET源码
游戏·.net·源码·vb.net