mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-20 23:03:13 +08:00
217 lines
8.4 KiB
Markdown
217 lines
8.4 KiB
Markdown
# Issue #85:Telegram 长轮询阻塞发送最小修复方案
|
||
|
||
日期:2026-08-30。代码基线:v4.1.0 / `e08c3e9`。状态:待实施。
|
||
|
||
需求来源:[Issue #85](https://github.com/xmanrui/dsh-im/issues/85)。本文只定义关闭该 Issue 所需的最小改动;列出的测试均为待补测试。
|
||
|
||
## 1. 决策
|
||
|
||
为每个 Telegram Runtime 创建一个私有、代理感知、最多 4 条同源连接的 Undici dispatcher,并让该 Bot 的长轮询和全部 Telegram HTTP 请求使用它。
|
||
|
||
同时满足:
|
||
|
||
- `fetch`、dispatcher 和 `FormData` 来自同一个固定版本的 `undici`。
|
||
- Runtime 在正常停止和启动失败时释放 dispatcher。
|
||
- 不修改进程全局 dispatcher。
|
||
- 不增加发送重试。
|
||
- 不修改现有超时和 `CHANNEL_DELIVERY_UNCERTAIN` 语义。
|
||
|
||
本次不重构生命周期、不建设通用 Transport 框架、不顺带处理其他 hardening 项目。
|
||
|
||
## 2. 原因
|
||
|
||
当前错误链路是:
|
||
|
||
1. `TelegramRuntime` 使用默认全局 `fetch` 创建 `TelegramApi`。
|
||
2. `getUpdates(timeout: 25)` 与消息发送、草稿更新和文件请求并发。
|
||
3. 报告环境的全局 dispatcher、代理或连接路径把同源请求串行化。
|
||
4. 发送请求排在长轮询后,先触发 15 秒超时。
|
||
5. Telegram API 将超时标记为 `telegram-timeout` 和 `deliveryOutcome = unknown`。
|
||
6. 上层正确转换为 `CHANNEL_DELIVERY_UNCERTAIN`。
|
||
|
||
Issue 中“Node 默认 Undici 只有一条连接”不是普遍成立的默认行为。独立 Agent 能恢复发送,只能证明问题位于实际 HTTP 传输路径。修复目标是让 Telegram 不再依赖进程全局传输状态。
|
||
|
||
## 3. 范围
|
||
|
||
本次包含:
|
||
|
||
- Telegram Bot API 长轮询和出站请求。
|
||
- Telegram 文件上传和平台文件下载。
|
||
- 每个 Runtime 私有的 dispatcher。
|
||
- `HTTP_PROXY`、`HTTPS_PROXY`、`NO_PROXY` 及对应小写变量。
|
||
- `TelegramApi` 的 `FormData` 注入点。
|
||
- 正常停止和启动失败时的资源释放。
|
||
- 核心回归、真实 multipart、基本资源所有权和发布包测试。
|
||
|
||
本次不包含:
|
||
|
||
- `start()` / `stop()` generation 或状态机重构。
|
||
- poll 崩溃后的新清理框架;继续由现有 supervisor 重连并调用 `stop()`。
|
||
- `inspectTelegramToken()` 的网络策略调整。
|
||
- Token、代理凭据和嵌套错误的通用脱敏框架。
|
||
- 双连接池、请求优先级或可配置连接数。
|
||
- 其他 IM 渠道改造。
|
||
|
||
## 4. 最小实现
|
||
|
||
### 4.1 依赖
|
||
|
||
增加精确运行时依赖 `undici: 7.29.0`。该版本满足项目 Node `>=22.19` 的运行条件。
|
||
|
||
`undici` 必须作为 Host bundle 的外置运行时依赖,避免源码环境可用但发布包无法解析,或被 esbuild 打入 bundle 后产生另一份实现。
|
||
|
||
### 4.2 Telegram HTTP 工厂
|
||
|
||
新增 `src/channels/telegram/telegram-http.mjs`:
|
||
|
||
```js
|
||
import {
|
||
EnvHttpProxyAgent,
|
||
FormData as UndiciFormData,
|
||
fetch as undiciFetch,
|
||
} from 'undici';
|
||
|
||
export function createTelegramHttpTransport() {
|
||
const dispatcher = new EnvHttpProxyAgent({ connections: 4 });
|
||
let destroyed = false;
|
||
|
||
return {
|
||
fetchImpl: (url, options = {}) => undiciFetch(url, {
|
||
...options,
|
||
dispatcher,
|
||
}),
|
||
FormDataImpl: UndiciFormData,
|
||
async destroy() {
|
||
if (destroyed) return;
|
||
destroyed = true;
|
||
await dispatcher.destroy();
|
||
},
|
||
};
|
||
}
|
||
```
|
||
|
||
约束:
|
||
|
||
- 使用 `EnvHttpProxyAgent`,不使用会绕过标准代理变量的裸 `Agent`。
|
||
- `dispatcher` 写在 `...options` 后,调用方不能覆盖它。
|
||
- 不调用 `setGlobalDispatcher()`。
|
||
- 不调整 keep-alive 参数,不记录包含 Bot Token 的请求 URL。
|
||
|
||
### 4.3 TelegramApi
|
||
|
||
构造函数增加 `FormDataImpl = globalThis.FormData`,保存为私有字段;文件上传从 `new FormData()` 改为 `new this.#FormDataImpl()`。
|
||
|
||
Runtime 注入配套的 `undici.fetch + undici.FormData`。其他直接构造 `TelegramApi` 的调用继续默认使用全局 `fetch + FormData`,保持兼容。
|
||
|
||
不修改请求 payload、超时、redirect、文件限制和错误映射。
|
||
|
||
### 4.4 TelegramRuntime
|
||
|
||
增加 `#createHttpTransport` 和 `#httpTransport`:
|
||
|
||
- 构造函数默认 `createHttpTransport = createTelegramHttpTransport`。
|
||
- `#start()` 在 `createApi` 前创建并登记 transport。
|
||
- `createHttpTransport` 和 `createApi` 必须位于启动错误处理范围内。
|
||
- `createApi` 接收 `fetchImpl` 和 `FormDataImpl`。
|
||
|
||
`stop()` 只增加以下清理:
|
||
|
||
1. 捕获并清空当前 `#httpTransport`。
|
||
2. 沿用现有 abort 及 poll/bridge 有界等待。
|
||
3. 调用捕获 transport 的幂等 `destroy()`。
|
||
4. 销毁失败只记录固定 warning,不让 `stop()` 抛错或覆盖原始启动错误。
|
||
|
||
本次不改变现有 `start()` / `stop()` 状态机;既有启动取消竞态留给后续 hardening。
|
||
|
||
### 4.5 保持不变
|
||
|
||
- 普通发送默认 15 秒超时。
|
||
- `getUpdates` 为 25 秒长轮询并保留现有 10 秒余量。
|
||
- 文件上传默认 120 秒超时。
|
||
- timeout 继续映射为 `telegram-timeout + deliveryOutcome: unknown`。
|
||
- 上层继续产生 `CHANNEL_DELIVERY_UNCERTAIN`。
|
||
- 最终消息、编辑和文件上传均不自动重试。
|
||
|
||
## 5. 文件改动
|
||
|
||
| 文件 | 改动 |
|
||
| --- | --- |
|
||
| `package.json`、`package-lock.json` | 增加并锁定 `undici@7.29.0` |
|
||
| `src/channels/telegram/telegram-http.mjs` | 新增私有 transport 工厂 |
|
||
| `src/channels/telegram/telegram-api.mjs` | 注入匹配的 `FormDataImpl` |
|
||
| `src/channels/telegram/telegram-runtime.mjs` | 创建、注入和释放 transport |
|
||
| `plugin-src/host/build.mjs` | 外置 `undici` |
|
||
| `scripts/verify-package.mjs` | 校验直接依赖和 bundle 外置 |
|
||
| `test/channels/telegram/telegram-http.test.mjs` | 核心传输回归 |
|
||
| `test/channels/telegram/telegram.test.mjs` | API 和 Runtime 最小测试 |
|
||
| `THIRD_PARTY_NOTICES.md`、`CHANGELOG.md` | 许可证和修复记录 |
|
||
| `lib/index.js` | 通过现有构建命令生成 |
|
||
|
||
## 6. 最小测试集
|
||
|
||
### 6.1 长轮询不阻塞发送
|
||
|
||
使用本地 server 和真实 transport:保持 `/getUpdates` 不返回,再调用 `sendChatAction`,断言发送在释放长轮询前已经完成。使用事件顺序断言,不设置严格耗时阈值。
|
||
|
||
测试须隔离大小写两套代理环境变量,避免 CI 环境代理污染本地请求。
|
||
|
||
### 6.2 不依赖全局 dispatcher
|
||
|
||
在独立子进程中把全局 dispatcher 限制为一条连接并占住它,断言 Telegram 私有 transport 的请求仍能完成。
|
||
|
||
### 6.3 真实 multipart
|
||
|
||
通过真实 `undici.fetch` 向本地 server 执行一次 `sendDocument`,验证 multipart boundary、`chat_id`、文件名、媒体类型和二进制内容。其他 payload 继续由现有 fake fetch 测试覆盖。
|
||
|
||
### 6.4 基本资源所有权
|
||
|
||
通过 fake transport 覆盖四种情况:
|
||
|
||
- 正常 `start → stop` 只销毁一次。
|
||
- 连续两次 `stop()` 不重复销毁。
|
||
- `getMe` 失败会销毁并保留原始错误。
|
||
- `createApi` 抛错仍会销毁。
|
||
|
||
不在本次增加启动取消、重启代际和 poll 崩溃竞态矩阵。
|
||
|
||
### 6.5 现有语义与发布包
|
||
|
||
复用现有测试确认 uncertain 错误语义和“不发送 fallback 副本、不重试”保持不变,然后执行:
|
||
|
||
```text
|
||
npm run check
|
||
npm pack
|
||
在空临时目录安装 tarball
|
||
导入 lib/index.js
|
||
通过发布包执行一次本地 Telegram transport 请求
|
||
```
|
||
|
||
## 7. 验收标准
|
||
|
||
满足以下条件即可关闭 Issue #85:
|
||
|
||
- `/getUpdates` 未返回时,短 Telegram API 请求仍能先完成。
|
||
- Telegram transport 不受受限全局 dispatcher 影响。
|
||
- `sendDocument` 通过真实 multipart 成功。
|
||
- 正常停止、重复停止和启动失败不会重复销毁或遗留 transport。
|
||
- 标准代理变量仍由 Undici 官方代理实现处理。
|
||
- 现有 timeout、unknown delivery 和 `CHANNEL_DELIVERY_UNCERTAIN` 语义不变。
|
||
- 没有新增最终消息或文件上传重试。
|
||
- 完整构建、测试和干净发布包安装通过。
|
||
|
||
## 8. 后续加固
|
||
|
||
以下内容单独建 issue,不阻塞 #85:
|
||
|
||
- Runtime 生命周期串行化或 generation 机制。
|
||
- Telegram Token、代理凭据和 transport error 统一脱敏。
|
||
- `inspectTelegramToken()` 使用同一网络策略。
|
||
- poll 崩溃后的即时资源回收。
|
||
- 多聊天、大文件和短消息混合负载评估。
|
||
|
||
## 9. 参考资料
|
||
|
||
- [Issue #85](https://github.com/xmanrui/dsh-im/issues/85)
|
||
- [Undici EnvHttpProxyAgent v7.29.0](https://github.com/nodejs/undici/blob/v7.29.0/docs/docs/api/EnvHttpProxyAgent.md)
|
||
- [Undici Fetch v7.29.0](https://github.com/nodejs/undici/blob/v7.29.0/docs/docs/api/Fetch.md)
|
||
- [Node.js fetch 自定义 dispatcher](https://nodejs.org/dist/latest/docs/api/globals.html#custom-dispatcher)
|