Repository navigation
feat(driver): 新增 Telegram 网盘驱动 - #118
wumingzhinu wants to merge 2 commits into
Conversation
参考实现:https://github.com/katelya77/K-Vault ## 遇到的核心问题:Telegram 无法列举文件 K-Vault 能把 Telegram 当网盘用,是因为它的上传经由自己的 webhook,因此映射关系 一直握在自己手里。而本驱动要实现完整的 StorageDriver 语义(list/mkdir/rename/ remove/move/copy/put),必须能回答「这个聊天里现在有哪些文件」。Bot API 做不到: - **没有 getMessage** —— 无法按 message_id 回读任意消息; - getUpdates 只保留 24 小时,且**不包含 bot 自己发出的消息**; - 没有任何「列出会话历史消息」的方法。 所以把 manifest 写进聊天是行不通的:写进去就再也读不回来,等于数据不可访问。 K-Vault 自己也是把映射存在数据库里(functions/utils/telegram.js 的 shouldWriteTelegramMetadata),本驱动同理 —— Telegram 只负责存字节, 目录结构由 OpenList 侧维护的索引负责。这同时绕开了聊天消息 4096 字符的文本上限。 ## 索引设计 - 存放位置:DB 的 settings 表,key = `telegram_index_<sha256(bot_token|chat_id)[0:32]>` - 形态:`{ v, dirs: { "<目录路径>": Entry[] } }`,目录路径为 POSIX 绝对路径(根为 "") - 只由 bot_token + chat_id 派生,不含 root_path:root_path 是视图,不该分裂索引; 同一聊天被两个 storage 指向时视为同一份数据,这与 Telegram 的实际语义一致。 - SHA-256 用纯 JS 同步实现:WebCrypto.subtle 是异步的,而 key 需要同步值; 同时不引入 node:crypto(本仓库也跑在 Cloudflare Workers 上)。 - 解析时逐条校验(isValidIndexEntry),损坏或旧版本数据被安全丢弃而不是让驱动崩掉。 ## 下载:不缓存直链,走 createReadStream Telegram 的文件直链有效期至少 1 小时且不可控,存进索引必然过期。因此不返回 raw_url,改实现 createReadStream —— 请求发生时才 getFile 换新鲜直链,并透传 Range(raw.ts 已负责 206 / Content-Range / Content-Length)。 超过 20MB 的文件在 list 阶段就能判定,直接给出 raw_url_error 说明原因。 ## Bot API 硬限制(无法绕过,已写进错误信息) | 操作 | 上限 | 来源 | | --- | --- | --- | | 上传 sendDocument | 50MB | Bot API | | 下载 getFile | 20MB | Bot API,超限不返回 file_path | copy / move 用 copyMessage 在 Telegram 侧复制消息,比「下载再上传」少一个带宽 往返,也不受 20MB 下载上限影响。 ## 其他实现要点 - group → supergroup 迁移:旧的数字 chat_id 失效时,Telegram 会在 parameters.migrate_to_chat_id 给出新 id,客户端自动跟进并重试一次。 - 错误信息绝不回显 bot_token:URL 形如 /bot<token>/method,直接抛 URL 等于把 凭据写进日志。错误只带方法名与 Telegram 的 description,测试对此有断言。 - 所有写操作「单次载入 → 改 → 末尾统一持久化」,避免用陈旧快照覆盖他人写入, 中途失败也不会留下半截状态。 - 删除先落索引再清理 Telegram 消息:索引落地即代表文件不再可见, Telegram 侧清理失败(如消息已被人工删除)不回滚。 - 索引存储通过构造函数注入(IndexStore),默认落 DB;测试注入内存实现。 ## 测试 新增 25 个用例,全部通过: - index.test.ts(10):SHA-256 对照已知向量(含跨 64 字节分组边界与多字节字符)、 indexKey 稳定性与区分性、路径归一、损坏/旧版本/非法条目容错、序列化往返 (含中文与特殊字符)。 - driver.test.ts(15):init、put/list、排序、mkdir/rename/remove、目录改名时 后代路径迁移、递归删除、空目录在 copy 中不丢失、move、root_path 挂载、 同名覆盖不留孤儿、20MB 边界提示、createReadStream 的 Range 透传、 错误信息不含 token。 测试过程中发现并修掉 4 个真实缺陷: 1. rename 未迁移目录自身的 children 键 → 改名后内容为空; 2. rename 先搬走自身键再收集后代 → 后代迁移静默失效; 3. copy 复制单个文件时被目录分支跳过 → 文件从未写入索引; 4. copy / move / remove 把 srcPhysical 当成条目所在目录的**上级**, 而 op/storage.ts 传入的是**父目录**。 ## 未验证 没有真实 Bot Token,全部逻辑基于桩 fetch 验证。上线前请先用真实 bot 验证 init / put / list / 下载 / 删除一轮。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- 移除 editChatDescription / sendMessage / editMessageText:这三个方法来自 最初「manifest 写进聊天」的方案,该方案因 Bot API 无 getMessage 已被否掉 (见 index.ts 的说明),方法随之成为死代码。 - getChat 改为 verifyChatAccess() 并在 init() 调用。此前 driver 完全没用 getChat, 但它是少数会明确回报 chat not found 的方法;放在 init() 里,chat_id 写错或 bot 未被拉进聊天的配置错误能在绑定存储时就暴露,而不是等第一次 list 才报一个 难懂的错。 - 修正 types.ts 顶部已失效的注释(原文称聊天必须是 group/supergroup/channel 以便 用 ChatFullInfo.description 存放 manifest 指针):目录索引现已改存 OpenList 侧 DB,Telegram 只负责存字节,聊天类型不限,与 bot 的私聊同样可用。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
20M感觉意义不是很大,可以参考下列项目支持KWS或者MTProto:
https://github.com/ToiCF/CF-Workers-TGProxy
https://github.com/PIKACHUIM/TGFileWorker
|
不知道,我看b站有项目,但是视频也没说20M,也是cloudflare部署的,我只是让ai移植过来 |
感谢您的贡献,BOT API就是有20M的限制,这也是为什么一开始没有加TG驱动的原因 |
pikachuren
left a comment
There was a problem hiding this comment.
🙏 感谢贡献 PR #118!以下是本次代码审查报告。
总体评价 ✅ Approve ⭐⭐⭐⭐⭐
head commit reviewed: 20b58832a7b5df0ea7dcc54479b22dd3c34e36ea
设计决策完全正确:Telegram Bot API 无法列举消息,因此放弃「拉取历史」思路,改为 OpenList 侧维护目录索引,是唯一可行的路。createReadStream 在请求时实时调用 getFile 取新鲜直链并透传 Range,解决了 Telegram 直链有效期短的根本问题。测试采用构造注入(IndexStore 接口 + fakeFetch)而非全局 Monkey Patch,架构干净,测试可靠性高。
发现的问题
P1 — 生产路径下索引读写缺少并发保护
store.ts 的生产实现采用「读 DB → 修改 → 写回」模式:
export async function saveIndex(key: string, idx: IndexShape, env?: any): Promise<void> {
const db = await getDb(env) // 读最新快照
// ... 修改 settings 条目
await saveDb(db, env) // 整体覆盖写回
}Cloudflare Workers 的并发隔离是「每请求一个 isolate」,两个并发的写操作(例如同时 put 两个文件)都会通过 getDb/saveDb 读出同一版本,后写的会覆盖前写的索引变更,导致文件条目丢失。
建议参考已有的 singleflight(#73 已合并)对索引写操作做进程内合并,或在 saveIndex 中加入类似 saveDb 的版本号校验,检测到冲突时重试。单实例轻度使用下这个问题概率低,但需在 PR 描述或注释中明确记录此限制。
P2 — 索引存储在 settings 表,内容体积无上限
索引 JSON 保存在 db.settings 条目中(group: 99)。文件数量大时(数千条),整个索引会随 getDb / saveDb 的每次调用反复序列化/反序列化,既影响性能,也接近 Workers KV 的单值写入限制(25 MB)。建议在 index.ts 的注释中注明「已知限制:索引体积随文件数线性增长,不建议超过 ~10,000 条目」,或考虑后续分片存储。
P2 — 20MB 下载限制和 50MB 上传限制仅在注释和 raw_url_error 中提示
Telegram Bot API 的 getFile 对 >20MB 文件不返回 file_path,目前通过在 list() 时把 raw_url_error 填入 "超过 20MB" 来告知用户。建议在 put() 时若检测到 buf.byteLength > 50 * 1024 * 1024 提前抛出友好错误,而不是等 Telegram API 返回 Request Entity Too Large。
P2 — bot_token 安全
TelegramApiError 的错误消息来自 TelegramApiClient,该类持有 token 字段但注释明确约定「不回显 token」。测试中也有专门断言(!e.message.includes("123456"))。经核查 util.ts 中 url() 用 token 拼 URL 但只在 fetch 内部使用,未出现在抛出的错误字符串中。✅
测试覆盖确认 ✅
涵盖 15 个核心场景:init、put/list 语义、目录排序、mkdir/rename/remove、递归目录改名、递归删除、root_path 隔离、同名覆盖、非法文件名、copy(含空目录保留)、move、20MB 限制提示、createReadStream Range 透传、错误消息不含 token。测试质量优秀。
🤖 AI 自动审核声明:本评论由 AI 辅助自动化评审生成,重点聚焦功能正确性与安全性;审查不能替代人工判断,最终合并决策请由维护者确认。如发现评审有误,欢迎直接回复。
Summary / 摘要
参考 https://github.com/katelya77/K-Vault 实现,为本仓库新增 Telegram 网盘驱动:
一个 Telegram 聊天作为平铺的消息池,每条文件消息对应一个文件;目录结构由 OpenList 侧
维护的索引记录。实现了完整
StorageDriver语义(list/get/mkdir/rename/remove/move/copy/put/createReadStream)。驱动名:
telegram(别名tg)。最初想照搬 K-Vault 的「manifest 写进聊天」思路,但查 Bot API 文档后发现这条路是死的:
getMessagegetUpdates只保留 24 小时getUpdates不含 bot 自己发出的消息把目录清单写进聊天,就再也读不回来,等于数据不可访问。
K-Vault 能用 Telegram 当网盘,是因为它的上传都经由自己的 webhook,映射关系一直握在
自己手里(见
functions/utils/telegram.js的shouldWriteTelegramMetadata)。本驱动要实现完整文件系统语义,必须能主动回答目录内容,因此改为:Telegram 只负责存字节,
目录结构由 OpenList 侧索引负责。 这同时绕开了聊天消息 4096 字符的文本上限。
🔧 使用前置步骤
1. 创建 bot 并拿到 token
在 Telegram 里找 @BotFather →
/newbot→ 按提示起名,得到形如
123456:AAxxxxxxxxxxxxxxxxxxxxxxxxxxxxx的 token。2. 准备一个聊天,并把 bot 拉进去
推荐私聊或私有频道:bot 发出的每条文件消息对该聊天的所有成员可见,
频道不要设为公开(带
t.me/xxx链接那种),否则等于公开分享文件。Post Messages权限。需在 @Botfather 的
/setprivacy→Disable关闭隐私模式)。3. 拿到 chat_id
三种情况任选:
@mychannel@username-1002442994673(超级群/频道带-100前缀)数字 id 可用 @userinfobot 或自己调一次
https://api.telegram.org/bot<token>/getUpdates查看。id 写错的后果:驱动在
init()阶段就报chat not found,不会等第一次 list 才莫名其妙失败。4. 在 OpenList 添加存储
bot_token123456:AA...chat_id@usernameapi_basehttps://api.telegram.orgroot_path/vault。属于视图层,同一直驱可多个 storage 各自设不同值绑定存储时会调用
getMe+getChat校验凭据与聊天可达性。📦 硬限制(Bot API 规定,无法绕过)
sendDocumentput时直接抛错getFilegetFile不返回file_path超过 20 MB 的文件在
list阶段就能判定,FileItem.raw_url_error会直接写明原因,而不是等到下载才失败。
copy/move用copyMessage在 Telegram 侧复制消息,比「下载再上传」省一个带宽往返,也不受 20 MB 下载上限影响。
🏗 实现要点
索引设计
settings表,key =telegram_index_<sha256(bot_token|chat_id)[0:32]>{ v, dirs: { "<目录路径>": Entry[] } },目录路径为 POSIX 绝对路径(根为"")bot_token + chat_id派生,不含root_path:root_path是视图层,不该分裂索引;同一聊天被两个 storage 指向时视为同一份数据 —— 这与 Telegram 的实际语义一致,
因为消息本来就只存在于那一个聊天里。
subtle是异步的而 key 需要同步值;同时不引入
node:crypto(本仓库也跑在 Cloudflare Workers 上)。isValidIndexEntry),损坏或旧版本数据被安全丢弃而非让驱动崩掉。下载:不缓存直链,走
createReadStreamTelegram 文件直链有效期至少 1 小时且不可控,存进索引必然过期。因此不返回
raw_url,改实现createReadStream—— 请求发生时才getFile换新鲜直链,并透传Range(raw.ts已负责206/Content-Range/Content-Length)。其他
parameters.migrate_to_chat_id给出新 id,客户端自动跟进并重试一次。/bot<token>/method,直接抛 URL 等于把凭据写进日志。错误只带方法名与 Telegram 的
description,测试对此有断言。(如消息已被人工删除)不回滚。
IndexStore),默认落 DB;测试注入内存实现。✅ 测试
25 个用例全部通过:
index.test.ts(10)— SHA-256 对照已知向量(含跨 64 字节分组边界与多字节字符)、indexKey稳定性与区分性、路径归一、损坏/旧版本/非法条目容错、序列化往返(含中文与特殊字符)。driver.test.ts(15)— init 凭据校验、put/list、目录优先排序、mkdir/rename/remove、目录改名时后代路径迁移、递归删除、空目录在 copy 中不丢失、move、root_path 挂载、
同名覆盖不留孤儿、20MB 边界提示、
createReadStream的 Range 透传、错误信息不含 token。测试过程中发现并修掉 4 个真实缺陷:
rename未迁移目录自身的 children 键 → 改名后内容变空;rename先搬走自身键再收集后代 → 后代迁移静默失效;copy复制单个文件时被目录分支跳过 → 文件从未写入索引;copy/move/remove把srcPhysical当成条目所在目录的上级,而op/storage.ts的调用点传入的是父目录。第 4 个是接口语义错误,靠读
op/storage.ts的调用点才确认。🔗 参考项目
api_base归一化、按 mime 选上传方法、sendAudio失败回落sendDocument的经验均来自server/lib/storage/adapters/telegram.jstext1-4096 字符、ChatFullInfo.description0-255 字符,并确认不存在getMessage参考项目的已知问题(本驱动已规避):K-Vault 是扁平存储、无目录概念,且必需
经由自己的 webhook 才能拿到文件清单;直接照搬到 OpenList 的文件系统语义下不可行。
⚡ 未验证 / 注意事项
fetch验证。 上线前请用真实 bot 跑一轮init/put/list/ 下载 / 删除。不要放在公开频道 / 公开群,那等于公开分享文件。敏感数据请配合 meta 密码。
crypt驱动。文件清单