TeleDeck:把十个 Telegram 账号工具收进一个长期运行的容器

从多个独立插件和脚本迁移到一个共享 MTProto Client:模块隔离、Session 安全、流式模型容灾、备份恢复与可回滚生产切换。

工程笔记 · 2026-07-28 · 约 13 分钟阅读 · 标签:TypeScript、Telegram、MTProto、Docker、架构实践、自托管

我长期使用 Telegram,也陆续为自己的账号维护过群聊摘要、动态昵称、AI 问答、消息转发、私聊验证、新闻、天气、语录和备份等工具。

它们最初分散在不同插件、脚本和服务里:每个项目都能解决一个问题,但每个项目也可能各自建立 Telegram 连接、保存一份 Session、维护一套配置,并拥有不同的启动与更新方式。功能越多,真正让人疲惫的往往不是代码,而是长期运维。

TeleDeck 就是这次整理的结果:一个账号、一个 Session、一个 Telegram Client、一个容器,统一运行十个彼此隔离的模块。

这是个人长期维护项目。仓库目前保持私有,不提供公开下载;朋友确有需要时再单独授权。本文只记录可复用的工程思路,不公开任何生产凭据、服务器信息或私人配置。

为什么不继续堆独立服务

单个工具独立运行并没有错。问题出现在数量增加之后:

  • 多个进程可能同时使用同一个 Telegram Session;
  • 命令前缀和监听范围容易发生冲突;
  • 每个服务都有自己的 Docker、日志、更新和备份方式;
  • 同一类 Provider 配置被复制很多次;
  • 出现重复回复或重复转发时,很难判断是谁处理了消息;
  • 迁移和回滚需要在多个项目之间来回切换。

Telegram MTProto 用户账号工具与普通 Web 服务还有一个差异:同一个账号、同一个正式功能,不适合通过双开实例做灰度。 两个实例同时消费相同事件,可能导致重复回复、重复转发和会话冲突。

因此,TeleDeck 的目标不是"把所有代码放进一个大文件",而是统一运行时,同时保留清晰的模块边界。

一个 Client,十个模块

当前运行的模块包括:

  1. SumPlus:群聊摘要、日报、热点、人物、待办、金句、关系网与图片海报;
  2. AutoName:按时间、天气、时区和轮换文案更新个人昵称;
  3. AI:普通问答、回复上下文、图片理解与搜索模式;
  4. Shift:按来源、目标、Topic、媒体类型和过滤规则实时转发;
  5. Repeat:从回复消息开始,按数量与次数复读;
  6. PMCaptcha:处理陌生私聊验证,并维护可信用户状态;
  7. Quote:把回复消息生成 WebP 语录,远端失败时本地渲染;
  8. Backup:生成带清单和哈希校验的运行数据备份,并支持受控恢复;
  9. News:在 Telegram 内获取每日新闻与固定栏目;
  10. Weather:查询全球城市实时天气。

这些模块共用一个 Telegram Client,但配置、状态、命令所有权、启动开关和数据目录彼此隔离。

Telegram MTProto
  → Shared Client
  → Event Intake
  → Module Router
      ├─ SumPlus
      ├─ AutoName
      ├─ AI
      ├─ Shift
      ├─ Repeat
      ├─ PMCaptcha
      ├─ Quote
      ├─ Backup
      ├─ News
      └─ Weather

路由器在启动时检查重复命令所有权。一个命令只能由一个启用模块处理;观察型模块与命令型模块也使用不同入口。这样可以从结构上减少"谁抢走了这条消息"的不确定性。

Session 是最重要的安全边界

TeleDeck 使用的是 MTProto 用户账号,而不是公开 Bot。Session 等同于账号登录凭据,因此设计时优先处理以下问题:

  • Session 永远不进入 Git;
  • 登录与正式运行分离;
  • 二维码或手机号登录只负责生成本地 Session 文件;
  • 同一 Session 不交给多个生产实例;
  • 容器数据目录使用持久化挂载;
  • 备份默认不包含 Session,只有显式选择才加入;
  • 配置展示接口必须对 Provider Key 脱敏。

这类工具最危险的失败并不是某个命令报错,而是"为了测试新服务,让旧服务和新服务同时监听正式账号"。因此迁移过程必须比普通无状态应用更保守。

分阶段迁移,而不是一次性替换

这次迁移采用了分阶段方式:

  1. 先建立全新的 TeleDeck Session;
  2. 在隔离环境验证共享 Client 与模块路由;
  3. 优先迁移 News、Weather、Repeat 等低风险命令;
  4. 每迁移一个正式处理器,先停止旧实现,再启用 TeleDeck 对应模块;
  5. Shift 和 PMCaptcha 这类观察型模块最后切换;
  6. 旧数据和旧服务保留一段时间用于回滚,但不再同时监听;
  7. 每次切换都执行真实 Telegram 命令并检查日志。

这里追求的不是字面意义上的"零停机",而是不重复处理、不丢失回滚能力、每一步都有证据。对个人账号工具而言,几秒钟的受控切换远比双开安全。

大模型线路:真实模型名与独立故障域

AI 和摘要模块都使用 OpenAI-compatible Provider。最初看起来只要配置一个主模型和几个 fallback 就够了,但实际运行后发现,真正可靠的容灾需要回答三个问题:

  1. 模型名是否对应真实上游,而不是不透明别名?
  2. fallback 是否来自不同账号或余额池?
  3. 目标接口究竟支持普通 JSON、流式 SSE,还是 Responses 协议?

如果多个模型最终共用同一个上游账号池,它们并不构成真正冗余。反过来,不同中转站即使列出了高端模型,也可能出现列表可见、请求不可用、余额不足或协议不兼容。

因此上线前逐条执行了真实请求,检查:

  • HTTP 状态;
  • 是否产生最终正文;
  • 是否保留指定事实;
  • 流式事件能否完整结束;
  • 搜索参数是否被上游接受;
  • 推理内容是否挤占最终输出;
  • 请求是否命中预期的独立账号组。

最终,普通聊天、搜索和摘要没有机械地共用同一条 fallback 链。明确不支持搜索参数的模型只用于聊天;只产生推理却没有最终正文的模型不会进入搜索链。

流式兼容不是加一个 stream: true

不同 OpenAI-compatible 服务对流式响应的实现差异很大。一些服务在非流式请求下会提前断开,却能稳定输出 SSE;另一些推理模型会先返回很长的 reasoning_content,最后才给出 delta.content

TeleDeck 为此增加了流式解析层:

  • 逐行解析 data: 事件;
  • 忽略 [DONE]
  • 只拼接最终正文 delta.content
  • 不把内部推理字段展示给用户;
  • 单个损坏片段不会让整次响应失败;
  • 没有最终正文时触发下一条 fallback,而不是返回空消息。

同时,自动回复路径不能擅自把 Provider 的流式配置覆盖为非流式。这个问题通过测试先复现,再修改实现,避免只修复摘要却遗漏短回复。

Quote:远端服务与本地降级

语录模块采用双路径:优先调用固定版本的内部 Quote API生成 WebP;如果服务不可用、返回 HTML或图片数据无效,则自动使用本地 Canvas。

本地镜像安装 Noto CJK 字体,确保中文不会变成方框或十六进制占位。Quote API只暴露在 Docker内部网络,不占用宿主端口。

这类降级路径必须真实生成并检查图片,而不是只判断 HTTP 200。对媒体功能而言,"请求成功"与"用户收到可读图片"是两件事。

Backup:默认不碰 Session

Backup 模块采用显式安全边界:

  • 只能在 Telegram收藏夹运行;
  • 默认备份不包含 Session;
  • 备份携带 manifest、文件大小和 SHA-256;
  • 恢复前验证路径、文件类型、清单和哈希;
  • 不接受符号链接和未声明成员;
  • 无 Session归档恢复时,保留本机当前 Session;
  • 数据替换前保留 rollback;
  • 恢复成功后重启,让所有模块从新数据重新加载。

备份能否恢复,不能等到事故发生时才知道。因此测试里包含真实归档、篡改、路径穿越、Session保留和回滚场景。

Docker 与长期运行

TeleDeck 运行在一台长期在线的 Mac mini上,由 Docker Compose管理。容器使用非 root用户,数据通过挂载目录持久化,并提供健康检查。

典型更新流程是:

拉取代码
  → 运行全部测试与类型检查
  → 构建镜像
  → 重建容器
  → 等待健康状态
  → 检查启动模块
  → 检查 Session冲突与 Fatal日志
  → 执行代表性真实命令

真正的完成标准不是"镜像构建成功",而是生产容器健康、模块全部上线、真实命令可用,并且日志能证明请求走了预期路径。

测试覆盖什么

项目目前的自动化测试覆盖:

  • 模块启动、停止和失败回滚;
  • 命令所有权与事件路由;
  • AI 普通与 SSE 响应;
  • SumPlus Provider故障切换;
  • Shift规则、过滤、相册和编辑消息;
  • PMCaptcha验证状态;
  • Quote远端失败与本地图片;
  • Backup清单、哈希、路径与恢复事务;
  • News、Weather、AutoName等模块行为;
  • 配置权限与敏感信息脱敏。

CI 还执行 TypeScript检查、依赖审计、Docker构建、非 root验证和容器内模块导入。

代码审查与加固(2026-07-30)

项目上线一段时间后,对全部代码做了一次系统性审查,修复了以下问题,共涉及 13 个文件,净增约 72 行、删除 16 行:

Bug 修复

  • src/app.tsmaintenance.run() 的异常处理存在逻辑缺陷——catch 块里恢复 host/runtime 失败会被 finally 吞掉,现改为只在 try 成功路径恢复,finally 只负责清理 maintaining 标志,失败正常抛出。
  • src/core/telegram-runtime.tsenqueue() 的 Promise 链会无限增长,长时间运行数周后会积累大量 Promise 对象。现在任务完成后重置 queue,不再泄漏内存。
  • src/index.tsonModuleError 日志改用 error.stack || error.message,避免 TypeError 等 message 为空的错误静默丢失。

运维加固

  • docker-compose.yml:teledeck 容器补充 cap_drop: [ALL],与 quote-api 容器保持一致。
  • .github/workflows/ci.yml:CI 明确安装 canvas 原生依赖(libcairo2-dev 等),确保构建可复现。

文档与配置完善

  • .env.example:每个变量都加了中文注释,说明是否需要 API Key、默认值和作用。
  • README.md / README.en.md:补充 v0.1.0 版本徽章。
  • Dockerfile:添加 OCI 标准 LABEL(title / description / licenses / source)。
  • package.json:补充 repository / bugs / homepage 字段。
  • .gitignore:补充 .env.local*.tsbuildinfo
  • src/modules/news/module.ts / weather/module.ts:注释标注 htmlEscape 辅助函数重复,待后续统一抽取到 src/core/html.ts

审查完成后,160 项测试全部通过,TypeScript 类型检查干净,Docker 镜像重新构建并更新到生产容器,健康检查正常。

为什么仓库仍然保持私有

TeleDeck 已经具备完整 README、中英文部署指南、迁移指南、安全政策、贡献说明、维护说明和许可证归属,但我暂时没有把它变成公开项目。

原因并不是代码不能运行,而是它首先服务于我自己的账号和工作流。公开维护意味着需要长期处理通用部署环境、兼容性问题、Issue和支持边界。现阶段更合适的方式是:

  • 生产配置与源码模板严格分离;
  • 仓库保持私有;
  • 朋友确有需要时单独授权;
  • 不直接复制包含 data/.env 的生产目录;
  • 分享时只提供干净代码和部署文档,让对方创建自己的 Session与 Provider配置。

小结

TeleDeck 没有发明新的 Telegram协议,也不是把十个功能强行塞进一个巨型模块。它解决的是个人长期运维中的现实问题:

  • 只维持一个账号连接;
  • 模块边界清楚;
  • 配置和凭据不混入代码;
  • 迁移不双开;
  • 模型容灾基于真实验证;
  • 媒体和备份都有降级与回滚;
  • 每次更新都有自动测试和生产验收。

对个人自托管项目而言,功能"能跑"只是起点。真正让人安心的是:知道它为什么不会重复处理,知道故障时会走哪条路径,也知道如何安全恢复到上一个状态。

全部文章 · 返回 OneMJJ 首页

OneMJJ · 少踩坑,多留传家宝 · Public tools first.