4.7 KiB
4.7 KiB
电费插件的输入边界:同一个"静默降级",三个变种
- 建档:2026-09-29(晚间收尾)
- 状态:🟢 三类均已修 + 真实数据实测 + 回归通过;待主人实机复验一次
- 触发场景:主人当天三次追问同一件事——"错误的输入会发生什么"
一、一句话总结
同一个毛病长出了三张脸:输入无法被正确理解时,程序不报错,而是"猜一个"然后照常输出。
于是用户的输入与看到的输出之间,没有因果关系——主人当天的原话是"随机返回"。
| 变种 | 谁发现 | 表现 | 根因 |
|---|---|---|---|
| ① 垃圾输入 | 主人随手敲三行 | 无法识别的输入被降级成"只给了校区",照样返回 26 个楼栋 | 解析器只有两种出口(查到 / 没找到),没有"看不懂"这个状态 |
| ② 消息尾缀 | 主人实机复测 | 修好 ① 之后,合法输入也全挂 | 同机另一插件给每条消息尾部追加了编号;本机拿到的是被改装过的副本,却拿它去做字符校验 |
| ③ 数字匹配 | 主人 22:01("依旧 0 等于 10") | 0 → 10 号楼;06 / 09 → 永远找不到楼栋 |
选项匹配用的是子串包含("0号公寓" in "10号公寓" 成立),而不是数字相等 |
二、变种 ③ 的根因细节(最隐蔽的一个)
原实现:
def _pick(items, key, needle):
for it in items:
if needle in str(it.get("label", "")): # ← 子串匹配
return it
调用处传的是 f"{build_no}号公寓",于是:
build_no = "0"→ needle"0号公寓"→ 遍历到10号公寓时子串命中 → 悄悄查了 10 号楼;build_no = "06"→ needle"06号公寓"→ 谁都不是它的子串 → 返回 None → "永远找不到"。
同一句输入,一边是悄悄地错,一边是永远找不到——两个毛病同一个根。
三、修法
- 新增按数字精确匹配
_pick_no():取选项名开头的数字做整数比较(6号公寓→ 6、101房→ 101),楼栋与房间都改走它; _pick()调整为三段式:先精确 → 再数字 → 最后才子串(校区名仍需模糊匹配,故保留最后一层);- 顺带白拿一个好处:前导零自动归一(
06→ 6 号楼、09→ 9 号楼)。
没有做的事情(刻意的):没有把 _pick 的子串匹配整个删掉——校区名("含浦" → "含浦学生宿舍")依赖它。收窄的是"数字类"匹配,不是全部。
四、验证(跑的是真实站点数据)
| 输入形态 | 修前 | 修后 |
|---|---|---|
0-XXX |
❌ 悄悄去查 10 号楼 | ✅ 如实回"没有找到 0 号楼" |
06-XXX(前导零) |
❌ 永远找不到 6 号楼 | ✅ 正确落到 6 号楼,并查到房间 |
09-XXX |
❌ 永远找不到 9 号楼 | ✅ 正确落到 9 号楼(房间不存在则如实回) |
10-XXX |
⚠️ 会先命中 1 号楼 | ✅ 10 号楼 |
| 常规写法 | ✅ | ✅ 不变 |
- 跑的是真实站点的选项列表(含 26 栋楼、两百余间房),不是构造样例;
- 解析层 27 例回归 → 27 / 27 通过;
- 插件已重载生效;改前主文件已完整备份(
/tmp,可回滚)。
五、要记住的三条
- 只测 happy path = 没测。 我修好 ① 之后宣布"27 例全过"——那 27 例全是合法输入。主人随手敲三行垃圾,当场撞出两个。
- 同一个坑在同一个位置栽了两次:① 的"残留校区词"补过一次,重写解析器时又漏了;③ 的"子串匹配"也属于同一个"用包含代替相等"的思路。→ 它需要的不是再修一遍,而是把用例固化成文件、每次改完就跑。(主人中午提过的那份 27 例用例,尚未落成
test_parse.py,仍挂账。) - 入参不一定是用户打的原文。 修 ① 之后全挂,因为消息在到达我之前被别的插件改装过。→ 以后凡是"我读到的内容"要参与判断,先确认它是不是原文。
六、附带发现(并入观察档)
- 反刷屏与"欢迎"撞车:本机为新成员写的欢迎语(三行 + emoji)被判刷屏点名。判定依据是"1 秒内的行数与关键词命中",而欢迎与刷屏在这套统计特征上高度重合。两条可能的修法:给欢迎类模板放行,或把欢迎压成"一小段文字 + 一张图片"。(未改,待主人定。)
- 同群两台电费 bot 抢答:另一台是别校格式(提示语与本校房号规则完全不搭),会对同一条消息各答一遍。已多次记档,继续观察。