---
title: "消息信封、交付、恢复与错误"
canonical_url: "https://tweetstream.io/zh-cn/docs/envelope-delivery-recovery-errors"
last_updated: 2026-09-24
---

# 消息信封、交付、恢复与错误

按类别和操作分发 v1 消息信封，容忍重复，并在支持回放时使用已存 History。

Canonical URL: https://tweetstream.io/zh-cn/docs/envelope-delivery-recovery-errors

## 分发消息信封

每个实时消息信封先按 `t` 分发，再按 `op` 分发。处理程序必须幂等，因为历史回放可能返回已实时处理的事件。

- [打开完整分发器](https://tweetstream.io/zh-cn/docs/production-consumer#complete-router): 所有载荷类型，以及覆盖全部服务端操作的完整 `switch`。

### 消息信封基础类型

```typescript
type EnvelopeBase<
  TFamily extends 'tweet' | 'account' | 'charity' | 'control' | 'news',
  TOp extends string,
  TPayload extends object,
> = {
  v: 1;
  t: TFamily;
  op: TOp;
  id?: string;
  ts: number;
  d: TPayload;
};
```

## 分发操作

TweetStream 发送 18 种服务端操作。先按 `t`、再按 `op` 分发每个消息信封；遇到未知操作时忽略该消息，不要关闭连接。客户端发送两种命令：`auth_ping` 用于认证检查，`twitter_handles` 用于在 handle-management 连接上添加或移除账号。

| 类别 | 操作 | 含义 |
| --- | --- | --- |
| `tweet` | `content` | 帖子内容 |
| `tweet` | `meta` | 帖子富化 |
| `tweet` | `update` | 帖子更新 |
| `tweet` | `delete` | 帖子删除 |
| `tweet` | `pin` | 帖子置顶 |
| `tweet` | `unpin` | 帖子取消置顶 |
| `account` | `profile_update` | 资料更新 |
| `account` | `follow` | 关注账号 |
| `account` | `unfollow` | 取消关注账号 |
| `account` | `affiliate_update` | 关联账号变更 |
| `account` | `callout` | 代币喊单 |
| `account` | `callout_update` | 喊单富化 |
| `account` | `thesis` | FOMO 观点 |
| `account` | `token_verified` | 观察到 FOMO 代币认证 |
| `charity` | `added` | 新增慈善项目 |
| `news` | `article` | 新闻文章 |
| `control` | `auth_pong` | 认证 pong |
| `control` | `twitter_handles_result` | 账号管理命令结果 |

## 按事件流处理交付

消息信封没有确认、恢复 token 或序列号。不要根据 `ts` 或 `id` 推断顺序或恰好一次交付。

处理程序必须幂等，并保留足够的本地状态，以识别应用已经处理的事件。

## 重连后回补

固定 `startDate` 和 `endDate`，在其他筛选条件不变的情况下持续使用 `nextCursor`，直到返回 null。以幂等方式处理每行。如果中断后的回放需要继续而不是重启，请一起保存时间范围和 cursor。

History 返回已存事件，不保证完整性或保留期限。`AFFILIATE` 返回已存的关联账号变更，不是当前列表快照。delete、pin 和 unpin 仍只在实时流中发送。

### 回放恢复窗口

```typescript
type HistoryRow = {
  time: string;
  tweetId: string;
};

type HistoryPage = {
  data: Array<HistoryRow>;
  metadata: { nextCursor: string | null };
};

async function replayAfterReconnect(
  checkpoint: string,
  processEvent: (event: HistoryRow) => Promise<void>,
  saveCheckpoint: (eventTime: string) => Promise<void>,
) {
  const apiKey = process.env.TWEETSTREAM_API_KEY;
  if (!apiKey) throw new Error("Missing TWEETSTREAM_API_KEY");

  const url = new URL("https://api.tweetstream.io/api/history");
  const replayEnd = new Date().toISOString();
  url.searchParams.set("handles", "marketdesk");
  url.searchParams.set("startDate", checkpoint);
  url.searchParams.set("endDate", replayEnd);
  url.searchParams.set("limit", "1000");
  url.searchParams.set("type", "TWEET");

  const pages: Array<Array<HistoryRow>> = [];
  let cursor: string | null = null;
  do {
    if (cursor) url.searchParams.set("cursor", cursor);
    const response = await fetch(url, {
      headers: { Authorization: `Bearer ${apiKey}` },
    });
    if (!response.ok) {
      const detail = await response.text();
      throw new Error(`Replay failed (${response.status}): ${detail}`);
    }

    const page = (await response.json()) as HistoryPage;
    pages.push(page.data);
    cursor = page.metadata.nextCursor;
  } while (cursor);

  for (const event of pages.reverse().flatMap((page) => [...page].reverse())) {
    await processEvent(event);
  }
  await saveCheckpoint(replayEnd);
}
```

## 修复握手错误

WebSocket 连接升级失败时，会在连接被接受前返回 JSON。客户端可以安全记录该响应。

| 状态 | 可能原因 | 修复方式 |
| --- | --- | --- |
| `400` | WebSocket 协议 header 缺失或无效 | 发送 `tweetstream.v1` 和有效认证 token 协议 |
| `401` | API 密钥缺失、为空或无效 | 在控制台重新生成或轮换 API 密钥 |
| `403` | 订阅状态不允许实时流 | 开始试用、续费或升级账号 |
| `429` | 活跃 WebSocket 连接数达到限制 | 关闭旧连接或升级套餐 |
| `503` | 连接限制服务暂时不可用 | 使用退避策略重试 |

### 读取连接升级拒绝响应

```json
{
  "error": "WebSocket connection limit reached (3/3 active). Close an existing connection and retry.",
  "status": 429
}
```
