系列第17篇 | 一个bug不可怕,三个同时来才致命
凌晨4点的飞书
2026年7月17日,凌晨4点。
我睡不着,打开飞书看了一眼小密(我的AI统筹Agent)的消息。然后我愣住了:
【小密统筹报告 07-17 04:04】
【小密统筹报告 07-17 04:34】
【小密统筹报告 07-17 05:04】
【小密统筹报告 07-17 05:34】
【小密统筹报告 07-17 06:04】
【小密统筹报告 07-17 06:34】
【小密统筹报告 07-17 07:04】
7条消息,每条都只有一个标题,没有任何内容。没有任务表格,没有进度,没有状态。
就像一个员工每半小时给你发一封邮件,标题写"工作汇报",打开里面是空白的。
他没在偷懒------他一直在发邮件。但他也没在工作------邮件里什么都没有。
我发了条消息:"有什么问题,一直在空转。"
然后我开始排查,发现不是一个bug,是三个。它们来自三个完全不同的系统层,互相不知道对方的存在,但效果叠加在一起------整个心跳系统"假装工作"了整整5个小时。
第一个炸弹:空报告
现象:心跳每30分钟发一条报告到飞书,从凌晨2点开始正常,4点开始变成只有标题的空消息。
排查过程:
我打开format_report.py,看到了这个函数:
python
def has_content(notes, timeout_tasks):
if notes: # ← 问题在这里
return True
if timeout_tasks:
return True
return False
看起来没毛病?但问题在于notes里装了什么。
当所有任务都处理完了,脚本会往notes里写一句"无待复核任务"。bool("无待复核任务")返回True,所以has_content说"有内容!",继续生成报告。
但后面有个过滤器:
python
filtered_notes = [n for n in notes if not n.startswith("无")]
"无待复核任务"以"无"开头,被过滤掉了。filtered_notes变成空列表。
但has_content看的是过滤前的notes ,不是过滤后的filtered_notes。
就像你在厨房检查食材有没有过期,但没检查做出来的菜有没有变质。
最终效果:脚本说"有内容"→生成报告→内容被过滤→只剩标题→发送空报告。
修复:
python
def has_content(notes, timeout_tasks):
# 不再检查原始notes,检查最终输出
lines = format_lines(notes, timeout_tasks)
content_lines = [l for l in lines if l.strip()
and not l.startswith('【')
and not l.startswith('|')]
return len(content_lines) > 0
教训:判断"有没有内容",要在出口看输出,不要在入口看输入。
第二个炸弹:手机白屏
现象:手机浏览器一片白,控制台没报错,Vue完全没渲染。
排查过程:
第一层:资源路径混乱。
mobile/index.html引用了/assets/xxx.js,但文件实际部署在/mobile/assets/下。浏览器请求404,JS不加载,Vue不启动。
但这个修好后,还有第二层------更隐蔽的。
localStorage跨设备污染。
我之前在PC端访问系统,点了侧边栏折叠按钮。JS把状态存进localStorage:
javascript
localStorage.setItem('sidebarCollapsed', 'true')
localStorage是按域名存储 的,跟设备无关。同一个域名(nasioe.cc.cd),PC端和手机端共享同一份localStorage。
我用手机打开同一个域名,浏览器读到sidebarCollapsed = 'true',JS把侧边栏宽度设成60px。手机屏幕375px宽,60px侧边栏+主内容区,布局全乱。
你以为localStorage是"本地"的,其实它是"本域名"的。
就像你在家里的冰箱贴了一张"今晚吃火锅"的便条,结果你出差住酒店,酒店冰箱里也有一张同样的便条------因为你们用的是同一个美团账号。
修复:
javascript
// 只在PC端恢复折叠状态
if (window.innerWidth > 768) {
const saved = localStorage.getItem('sidebarCollapsed');
if (saved === 'true') {
sidebar.classList.add('collapsed');
}
}
第三个炸弹:CSS幽灵
现象:PC端侧边栏折叠按钮被遮挡,折叠后菜单文字没隐藏。
排查过程:
我grep了CSS,找到了这段:
css
.sidebar.collapsed span:not(.menu-icon) {
display: none !important;
}
选择器匹配的是span:not(.menu-icon)------也就是隐藏非图标的span。
但实际HTML是这样的:
html
<a href="/dashboard.html" class="menu-item">
<span class="menu-icon">📊</span>
数据看板 <!-- ← 文字直接在<a>里,不在<span>里 -->
</a>
文字不在<span>里,直接写在<a>标签里。CSS选择器根本匹配不到它。
CSS不会报错。它只是静静地失效,看着你的页面一团糟。
就像你给门配了一把钥匙,但门锁已经换了。钥匙插进去,转不动,但门看起来还是那扇门。
修复:
css
.sidebar.collapsed a {
font-size: 0; /* 隐藏<a>内的文字 */
}
.sidebar.collapsed .menu-icon {
font-size: 18px; /* 强制显示图标 */
}
三个bug的"完美风暴"
| Bug | 影响 | 持续时间 |
|---|---|---|
| 空报告 | 飞书消息全是空白标题 | 5小时 |
| 手机白屏 | 手机端完全无法访问 | 直到修复 |
| CSS幽灵 | PC端侧边栏折叠失效 | 直到修复 |
三个bug单独看都不致命。但叠加在一起:
- 空报告→用户看不到系统在干什么("一直在空转")
- 手机白屏→移动端完全不可用
- CSS幽灵→PC端UI也有问题
从用户视角看:飞书里一串空消息,手机打不开,电脑侧边栏也是坏的。整个系统似乎瘫痪了。
但实际上,核心逻辑(任务派发、Agent调度、心跳监控)一直在正常运行。出问题的全是展示层。
一个"内部正常但外部表现异常"的系统,在用户眼里跟"完全坏掉"没有区别。
三个教训
1. 判断"有没有内容"要看最终输出
has_content检查原始notes,但notes在后面被过滤了。入口看到的和出口输出的不是同一回事。
2. localStorage是域名级别的,不是设备级别的
多设备共用的Web应用,UI状态必须考虑跨设备兼容。用window.innerWidth判断设备类型是最简单的方案。
3. HTML结构改了,CSS选择器必须同步grep
CSS选择器是"盲"的------DOM结构变了,选择器失效,样式静默丢失。改HTML后第一件事:grep所有引用该class的CSS文件。