内容
# telegram-rest-gateway
自托管的REST/WebSocket包装器,基于**TDLib**(原生C++接口,`td::ClientManager`)和**Drogon**构建。为账户提供语言无关的HTTP访问,隐藏TDLib的异步特性,并通过WebSocket提供更新。
> 状态:**阶段0(框架)**。桥接器/授权/消息功能正在开发中,按照阶段进行(参见 `docs/SPEC_ELABORATION.md` §12)。仅支持Docker/CI构建。
## 文档
| 文件 | 用途 |
|---|---|
| [`TZ_TDLib_Drogon_REST.md`](TZ_TDLib_Drogon_REST.md) | 主要技术要求(v2.0) |
| [`docs/SPEC_ELABORATION.md`](docs/SPEC_ELABORATION.md) | 实现级别的详细说明(v3.0) |
| [`docs/DECISIONS.md`](docs/DECISIONS.md) | 已接受的决策日志 |
| [`docs/GITFLOW.md`](docs/GITFLOW.md) | 分支模型 |
| [`openapi/openapi.yaml`](openapi/openapi.yaml) | HTTP合约的单一真实来源 |
## 技术栈和关键决策
- **C++20**(Drogon的协程),TDLib使用C++17编译。
- **TDLib静态链接**,通过`Td::TdStatic` - 原生C++ API不导出`libtdjson.so`。
- 统一的 **OpenSSL 3.0**,用于TDLib、Drogon和我们的代码。
- 单账户,只使用WebSocket进行更新,TDLib数据库加密,JSON中的ID为字符串。
- 部署:多架构(amd64 + arm64)Docker,最终目标是无root的distroless。
## 结构
```
src/
main.cpp — 入口点,/v1/health,--healthcheck
bridge/
transport.hpp — ITdTransport(td::ClientManager的缝隙)
real_transport.* — RealTdTransport(实际TDLib的唯一点)
clock.hpp — 时间注入(SteadyClock / FakeClock在测试中)
tests/
fake_transport.hpp — FakeTdTransport(脚本化,线程安全)
transport_smoke_test.cpp
openapi/openapi.yaml
Dockerfile.builder — 构建镜像(TDLib + Drogon)
Dockerfile — 服务镜像(FROM builder)→ distroless
```
CI/CD - GitHub Actions ([`.github/workflows/ci.yml`](.github/workflows/ci.yml)):lint(clang-format),抑制卫士,可缓存的GHCR构建镜像(TDLib + Drogon),ASan / TSan下的测试,clang-tidy和Release服务构建。有关旧管道变量的历史说明,请参见[`docs/CICD.md`](docs/CICD.md)。
## 构建(在容器中)
```bash
# 1. 固定TDLIB_REF(完整的git SHA!)和DROGON_REF。
# 2. 构建构建镜像(耗时;仅在更改时执行):
docker build -f Dockerfile.builder \
--build-arg TDLIB_REF="$(grep -oP '(?<=^TDLIB_REF=).*' TDLIB_REF)" \
--build-arg DROGON_REF="$(grep -oP '(?<=^DROGON_REF=).*' DROGON_REF)" \
-t tgw-builder:local .
# 3. 构建服务:
docker build --build-arg BUILDER_IMAGE=tgw-builder:local -t telegram-rest-gateway .
```
在Docker之外进行本地构建需要安装TDLib(`Td::TdStatic`),Drogon和OpenSSL 3.0;配置通过`CMakePresets.json`(`cmake --preset dev-debug`)进行。
## 配置(环境变量)
| 变量 | 默认值 | 用途 |
|---|---|---|
| `TGW_LISTEN_ADDRESS` | `127.0.0.1` | 监听地址(在Docker中为`0.0.0.0`) |
| `TGW_LISTEN_PORT` | `8080` | HTTP端口 |
| `TGW_SESSION` / `TGW_SESSION_FILE` | — | 会话字符串(`td.binlog`的base64编码),用于无状态启动 |
| `BEARER_TOKENS` | — | 按字符串划分的令牌:`<token>[ <scopes>]`;作用域:`read`,`write`,`admin`(无作用域 = 所有) |
| `TGW_WS_MAX_PENDING_BYTES` | `8388608` | WS背压:上次pong的字节限制;0 - 禁用 |
| `TGW_MAX_MEMORY_BODY_BYTES` | `1048576` | 请求体磁盘存储阈值(上传时的RSS) |
| `TGW_KAFKA_BROKERS` | — | Kafka / Redpanda启动;空 - Kafka事件已禁用 |
| `TGW_KAFKA_TOPIC` | `tgw.updates` | 事件主题 |
| `TGW_KAFKA_CLIENT_ID` | `tgw-<session_id>` | 生产者client.id |
### 可选地将会话存储在S3 / MinIO中
如果设置了启动时从S3(如果本地不存在)拉取`td.binlog`,并在关闭时定期和正常关闭时将其推回。允许在不挂载卷的情况下完全无状态地运行服务。仅当bucket + credentials + endpoint填充时启用。
| 变量 | 默认值 | 用途 |
|---|---|---|
| `TGW_S3_ENDPOINT` | — | `http(s)://host[:port]`(AWS:`https://s3.<region>.amazonaws.com`) |
| `TGW_S3_REGION` | `us-east-1` | 签名SigV4的区域 |
| `TGW_S3_BUCKET` | — | 存储桶名称 |
| `TGW_SESSION_ID` | `default` | 实例标签→ S3路径(段`[A-Za-z0-9._-]`) |
| `TGW_S3_PREFIX` | `telegram-sessions` | 键前缀 |
| `TGW_S3_KEY` | *(派生)* | 显式键覆盖;否则`<prefix>/<session_id>/td.binlog` |
| `TGW_S3_ACCESS_KEY_ID` / `_FILE` | — | 访问密钥 |
| `TGW_S3_SECRET_ACCESS_KEY` / `_FILE` | — | 秘密密钥 |
| `TGW_S3_PATH_STYLE` | `true` | `true` - path-style(MinIO);`false` - virtual-host(AWS) |
| `TGW_S3_SYNC_INTERVAL_SECONDS` | `300` | 会话背景备份到S3的间隔 |
**单个存储桶上的多个帐户。** 每个实例根据`TGW_SESSION_ID`获得自己的路径:`telegram-sessions/<session_id>/td.binlog`。为每个帐户分配不同的`TGW_SESSION_ID`(在登录之前无法访问Telegram account_id,因此由操作员分配)。不能同时在两个实例上运行相同的`session_id` - Telegram将杀死会话(`AUTH_KEY_DUPLICATED`)。
**作用域令牌。** 字符串`BEARER_TOKENS`格式为`tgw_xxx read`,提供仅限于读取(GET + WS)的令牌,`read,write` - 还支持突变,但不包括`/v1/auth/*`(登录和会话导出仅限`admin`/完整令牌)。作用域不足→`403 INSUFFICIENT_SCOPE`。为代理(MCP)提供最小必要的作用域。
秘密(`api_id`,`api_hash`,`database_encryption_key`,Bearer令牌,S3凭证)- 仅通过`*_FILE`/秘密管理器,从不直接进入镜像/环境变量。
## Kafka事件
如果设置了`TGW_KAFKA_BROKERS`,则allowlist中的每个更新(与WS中的相同)都会发布到主题`TGW_KAFKA_TOPIC`。消息体的格式与WS帧相同(`type/update_type/seq/session_id/data`);**消息键为`<session_id>:<chat_id>`**(帐户ID前缀;聊天顺序保证分区)。至少一次交付:消费者根据`(session_id, seq)`进行去重;`seq`中的间隙=丢失(参见`tgw_kafka_dropped_total`)。生产者永远不会阻塞Telegram更新的接收:当队列满时,事件会被丢弃并记录指标。
## 存档器:对话搜索
[`archiver/`](archiver/README.md) - Kafka事件的消费者→SQLite + FTS5:对保存的整个历史记录进行全文搜索(`GET :8090/search?q=`),包括编辑和删除的消息。代理可以使用MCP工具`telegram_search_history`访问它。
## MCP:代理连接器
[`mcp/`](mcp/README.md) - MCP服务器(TypeScript,stdio):Claude Code / Desktop和任何MCP客户端都可以使用14种帐户工具(消息,反应,解析,媒体)。一个服务器=一个帐户;令牌=完全访问权限,请像秘密一样存储。
## Kubernetes部署(Helm)
图表:[`deploy/helm/telegram-rest-gateway`](deploy/helm/telegram-rest-gateway)。功能:一个会话=一个Pod(**策略 recreate,不能扩展副本** - 否则`AUTH_KEY_DUPLICATED`),无PVC(S3中的会话),`/v1/health`就绪检查(未授权的Pod应通过`/ui`接受登录流量)。秘密通过`existingSecret`。
```bash
kubectl create secret generic telegram-rest-gateway \
--from-literal=API_ID=... --from-literal=API_HASH=... \
--from-literal=DATABASE_ENCRYPTION_KEY=... --from-literal=BEARER_TOKENS=... \
--from-literal=TGW_S3_ACCESS_KEY_ID=... --from-literal=TGW_S3_SECRET_ACCESS_KEY=...
helm install tgw deploy/helm/telegram-rest-gateway \
--set 'accounts[0].sessionId=<account_id>'
```
## 指标
`GET /metrics` - Prometheus文本格式(无Bearer令牌):授权状态,WS订阅者,桥接请求,HTTP/更新计数器。
## Web登录界面
`GET /ui` - 自包含登录页面(无Bearer令牌过滤器;令牌在表单中输入):选择**二维码**(TDlib链接自动刷新)或**电话**(号码→代码→2FA)。授权后显示帐户。
## OpenAPI
所有端点的规范:[`docs/openapi.yaml`](docs/openapi.yaml)。
## 许可证
[MIT](LICENSE)。