OpenClaw Telegram 插件可靠性工程指南:从持久化入站到流式出站的维护者契约
2026/9/15 17:09:44 网站建设 项目流程

OpenClaw Telegram 插件可靠性工程指南:从持久化入站到流式出站的维护者契约

【免费下载链接】openclawThe AI that really does things. Any OS. Any Platform. The lobster way. 🦞项目地址: https://gitcode.com/GitHub_Trending/cl/openclaw

本指南围绕 extensions/telegram/AGENTS.md 展开,系统解析 OpenClaw 内置 Telegram 通道插件在消息可靠性、流式回答、上下文授权与交互面设计上的维护者决策与评审不变量。读完你将掌握:核心持久的 ingress drain 如何与 Telegram 特有的轮询/Webhook 传输协同、为什么“先落盘再应答”是防丢消息的根基、以及任何改动如何通过 crash-window 与真实 Telegram 探测验证。

extensions/telegram/AGENTS.md是仓库维护者为 Telegram 插件划定的“评审绑定不变量”——它不是实现细节的随记,而是有意为之的架构决策。文档基于 Telegram Bot API 10.3 验证(2026-08-24),所有改动都应在此框架内进行。


一、文档定位:先理解插件边界

在深入 Telegram 具体规则前,必须同时阅读 extensions/AGENTS.md 中的插件边界规则。其核心结论是:extensions/下的所有内置插件都遵循与第三方插件相同的边界约束——

  • 生产代码只允许从openclaw/plugin-sdk/*与插件自身的局部 barrel(如api.tsruntime-api.ts)导入;
  • 禁止直接导入src/**src/channels/**或其它扩展的src/**
  • 插件运行时依赖归属插件自身package.json,运行时不会自动安装依赖,安装/更新/doctor 才是修复点;
  • 插件可用性来自 manifest 所有权 + 定向激活,不允许依赖 import 时的全局注册副作用。

从文件结构看,Telegram 插件是仓库中代码量最大的通道插件之一:extensions/telegram/src 下按bot/(grammY 中间件与派发)、miniapp/(Telegram Mini App)、telegram-ingress-*(持久化入站)、bot-message-dispatch.*(出站派发)等维度组织,并配套大量*.test.ts单测与*.runtime.test.ts运行时集成测试。这与本文档“行为改动需要真实证明、可靠性改动需要崩溃窗口证明”的评审标准互为表里。


二、核心 drain 契约:Telegram 不重复实现的东西

2.1 契约归属

文档明确指出:入站持久化 drain 的核心契约不属于 Telegram 插件,而是核心仓(core)的资产,归属 src/channels/message/ingress-drain.ts(以及 claim-owner、retry-policy 两个配套模块)。Telegram 插件只是在核心 drain 之上做传输适配。

对应的证据文件:

  • src/channels/message/ingress-drain.test.ts
  • src/channels/message/ingress-claim-owner.test.ts
  • src/channels/message/ingress-retry-policy.test.ts

2.2 六条核心契约逐条解读

结合 ingress-drain.ts 源码,可以印证每一条契约的落地方式:

1. 完成行通过complete()墓碑化,绝不delete()

代码中completeClaimWithRetry负责写墓碑(tombstone),createIngressWriter提供的三个原语是completeClaimWithRetry/releaseClaim/failClaim。墓碑化保证了崩溃恢复时能看到“该事件已处理”的事实,而物理删除则无法区分“从未处理”与“已处理但丢了记录”。

2. 在“回合接管(turn adoption)”时 complete,而不是在 settle 时

源码生命周期中,onAdopted回调里执行state.phase = "adopted"; clearStallTimer(state); await completeClaimWithRetry(...)(ingress-drain.ts)。也就是说:一旦事件被某个回合正式收养,立即落墓碑并释放 lane,而不是等整个回合结束。deferral(延迟交接)期间 claim 仍然被持有,watchdog 保持武装;watchdog 超时走共享的重试处置。

3. 统一的重试策略:attempt 下限 + age 门槛(默认 8 次 / 24 小时)

src/channels/message/ingress-retry-policy.ts 定义了四个默认值:

参数默认值含义
maxAttempts8最大尝试次数(attempt 下限)
deadLetterMinAgeMs24h死信最小年龄门槛
baseMs1 000指数退避初始延迟(factor 2,无抖动)
maxMs3 * 60_000退避封顶

关键语义在shouldDeadLetterRetryableIngressEvent死信必须同时满足 attempt ≥ maxAttempts 且 now - receivedAt ≥ 24h。超限但未到龄的事件会继续以封顶延迟重试,直到年龄满足——这是为了避免刚收到的事件因瞬时故障被过早丢进死信。

4. 派发/延迟期间以claimLeaseMs / 3为周期做 claim 续租心跳

armClaimRefreshintervalMs = Math.max(1, Math.floor(claimLeaseMs / 3))(ingress-drain.ts),心跳一直持续到墓碑提交为止(包含 complete 重试的 wedge 窗口)。若refreshClaim返回 false,说明 claim token 已被其它 owner 接管,则触发markLeaseReclaimed的 guillotine 封闭:后续onAdopted会抛IngressAdoptionLostError,且不允许再 release/fail 他人持有的 claim。

5. 瞬时失败绝不静默 complete——通过 disposition 走 release/fail

applyFailureDisposition是唯一的失败出口:GatewayDrainingError直接 release 且不消耗失败预算;否则调用resolveIngressFailureDisposition,得到fail(死信)或release(保留待重试)两种处置。

6. supersede 只作用于收养前(pre-adoption)

supersedeActiveIfNeeded只对未收养的工作生效;收养后中断属于核心的 reply-run registry / queue interrupt 职责,Telegram 侧不干预。

2.3 崩溃窗口语义:为什么“complete at adoption”是安全的

源码注释点明了顺序敏感点:“adoption 的 tombstone 重试 wedge 期间,dispatch 副作用已经发生,此时不能 release claim(否则重放风险)”。因此代码在墓碑重试期间把 phase 先置为adopted,即使 tombstone 写入失败也保持 claim 持有,避免同一事件被双发。这正是文档“Never silently complete on transient failure”背后的工程动机。


三、Telegram 自有的传输与通道策略

核心 drain 是通用骨架,Telegram 插件在其上落实传输层策略,主要证据集中在 extensions/telegram/src/telegram-ingress-spool.ts、telegram-ingress-worker.ts、telegram-ingress-drain-factory.ts。

3.1 两种传输都必须“先持久化,后应答”(Durable-before-ack)

  • 轮询(Polling):ingress worker 只有在父进程把 spool 提交(writeTelegramSpooledUpdate)并确认后,才推进本地 offset。从 telegram-ingress-worker.ts 的消息协议可以看到 parent ↔ worker 之间存在显式的spool-ack命令(ok: true/false),worker 仅在ok: true后才继续推进 offset。
  • Webhook:只有 spool 写入成功后才返回 HTTP 200;写入失败返回非 200,这本身就是 Telegram 的 redelivery 契约。

为什么这是防丢消息的根基:Telegram 的 getUpdates 以 offset 推进确认消息已消费,Webhook 以 200 确认已接收。如果先应答后落盘,进程在应答与落盘之间崩溃,消息就永久丢失且 Telegram 不再重投。先落盘后应答把“至少一次投递”建立在本地持久化之上。

3.2 update_id ↔ 事件 ID 编码与 lane 推导

telegram-ingress-spool.ts提供resolveTelegramUpdateId(必须是非负安全整数)与telegramQueueEventIdupdate_id用 0 左填充到 16 位作为事件 ID),保证事件 ID 稳定且可按顺序排序。lane 推导走getTelegramSequentialKey(sequential-key),形成 per-chat/per-topic 的串行化通道;这部分逻辑必须留在 spool/lane 推导层,不允许在别处另起炉灶。

3.3 轮询与 Webhook 共用同一套 drain

两种传输最终都走createTelegramTransportIngressDrain(...).drainOnce(),即“先入队,再泵一次 drain”,禁止任何私有 claim 循环telegram-ingress-drain-factory.ts的注释点明设计意图:“一个 monitor 同时服务 polling + webhook:channel 侧追加、共享 claim → dispatch 带 turnAdoptionLifecycle → 在收养时 complete”。回调(callback_query)的应答在onDurableAdmission(落盘提交之后、claim 之前)执行,避免 callback 应答抹掉 Telegram 的重投路径。

3.4 停滞超时:OPENCLAW_TELEGRAM_SPOOLED_HANDLER_TIMEOUT_MS

环境变量OPENCLAW_TELEGRAM_SPOOLED_HANDLER_TIMEOUT_MS映射到adoptionStallTimeoutMs,默认5 分钟DEFAULT_INGRESS_ADOPTION_STALL_MS = 5 * 60 * 1000)。解析逻辑在 telegram-ingress-drain.ts:优先取显式配置,其次取环境变量,最后回退默认值,且经过clampPositiveTimerTimeoutMs校验。停滞 watchdog 在 claim→adoption 之间触发,超时后走共享重试处置而非直接静默完成。

3.5 不可重试分类器与 supersede 谓词

  • 不可重试分类器telegram-ingress-non-retryable.ts负责把“缺 harness、dispatch-dedupe 回滚”等场景判为不可重试,直接 fail(死信)而非 release。
  • supersede 谓词telegram-ingress-supersede.ts规定——只有文本消息、看起来已授权的显式命令(以及待处理的 ambient room_event)可以 supersede 未收养的同 lane 工作;普通消息永远不能 supersede。room_event(如“进入会话”的系统事件)共享 sequential lane,因此后续用户回合可以在收养前 supersede 它;已收养的用户回合则绝不被触碰(核心 drain 的 supersede 仅限收养前)。

3.6 禁止每消息全量存储写

文档明确:热路径上的 SQLite 写入必须是**逐条目(per-entry)**的,禁止每次发送/读取都重写整份缓存。历史教训是 sent-message-cache 回归——重写缓存会让事件循环停顿,而这个停顿会伪装成轮询停滞,难以排查。

3.7 传输错误分类

getUpdates worker 的本地重试规则:

  • Bot API5xx429本地重试,并遵循parameters.retry_after
  • 401/404保持致命(fatal);
  • 409必须传播给父会话(parent session),由父会话负责 webhook-conflict 恢复;
  • 解析 Bot API 错误体要防御性处理:错误码在error_code而非.code,且非 2xx 响应体不一定是 JSON(例如 502 可能返回 HTML 页面)。

3.8 发送漏斗对等性(Send funnel parity)

持久化漏斗(send.ts)与流式漏斗(bot/delivery.*)必须同样优雅降级

触发场景降级行为
富文本实体 400回退为纯文本
caption 解析 400回退为纯 caption
quote-not-found 400回退为传统回复(legacy reply)

新增的恢复逻辑必须进共享谓词(send-error-predicates.tsreply-parameters.ts),绝不只修其中一个漏斗——否则两个漏斗行为漂移,同一消息在不同路径下表现不一致。

3.9 出站洪峰等待与 webhook 安全顺序

  • 出站洪峰等待遵循retry_after,封顶值为TELEGRAM_OUTBOUND_RETRY_AFTER_CAP_MS(见 extensions/telegram/src/retry-after.ts,值为 60_000ms),不允许把 Telegram 发送重新钳制到通用通道重试上限。
  • Webhook 安全顺序:先校验 secret header(常量时间比较、单 header 强制、401 时关闭连接),然后才做请求限流;限流预算只统计认证失败的尝试,从而保证 Telegram 自身的投递永远不会被限流误伤。
  • 所有插件拥有的 undici 传输必须在所有退出路径关闭:轮询会话、webhook 关闭与启动失败、probe-cache 驱逐。

四、流式回答(Streaming)的维护者决策

4.1 为什么禁用 sendMessageDraft

Telegram 的 draft(草稿)只是私聊中 30 秒的临时预览,最终投递仍需独立的sendMessage。OpenClaw 的流式实现采用sendMessage建立消息 +editMessageText持续编辑 + 原地定稿,用户看到的是一个持续存在的答案气泡,而不是草稿闪烁。

4.2 只拥有一个可见预览消息

流式过程只允许一个可见的预览消息,向前编辑它;除非最终编辑真的失败,否则不额外发送一条最终气泡。这与 4.1 结合,保证用户在聊天里看到稳定单一的回答。

4.3 保留首预览防抖

如果 provider 发送 token 级增量,应把增量合并为累积预览文本,而不是移除防抖。防抖存在的意义是避免高频 delta 打爆 Bot API 调用配额。

4.4 在 Telegram 层尊重 Telegram 限制

  • 文本超过4096 字符时链式拆分为续写消息;
  • 投票保持当前 Bot API 的12 选项上限

这些限制必须在 Telegram 层处理,不能指望上层语义自动适配。


五、Telegram API 所有权

5.1 优先 grammY 原生能力

当 grammY 原语与 Telegram 原生 helper 已经能直接建模所需行为时,优先复用 grammY,禁止自造重复的 Bot API 包装。这与扩展边界哲学一致:核心已拥有的能力不应在通道层重复实现。

5.2 节流是 bot-token 作用域的

所有使用同一 token 的 Telegram API 客户端共享同一个 grammYapiThrottler()实例。多实例节流会导致同一个 bot 的请求配额被重复计算,超出 429 防护的设计意图。

5.3 话题(topic)语义的两个硬规则

  • 不要在没有话题元数据的情况下静默重试失败的话题发送——投到错误表面(wrong surface)的成功比响亮的 Telegram 报错更糟糕;
  • DM 话题与论坛话题是两回事direct_messages_topic_idmessage_thread_id不可互换。

六、上下文与授权

6.1 回复上下文的来源边界

回复(reply)上下文只能来自OpenClaw 观察到的消息。虽然 Bot API 的 update 暴露reply_to_message,但 Bot API没有任意getMessage(chat, id)的后期回填(hydration)路径——也就是说不能事后按 chat+id 任意取回旧消息补上下文。因此回复链必须在接收时完整捕获。

6.2 本地上下文优先于陈旧回复祖先

提示词中,当前本地聊天的上下文必须压过陈旧的回复祖先链。很久以前被回复的消息不应看起来像当前活跃对话。这是防止模型“跑题到旧话题”的关键提示工程约束。

6.3 群组历史窗口:永远开启、滚动前进

  • 群组的历史窗口永远开启,并以historyLimit为界。文档明确禁止重引入 prompt-history 门控模式——那次回归曾让 ambient rooms(房间事件类会话)失明。
  • 历史窗口是滚动的:用“自你上次回复以来”的自条目水位(self-entry watermark)选取视图,禁止重引入破坏性清空。原因很实在:room_event 不持久化到 session,清掉的上下文无法恢复。

6.4 授权模型的三个要点

  • 配对(pairing)仅限 DM。群组与话题的授权必须走显式配置的 allowlist;
  • Telegram allowlist 使用数字 sender ID。用户名是可选的、可变的,不能作为 Bot API 中可靠的任意用户查询键;
  • 群组与频道的可见回复由策略控制:普通房间回复保持私密,除非配置messages.groupChat.visibleReplies: "automatic",或 agent 显式调用message.send

七、交互面(Interactive Surfaces)

  • 原生回调保持结构化:审批(approval)、原生命令(native command)、插件、单选(select)、多选(multiselect)回调不得作为原始回调文本透传
  • 回调值必须逐字节保真,包括env|prod这类带分隔符的值——解析端依赖分隔符还原语义;
  • 原生斜杠命令保持快路径(fast-pathable):在完整 workspace 与 agent-turn 初始化之前就能路由执行,保证常用命令的低延迟响应。

八、评审标准:什么改动需要什么证明

文档把证明要求分成两类,这是所有 Telegram 相关 PR 的验收门槛:

改动类型覆盖范围证明要求
行为改动传输(transport)、流式(streaming)、话题(topics)、回调(callbacks)、授权(authorization)、回复上下文(reply context)真实 Telegram 证明:优先 bot-to-bot QA 通道或等效的真实 Telegram 探测,禁止仅凭合成(synthetic)验证
可靠性改动spool、drain、retry、ack、offset 路径crash-window 或 restart-replay 测试证明,而非仅 happy-path 测试

仓库内大量*.runtime.test.ts*.e2e.test.tswebhook.test.ts(如对OPENCLAW_TELEGRAM_SPOOLED_HANDLER_TIMEOUT_MS的 env stub 验证)正是这一标准的具体执行形态:用重启重放与崩溃窗口测试来钉死持久化不变量。


九、快速核对清单

改任何extensions/telegram/下的代码前,逐条自检:

  1. 是否触碰了核心 drain 契约(complete-at-adoption、tombstone、claim 心跳、统一重试)?若是,先读 src/channels/message/ingress-drain.ts,确认没有在 Telegram 层重复实现;
  2. 入站是否仍满足“先持久化后应答”(polling offset 在 spool 确认后推进,webhook 在 spool 写成功后 200)?
  3. lane 推导与 update_id 编码是否仍在 spool/sequential-key 层?
  4. 失败是否走了 disposition(release/fail)而非静默 complete?
  5. 流式是否仍单预览消息 + 原地定稿,未重引入sendMessageDraft
  6. 发送漏斗的两个路径(send.tsbot/delivery.*)是否共享同一降级谓词?
  7. 授权是否用数字 sender ID、配对是否仅 DM、群组可见回复是否受策略控制?
  8. 回调值是否结构化且逐字节保真?
  9. 测试是否匹配评审标准:行为改动有真实 Telegram 证明,可靠性改动有 crash-window/restart-replay 证明?

这份清单直接映射本文档的全部不变量;对每一项的深入实现,都可以在extensions/telegram/src与其配套测试中找到对应代码证据。

【免费下载链接】openclawThe AI that really does things. Any OS. Any Platform. The lobster way. 🦞项目地址: https://gitcode.com/GitHub_Trending/cl/openclaw

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询