无日志报错故障排查实战:WebSocket 推送失效、递归栈溢出、消息体兼容问题根治方案

发布时间:2026/7/28 21:11:41
无日志报错故障排查实战:WebSocket 推送失效、递归栈溢出、消息体兼容问题根治方案 本期敖行客研发实战日记完整复盘整套排查与修复流程专治这类「静默失效」的疑难问题从代码未执行、空方法覆盖、无限递归埋雷到工程空壳目录导致修改不生效、多业务消息无法区分、重启/恢复状态码混淆、代码执行顺序错误等一连串连锁底层 Bug逐层拆解根因。这次的活儿起点只有一句话“TboT 重启完前端的 websocket 不提醒了。”没有报错、没有异常堆栈日志干干净净——最难查的正是这种。顺着这条线往下摸先挖出一段被注释掉的重写、一对自己调自己的 getter又撞上一个只剩空壳的项目目录等推送真正通了才发现新问题在后头TboT 和编程空间的消息前端根本分不清重启和恢复推出来的状态码一模一样。下面按排查顺序复盘。一、没有异常的故障先确认它到底有没有被调用“不提醒”这类问题有个天然的岔路口是消息没发出去还是发出去了前端没收到。先读 notifyMsg 本身——推送逻辑、多端遍历、站内消息落库全都完好看不出毛病。那就往上游走一层。触发链路是这样的定时 worker 执行完 K8s 操作调 Manager.notifyOptResult再由它调到 service.notifyMsg。翻到 OpenClawManager答案就摆在那儿——整个 notifyOptResult 重写被一整段注释掉了。而基类 Manager 里的默认实现方法体里唯一一行也是注释是个不折不扣的空实现。于是 TboT 走的是空方法一条消息都不推。对照旁边的 IdeManager 和 BoxManager两个都老老实实重写了这个方法——这也解释了为什么只有 TboT 不提醒编程空间一切正常。图 1三个 Manager 的对比——只有 OpenClawManager 回退到了基类的空实现经验日志干净、代码看着也对的时候先别急着怀疑逻辑去确认这段代码到底有没有被执行。一个空的基类实现加一段被注释的重写比任何异常都安静。同一套基类下有多个兄弟实现时横向对比是最快的定位手段。二、连坐的第二颗雷两个自己调自己的 getter把注释放开之前顺手读了一遍消息体 OpenClawMessageVo结果又捞出一个getUpdateStatus 的方法体是 return getUpdateStatus()setUpdateStatus 里写的是 setUpdateStatus(updateStatus)。两个方法都在调用自己是标准的无限递归。这颗雷平时不响是因为压根没人走到。但基类的 fillNotifyMsg 里有个分支当 result 是 1 或 5、且 updateStatus 等于 1 时会调 msg.setUpdateStatus(2)。也就是说只要我把 notifyOptResult 放开、并且赶上一次“更新重启”立刻就是 StackOverflowError。两个 bug 是叠着的只修一个等于把故障从“不提醒”换成“栈溢出”。同一个类里还有第三处getVpsId 写的是 Long.valueOf(getOpenClawId())openClawId 为 null 时会直接 NPE。这三处一并修掉——递归的那两个手写重写直接删除Lombok 的 Data 生成的 getter/setter 本来就满足接口getVpsId 改成直接返回不再多做一次拆装箱。图 2三处缺陷与触发路径——放开注释就会踩中的连锁反应经验修复一处故障后顺着它被恢复的执行路径把沿途代码读一遍。平时“不响”的雷往往只是因为调用它的那段代码恰好是死的你把上游修通的同时也把下游的雷激活了。三、改了半天没生效一个只剩 .idea 的空壳目录这一节和技术无关但值得单独写出来因为它差点让整件事白干。拿到的路径是 at-work-cloud-0719 下的某个文件。读得到、改得动、编译也过一切正常。直到后面为了确认改动落点用 find 在 0719 里搜 OpenClawManager.java返回空再 ls 一层这个目录里只有一个 .idea 文件夹没有任何源码。真正的源码在 at-work-cloud-0721。之前所有的读写实际都落在了 0721 上。分析和改动本身没错落点也恰好是当前在用的版本但引用的每一个路径都指向了一个空壳。用 grep 反查自己写下的那句注释才确认了这一点。这类目录在长期迭代的项目里很常见——按日期拉分支、拉副本清理时删了源码却留下 .idea外面看起来和正常工程一模一样。教训是面对多个同名带日期的目录改动前先确认目标目录里真的有源码别只看路径能不能打开。经验工程环境本身也是需要核对的信息。同名带日期的多份副本、只剩配置的空壳目录都会让“文件已修改”这个反馈变得不可信。改完之后用 find 或 grep 反查一次自己的改动落在哪成本极低能挡住最尴尬的一类返工。四、推送通了但前端分不清加一个 bizType推送恢复之后新问题浮出水面TboT 和编程空间的状态消息前端根本区分不了。两边都发 code300。更麻烦的是TboT 这条链路当初为了省事直接复用了 WebSocketVo 里现成的 ideId 和 ideName 字段来装自己的 id 和名字。于是抓包看到的两个包字段构成完全一致没有任何一个字段能用来判断这是谁发的。前端的代码印证了这一点coding.vue 里只判断了 data.code 300然后就去刷编程空间列表。也就是说重启一个 TboT编程空间的列表也会跟着刷一次。解法是加一个 bizType 字段取值 ide 或 tbot在三处推送点分别打标。之所以选择加字段而不是给 TboT 换一个新的 code比如 301是因为新 code 会让所有现存前端的 code 300 分支直接漏掉 TboT 消息属于破坏性变更加字段则是纯增量老前端不读它行为完全不变。图 3改前两个包无法区分改后靠 bizType 分流经验给共用的消息通道加业务标识优先选择“加字段”而不是“换编码”。加字段是纯增量老消费者不受影响换编码会让所有按老编码分支走的消费者集体失联。另外灰度期务必把字段为空的情况归到老行为那一侧新前端配旧服务端才不会翻车。五、重启和恢复都是 4先看清结果码是怎么来的接下来的需求很直接重启成功和恢复成功要用不同的 type现在都是 4。先弄清 4 是怎么来的。基类的注释写得很明确结果码 1 创建、2 启动、3 停止、4 不更新重启、5 更新重启、9 销毁成功为正数、失败为负数。而恢复这个操作底层复用的就是重启所以它拿到的结果码同样是 4——两者推出来的 type 自然一模一样。区分它们的信息不在结果码里而在一个 redis 标记 isRecover 上。但真要按这个标记分流时撞上了一个顺序问题type 在第 147 行就已经赋值完毕isRecover 却要到第 152 行才从 redis 里读出来。不调整顺序这个分流根本拿不到标记。把标记的读取整段提到赋值之前问题才算解决。顺带还清掉了一次重复的 redis delete——同一个 key 上面几行已经删过了。最终恢复占用 12成功和 13失败。随后追加的启动状态略有不同启动本身是 2 和 -2已经能区分成功失败给它配 14 和 15 纯粹是为了和前面凑成一套“正数即成功、前端不必解析负号”的取值方案。图 4type 取值的改前改后以及一并修掉的赋值顺序问题经验动手改状态码之前先把它的来源查清楚。“两个操作状态码相同”可能不是疏漏而是因为它们在底层本就是同一个操作——区分它们的信息得去别处找。另外当赋值依赖某个前置计算时务必确认两者的先后顺序这类顺序错误编译器不会报测试也未必覆盖得到。六、克制的边界那个前端的 RangeError中途插进来一个问题在 TboT 里开新对话、传一个 PDF 点发送报 RangeError: Maximum call stack size exceeded。这个错误的形态很典型——大文件转 base64 时用了 String.fromCharCode(...new Uint8Array(buf)) 这类展开写法参数个数等于字节数几百万个实参直接爆掉调用栈且只跟文件大小有关跟是不是新对话无关。按这个特征去定位很快找到了唯一匹配的那句 toast 和它上游的发送链路。但到此为止了这份工程里根本没有带附件发送的 UI。attachments 只出现在底层的 store 和 socket 封装里没有任何页面往里塞值截图上那张卡片的文案在整个目录里也搜不到翻遍所有分支同样没有。结论只能是——那个页面的源码不在这台机器上。于是我停下来问路径而不是照着“最可能的写法”去改一个自己没看见的文件。猜测和定位是两回事前者听起来也很有道理但改错地方的代价比多问一句要大得多。经验排查跨端问题时分清“我推断出的根因”和“我验证过的根因”。当代码不在手边、证据链断在半路时把已确认的部分和待确认的部分如实交代清楚比给一个听起来完整的答案更有价值。小结图 5本次问题总览现象 → 根因 → 解法没有异常的故障先确认代码有没有被执行空的基类实现加一段被注释的重写比任何报错都安静。同一基类下有多个兄弟实现时横向对比是最快的定位手段——只有 TboT 不提醒本身就是线索。修通上游后要顺着执行路径复查下游平时不响的雷只是因为调用它的代码恰好是死的。工程环境也需要核对同名带日期的副本、只剩 .idea 的空壳目录会让“文件已修改”变得不可信。给共用通道加业务标识优先加字段而非换编码灰度期把空值归到老行为一侧。改状态码前先查它的来源两个操作码相同可能是因为底层本就是同一个操作。守住任务边界代码不在手边时如实交代已验证和未验证的部分别用猜测填补证据链。至此TboT 的状态推送从“完全没有”恢复到了正常bizType 让前端能把它和编程空间分开恢复与启动也有了各自独立的状态码。回头看这次真正花时间的都不是写代码是确认一段代码有没有被执行是核对改动到底落在哪个目录是弄清两个相同的数字背后其实是同一个操作。老系统里的问题大多如此——难的从来不是修而是先看清楚要修的到底是什么。敖行客介绍敖行客Allthinker聚焦服务企业研发团队及开发者以搭载自研企业级智能体引擎的 AT Work-Agent 研发工作台为核心支撑打造 AI 原生一体化研发协同体系依托企业智能体重构研发协作范式致力于赋能各类研发团队轻量化完成智能化升级。AT Work介绍AT Work-Agent 研发工作台是国内首个分钟级部署、AI 原生全链路研发协同平台依托企业级智能体赋能研发全流程零门槛打造专属 AI 研发团队实现研发效率与数据安全的双重飞跃。官网www.allthinker.com邮箱allthinkerallthinker.com