自建 TG 机器人自审:5 个坑

语料源静默退化、CI 被 registry 打挂、数据没有异地副本、出错没人知道,还有一个更吓人的:测试脚本的 glob 不递归,356 项里实际只跑了 6 个。附实测数字与取舍表。

技术 · 2026-10-24 · 约 6 分钟阅读 · 标签:Telegram、自托管、工程实践、备份、测试

起因

这套机器人是私有自建工具,一个账号、一个进程,同时跑着总结、生图、AI 问答、转发、备份十几件事。功能加得快,质量保障一直只有 tsc --noEmit 加手动试。

这次按「运维现状 → 代码 → 工具链」的顺序做了一次自审,标准只有一个:能拿证据复现的问题才算 bug。结果确认 5 个,当天修完 4 个,第 5 个(缺观测)补了模块。

坑 1:语料拉不到,插件静默退化成 12 条

现象:某个随机文案命令,用起来「感觉比以前少了很多」。代码里语料来自上游仓库,本地缓存约 150 条,拉不到就用 12 条内置兜底。

取证:宿主机和容器里分别请求语料源,都是 HTTP 000 / 12.0s 超时。也就是说兜底不是偶发,是必现。

更糟的是代码逻辑:

if (!force && this.items?.length) return this.items;

一旦回落到内置文案,items 就有值了,之后每次调用直接返回,直到进程重启 —— 上游恢复也不会自愈。

修法三条:

  • 失败后 5 分钟冷却自动重试,不再钉死到进程结束
  • 语料源改成镜像优先:实测 CDN 0.97s / 200,原站 12s 超时;原站与第二个上游保留为兜底
  • 手动刷新不再谎报成功:区分「远端 / 缓存 / 内置」,失败就直说

修完在容器里实测:157 条,刷新成功。

坑 2:CI 被 npm registry 打挂过一次

一条纯文档提交的流水线失败,耗时 7 分 43 秒,日志是 npm error audit endpoint returned an error —— registry 503,和代码无关。

修法:npm audit 重试三次;用 JSON 输出的 error 字段区分「接口挂了」和「真有漏洞」,前者只告警,后者(high/critical)照样红。

坑 3:SSRF 校验依赖的库有 ReDoS

npm audit 报 ip-address <= 10.7.3 的 ReDoS 公告。这个库不是无关的间接依赖 —— 媒体下载的白名单校验直接 import { Address4, Address6 },属于安全路径。

修法:10.7.1 → 10.7.4(同 major,API 不变),npm audit --omit=dev 归零。

坑 4:数据只在这一台机器上

备份脚本历史上指向一个目录,而项目数据在另一个目录;加密备份的远端配置也已失效。等于:进程挂了能重启,机器挂了什么都回不来 —— Session、规则、模型配置全在里面,2.9MB。

修法:不新建常驻脚本,直接用项目自带的备份链路(清单 + 体积上限 + SHA-256 校验 + 原子替换):

  • 每天 04:30 自动投递一份到收藏夹;重启错过时间点会补跑一次,同一天不重复
  • .bf now 在任意聊天手动触发,.bf now session 才带上敏感 Session
  • 实测:102 个文件,校验通过,状态落盘;重启后确认未重复上传

坑 5:出问题没人知道

所有模块报错只进 docker logs。日志里每天约 8 次 Channel ... difference too long(频道流重同步告警),只能被动发现,而且不知道影响面。

修法:运行时计数(每模块命令量、最近错误、重同步次数、FloodWait)+ console 钩子;.health 随时查,每天 09:00 一份到收藏夹,且只在收藏夹可读。

加装 ESLint 捞出的两个真问题

44 个问题里,两个不是风格问题:

  1. emoji 标题正则 /^[⏰🏆🔥…]\s/ 缺 u 标志。字符类里的 emoji 是代理对,没有 u 会按单个码元匹配,导致「以某个 emoji 开头」的判断误命中别的表情。
  2. 测试脚本写 tests/**/*.test.ts,在 npm 的 sh 下 ** 不递归 —— 实际只跑了一小部分。改成显式 glob 后,测试数从 6 变 351(现在是 356)。这条最吓人:绿了很久,其实没跑。

顺手清掉 21 个死函数

重构后 handler 里留了一批没人调用的格式化/取历史函数。手工数括号删太容易切错,改用 TypeScript AST 定位声明边界,循环四轮(删掉一批后,只有它们使用的依赖才变成新的死代码),最后 lint 零告警、测试全绿。

附:生图从 1k 换到 2k

同一上游只有 1k/2k/4k 三档。4k 实测两次都是上游 500,属于没真开;2k 可用。顺带发现 images 模式硬编码 1024x1024,横版 prompt 会被压成方图 —— 改成 1536x1024 并开放环境变量覆盖。实测 41.8s / 2.5MB,小字明显更实。

结果

| 项目 | 之前 | 现在 | |---|---|---| | 测试 | 343(且只跑了一部分) | 356 全量执行 | | Lint | 无 | ESLint 9,0 error 0 warning | | 依赖漏洞 | 1 moderate | 0 | | 异地备份 | 无 | 每日 102 个文件,含校验 | | 可观测 | 只有 docker logs | .health + 每日报告 | | 文案语料 | 12 条兜底且不自愈 | 157 条 + 5 分钟重试 |

取舍

| 决定 | 理由 | 代价 | |---|---|---| | 不用 restic/rclone 重建,复用项目内备份 | 远端 OAuth 已失效,重建要交互;项目自带校验链路够用 | 默认不含 Session,换机要重新登录一次 | | 语料走 CDN 镜像而非 vendor 进仓库 | 上游是别人作品,许可证不同;镜像快且能拿更新 | 依赖第三方 CDN,保留原站兜底 | | no-explicit-any 关掉 | MTProto 载荷本来无类型,开它只会逼出几百个 cast | 类型收益仍由 typecheck 保证 | | 4k 不上 | 每次 500,白等三秒再降级 | 分辨率停在 2k |

一句

自审的价值不在「找到几个 bug」,而在把「感觉不对」变成可复现的数字。而这次最大的收获是那条测试 glob —— 覆盖率是假的,比没有更危险。

*本文记录私有自建工具的工程结论与实测数据,不公开仓库地址与部署信息。*

全部文章 · 返回 OneMJJ 首页

OneMJJ · VPS、自托管与 AI 工具与历史维护笔记。少踩坑,多留传家宝。

部分工具由第三方运营;本站命令仅供复制,不会自动执行。运行前请核对环境、阅读源码并备份重要数据。本站可能展示 Google AdSense 广告,详见隐私政策。