# Chat API 参考文档：会话、消息、已读回执与事件

> 如何调用 Hibira Chat API：签名请求、会话与成员、消息、补齐漏掉的消息、已读回执与未读数、给浏览器用的用户 Token、附件、推送、内容审核、事件与限制。

Source: https://hibira.io/zh-cn/chat-api

## 开始之前

Chat API 会保存会话与其中的消息，并通过你的 App 已经创建的实时连接送达。由你的服务器决定每个会话有哪些人并调用 API；客户端只需要连接、登录并接收。

- **目前为 Beta。** 账号加入 Beta 或购买 Chat 附加包后即可使用。在此之前，每个请求都会返回 `403 the Chat API is not enabled for this app`。[加入候补名单](https://hibira.io/zh-cn/chat)即可申请。
- **Base URL：** `https://ws.hibira.io/apps/APP_ID/chat`（请使用控制台 App 页面上显示的主机）。
- **内容**为 JSON。错误以 4xx 或 5xx 状态码返回 `{"error": "..."}`。
- **用户 ID** 由你决定：使用与客户端传给 `pusher.signin()` 相同的 ID。

## 工作原理

1. 你的服务器创建会话及其成员（`PUT /conversations/{id}`）。
2. 每个客户端用 pusher-js 连接，并以自己的用户身份调用 `pusher.signin()`。这会订阅 `#server-to-user-<user_id>`，所以成员不需要为每个会话做频道授权。
3. 你的服务器代替用户发送消息（`POST /conversations/{id}/messages`）。Hibira 会以会话的下一个序号（`seq`）保存，接着把 `chat.message` 送给每位成员，并调用你的 `chat_message` Webhook。
4. 离线过的客户端用 `GET /conversations/{id}/messages?after=<手上最后的 seq>` 读回漏掉的消息。

每个变更都会先保存，再送出事件。如果 App 超过每日消息额度，变更仍会保存但不会送出事件，客户端可以读取补齐。

## 签名请求

来自你服务器的请求，与 Pusher HTTP API 的其他请求一样以 App key 与 secret 签名：查询字符串带有 `auth_key`、`auth_timestamp`、`auth_version=1.0`、`body_md5`（有内容时）与 `auth_signature`，也就是对 `METHOD\nPATH\nQUERY`（查询参数依名称排序）计算的 HMAC-SHA256。任何 Pusher 服务器库的签名代码都能用；以下是 Node.js 的完整写法：

```js
import crypto from 'node:crypto';

const { PUSHER_APP_ID: id, PUSHER_APP_KEY: key, PUSHER_APP_SECRET: secret } = process.env;

export async function chat(method, path, body) {
  const fullPath = `/apps/${id}/chat${path}`;
  const json = body === undefined ? '' : JSON.stringify(body);
  const params = { auth_key: key, auth_timestamp: Math.floor(Date.now() / 1000), auth_version: '1.0' };
  if (json) params.body_md5 = crypto.createHash('md5').update(json).digest('hex');
  const query = Object.keys(params).sort().map((k) => `${k}=${params[k]}`).join('&');
  const signature = crypto.createHmac('sha256', secret).update(`${method}\n${fullPath}\n${query}`).digest('hex');
  const res = await fetch(`https://ws.hibira.io${fullPath}?${query}&auth_signature=${signature}`, {
    method, body: json || undefined, headers: { 'Content-Type': 'application/json' },
  });
  if (!res.ok) throw new Error(`${res.status} ${(await res.json()).error}`);
  return res.json();
}

await chat('PUT', '/conversations/order-1042', { name: 'Order #1042', members: ['ann', 'support-bob'] });
await chat('POST', '/conversations/order-1042/messages', { user_id: 'ann', name: 'Ann', text: 'Where is my parcel?' });
```

`GET` 的查询参数（例如 `after` 或 `limit`）也包含在签名的查询字符串中。

## 会话与成员

| 方法与路径 | 内容或查询参数 | 返回 |
|---|---|---|
| `PUT /conversations/{id}` | `name`、`metadata`、`members`（用户 ID；省略则保留现有成员）、`roles`（`{user_id: role}`）、`watchers`（最多 10 个 private 或 presence 频道） | 新建 `201`、取代 `200`，`{conversation}` |
| `GET /conversations/{id}` | | `{conversation}` |
| `DELETE /conversations/{id}` | | `{}`；一并删除成员、消息与文件 |
| `POST /conversations/{id}/members` | `user_ids` | `{conversation}` |
| `PUT /conversations/{id}/members/{user_id}` | `role`（1–32 个 `a-z 0-9 _ -`；`""` 表示无） | `{conversation}` |
| `DELETE /conversations/{id}/members/{user_id}` | | `{conversation}` |
| `GET /users/{user_id}/conversations` | `limit`（1–100）、`before`（上一页的 `next`） | `{conversations, next}`，依最新动态排序，每笔含 `unread` |

会话：

```json
{"id": "order-1042", "name": "Order #1042", "metadata": {"kind": "support"},
 "members": [{"user_id": "ann", "read_seq": 3}, {"user_id": "support-bob", "read_seq": 1, "role": "agent"}],
 "last_seq": 3, "created_at": "...", "updated_at": "..."}
```

**角色与内部备注。** 带有 `visible_to`（最多 5 个角色）的消息只会送到拥有其中一个角色的成员：其他人不会收到、读到，也不会算入未读。**Watchers** 是会收到该会话所有事件的 private 或 presence 频道，让团队收件匣用一个订阅追踪所有会话。请只授权你的员工订阅。

## 消息

| 方法与路径 | 内容或查询参数 | 返回 |
|---|---|---|
| `POST /conversations/{id}/messages` | `user_id`（成员）、`name`、`text`、`metadata`、`push`、`push_title`、`push_url`、`visible_to`、`attachments` | `201 {message, push}` |
| `GET /conversations/{id}/messages` | `after`（seq）与 `limit`，或 `last`（最新 n 则） | `{messages, last_seq}`，由旧到新 |
| `GET /conversations/{id}/search` | `q`（1–100 字符）、`before`、`limit`（1–200） | `{messages, next}`，由新到旧 |
| `PUT /conversations/{id}/messages/{seq}` | `user_id`（发送者）、`text`、`metadata` | 含 `edited_at` 的 `{message}` |
| `DELETE /conversations/{id}/messages/{seq}` | 查询参数 `user_id`（发送者）；省略则以 App 身份删除 | 含 `deleted_at` 的 `{message}` |
| `POST /conversations/{id}/messages/{seq}/reactions` | `user_id`、`emoji` | `{message}` |
| `DELETE /conversations/{id}/messages/{seq}/reactions` | 查询参数 `user_id`、`emoji` | `{message}` |
| `POST /conversations/{id}/system` | `text`、`metadata`（例如 `{"event": "transferred", "to": "bob"}`） | `kind: "system"` 的 `201 {message}` |
| `POST /conversations/{id}/typing` | `user_id`、`typing`（`true` 或 `false`） | `{}`；不会保存 |

消息：

```json
{"id": "...", "seq": 3, "sender": "ann", "name": "Ann", "kind": "text",
 "text": "hi", "metadata": {"reply_to": 2}, "reactions": {"👍": ["support-bob"]}, "created_at": "..."}
```

编辑过的消息保留 `seq` 并加上 `edited_at`。删除的消息保留位置、带 `deleted_at` 且没有文字，之后不能再编辑或表情回应（`409`）。`after` 只返回更新的消息，所以回到在线的客户端要重新读取界面上的消息（`last=n`），才能取得编辑、删除与表情回应。搜索会匹配字词的任何部分，忽略大小写与全角，所以中文、日文、韩文都适用。系统消息说明发生了什么（加入、转接、结案）：只有你的服务器能发送，不会推送，也不会算入未读。

## 已读回执与未读数

| 方法与路径 | 内容 | 返回 |
|---|---|---|
| `POST /conversations/{id}/read` | `user_id`、`seq` | `{read_seq}` |

成员的已读位置只会前进，且不超过 `last_seq`；消息的发送者视为已读。用户会话列表中的 `unread` 计算 `read_seq` 之后、非系统消息的数量。有人的 `read_seq` 前进时，每位成员都会收到 `chat.read`。

## 浏览器与 App：用户 Token

客户端可以自己在 `/apps/APP_ID/chat/me` 底下调用 Chat API，带上 `Authorization: Bearer <user token>`，你的服务器不需要代理。Token 是用 App secret 签署的 JWT（HS256、`{sub, exp}`、最长 30 天）；你的服务器可用任何 JWT 库或 `POST /tokens` 产生：

| 方法与路径 | 内容 | 返回 |
|---|---|---|
| `POST /tokens` | `user_id`、`conversation_id`（只限该会话）、`join`（第一次使用时把用户加入会话）、`ttl`（60–86400 秒） | `201 {token, expires_at}` |

有了 Token，客户端可以列出自己的会话（`GET /me/conversations`）、读取、搜索、发送、编辑与删除自己的消息、加上表情回应、标记已读与送出输入中状态。用户一律是 Token 的 `sub`。客户端不能创建会话、变更成员、设置推送选项、禁言或封禁：这些都留在你的服务器。每位用户每秒最多写入 2 次，突发最多 10 次。带 `conversation_id` 与 `join` 的 Token 适合客服聊天的匿名访客：你的服务器创建会话、验证访客，再为自定义的 ID 发出短效 Token。

## 附件

1. 你的服务器申请上传：`POST /conversations/{id}/attachments`，带 `user_id`、`name`、`content_type` 与 `size`（最大 25 MB）。返回内容含 `attachment` 与 `upload`（`url`、`method`、`headers`）。
2. 客户端在 15 分钟内，用完全相同的标头把文件直接上传到 `upload.url`。
3. 你的服务器发送带 `attachments` 的消息：第 1 步取得的 `id`、`name` 与 `content_type`（每条最多 10 个；此时 `text` 可以留空）。

从 API 或事件读到的每个附件都带有一小时内可下载的 `url`。一天内没有送出的上传会被移除，删除消息或会话也会删除其文件。

## 推送给离线成员

新消息会通过 App 的 Hibira Push 推送给未连接的成员（`push: "offline"`，默认）、除发送者外的所有成员（`"always"`），或不推送（`"never"`）。标题是发送者与会话名称（或 `push_title`），正文是文字的前 180 个字符，点击时打开 `push_url`。设备以兼容 Beams 的推送 SDK 用相同的用户 ID 注册。返回的 `push` 会说明推送了几位成员，或为什么没有推送。

## 内容审核

| 方法与路径 | 内容或查询参数 | 返回 |
|---|---|---|
| `PUT /moderation/filter` | `words`（最多 500 个）、`action`（`mask` 或 `reject`） | `{filter}` |
| `POST /users/{user_id}/ban` | `reason`、`until`（RFC 3339；省略表示无期限） | `{user_id, banned, until}` |
| `DELETE /users/{user_id}/ban` | | `{user_id, banned}` |
| `POST /conversations/{id}/members/{user_id}/mute` | `reason`、`until` | `{conversation_id, user_id, muted, until}` |
| `DELETE /conversations/{id}/members/{user_id}/mute` | | `{conversation_id, user_id, muted}` |
| `POST /conversations/{id}/messages/{seq}/reports` | `user_id`、`reason` | `201 {report}` |
| `GET /moderation/reports` | `limit`（1–200） | `{reports}`，由新到旧 |

词语过滤会检查每条新的或编辑过的消息，匹配字词内部并忽略大小写与全角，所以中文与日文也能拦截。`mask` 把每个符合处换成＊；`reject` 返回 `422` 且不保存。被封禁的用户在 App 的所有会话中发送、编辑、表情回应与输入中都会得到 `403`；被禁言的成员在该会话中发送与输入中会得到 `403`，但仍可读取。

## 事件与 Webhook

事件会送到相关成员各自的 `#server-to-user-<user_id>`，以及会话的 watchers。每个事件算一则消息额度。

| 事件 | 对象 | 数据 |
|---|---|---|
| `chat.conversation` | 每位成员，于创建、变更或添加成员时 | `{conversation}` |
| `chat.message` | 每位成员 | `{conversation_id, message}` |
| `chat.message_updated` | 每位成员，于编辑、删除或表情回应之后 | `{conversation_id, message}` |
| `chat.typing` | 输入者以外的每位成员 | `{conversation_id, user_id, typing}` |
| `chat.read` | 每位成员 | `{conversation_id, user_id, read_seq}` |
| `chat.left` | 被移除的成员 | `{conversation_id}` |
| `chat.deleted` | 被删除会话的每位成员 | `{conversation_id}` |

```js
const pusher = new Pusher('APP_KEY', { cluster: 'hkg', wsHost: 'ws.hibira.io', userAuthentication: { endpoint: '/pusher/user-auth' } });
pusher.signin();
pusher.user.bind('chat.message', ({ conversation_id, message }) => showMessage(conversation_id, message));
```

每条新消息也会调用你服务器的 `chat_message` Webhook：`channel` 是会话 ID，`user_id` 是发送者，`data` 是 JSON `{"conversation_id", "message"}`。在 App 的某个 Webhook 事件中加入 `chat_message` 即可接收。

## 限制

| | |
|---|---|
| 会话 ID | 1–128 个 `A-Z a-z 0-9 _ - = @ , . ;` |
| 用户 ID | 与 `pusher.signin` 用户 ID 规则相同 |
| 每个会话的成员 | 100 |
| 消息文字 | 4,000 字符，不可空白 |
| 会话或发送者名称 | 100 字符 |
| `metadata` | JSON 对象，最多 4,096 bytes |
| 每次读取的消息 | 200 |
| 表情回应 | 每个 1–32 bytes，每条消息最多 20 种 |
| 附件 | 每个 25 MB，每条消息 10 个 |

写入共用 App 的 HTTP API 速率限制（超过时返回 `429`）。

## 每月活跃用户与保存期间

用户在某个月（UTC）发送消息或标记会话已读，即为该月的活跃用户，账号下所有 App 只计算一次。每个账号每月包含 500 位；Chat 附加包可提高到 1,000、5,000、20,000 或 60,000 位（[套餐](https://hibira.io/zh-cn/chat)）。当月额满后，尚未计入的用户在发送与已读时会得到 `429`，直到下个月；已计入的用户不受影响。`GET /usage` 返回 `{month, active_users, limit}`。

消息在 30 天内可完整编辑。之后，付费套餐或有 Chat 附加包的账号会将消息保存在封存中，API 仍会返回（唯读）；没有附加包的 免费套餐则会删除。

## 从 AI 代理使用

[Hibira MCP 服务器](https://hibira.io/zh-cn/mcp-server)提供 Chat API 工具，让 AI 编程代理帮你设置与测试聊天：`create_conversation`、`send_message`、`list_conversations` 与 `list_messages`。它们作用在你自己的 App 上，检查与签名请求相同，且账号需要已开通 Chat API。适用 Claude Code、Codex、Cursor 等工具的[代理插件](https://hibira.io/zh-cn/guides/agent-plugin)提供一个技能，依照这些步骤把 Chat API 接入项目。若需要 React 或纯 JavaScript 的现成会话界面，请见 [@hibira-io/chat-ui](https://hibira.io/zh-cn/guides/chat-ui)。
