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
相关推荐
zhangyifang_0092 小时前
openclaw常用命令
openclaw
huazi-J2 小时前
Datawhale openclaw 课程 task2:clawX本地openclaw使用skill
llm·datawhale·openclaw·龙虾
gallonyin3 小时前
【企业级龙虾】LLM Gateway 工程化落地:配置中心、429故障转移与统计持久化实战
gateway·openclaw
Web极客码3 小时前
OpenAI GPT-5.2-Codex (High) vs. Claude Opus 4.5 vs. Gemini 3 Pro:真实场景编程大横评
ai编程·claude code·claude skill·openclaw
API开发3 小时前
一个MCP操作所有的数据库
数据库·api·api接口·apisql·mcp·mcpserver·openclaw
supersolon3 小时前
OpenClaw安装碰到的一些问题和解决方法
linux·运维·ai·openclaw·龙虾
海兰4 小时前
【安全】OpenClaw 安全配置基础
安全·agent·openclaw
乱世刀疤4 小时前
openclaw常用指令
人工智能·openclaw
johnny2335 小时前
OpenClaw相关开源项目:IronClaw、MoltWorker、LobsterAI、TinyClaw/TinyAGI、HiClaw、QQBot
开源·openclaw