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,十个模块
当前运行的模块包括:
- SumPlus:群聊摘要、日报、热点、人物、待办、金句、关系网与图片海报;
- AutoName:按时间、天气、时区和轮换文案更新个人昵称;
- AI:普通问答、回复上下文、图片理解与搜索模式;
- Shift:按来源、目标、Topic、媒体类型和过滤规则实时转发;
- Repeat:从回复消息开始,按数量与次数复读;
- PMCaptcha:处理陌生私聊验证,并维护可信用户状态;
- Quote:把回复消息生成 WebP 语录,远端失败时本地渲染;
- Backup:生成带清单和哈希校验的运行数据备份,并支持受控恢复;
- News:在 Telegram 内获取每日新闻与固定栏目;
- 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 脱敏。
这类工具最危险的失败并不是某个命令报错,而是"为了测试新服务,让旧服务和新服务同时监听正式账号"。因此迁移过程必须比普通无状态应用更保守。
分阶段迁移,而不是一次性替换
这次迁移采用了分阶段方式:
- 先建立全新的 TeleDeck Session;
- 在隔离环境验证共享 Client 与模块路由;
- 优先迁移 News、Weather、Repeat 等低风险命令;
- 每迁移一个正式处理器,先停止旧实现,再启用 TeleDeck 对应模块;
- Shift 和 PMCaptcha 这类观察型模块最后切换;
- 旧数据和旧服务保留一段时间用于回滚,但不再同时监听;
- 每次切换都执行真实 Telegram 命令并检查日志。
这里追求的不是字面意义上的"零停机",而是不重复处理、不丢失回滚能力、每一步都有证据。对个人账号工具而言,几秒钟的受控切换远比双开安全。
大模型线路:真实模型名与独立故障域
AI 和摘要模块都使用 OpenAI-compatible Provider。最初看起来只要配置一个主模型和几个 fallback 就够了,但实际运行后发现,真正可靠的容灾需要回答三个问题:
- 模型名是否对应真实上游,而不是不透明别名?
- fallback 是否来自不同账号或余额池?
- 目标接口究竟支持普通 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.ts:maintenance.run()的异常处理存在逻辑缺陷——catch 块里恢复 host/runtime 失败会被 finally 吞掉,现改为只在 try 成功路径恢复,finally 只负责清理 maintaining 标志,失败正常抛出。src/core/telegram-runtime.ts:enqueue()的 Promise 链会无限增长,长时间运行数周后会积累大量 Promise 对象。现在任务完成后重置 queue,不再泄漏内存。src/index.ts:onModuleError日志改用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协议,也不是把十个功能强行塞进一个巨型模块。它解决的是个人长期运维中的现实问题:
- 只维持一个账号连接;
- 模块边界清楚;
- 配置和凭据不混入代码;
- 迁移不双开;
- 模型容灾基于真实验证;
- 媒体和备份都有降级与回滚;
- 每次更新都有自动测试和生产验收。
对个人自托管项目而言,功能"能跑"只是起点。真正让人安心的是:知道它为什么不会重复处理,知道故障时会走哪条路径,也知道如何安全恢复到上一个状态。