关于 conversations
面向使用 Zavu 对话 REST API 构建收件箱及相关工具的技能,涵盖跨渠道线程、搜索、消息读取、回复和 Webhook 更新。
主要功能
- 01跨渠道对话线程
- 02线程列表与身份搜索
- 03消息读取和已读状态管理
- 04使用线程发送方回复
- 05通过 Webhook 更新
适用场景
- 01构建共享收件箱线程列表
- 02查看联系人的对话历史
- 03通过线程原发送方回复
- 04用 Webhook 更新收件箱视图
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ zavudev/ zavu-skills --skill conversations安装后在 Claude Code 中输入/conversations确认已加载。
详细介绍
对话
适用场景
构建任何会读取收件箱的功能时,都可以使用此技能:共享收件箱 UI、帮助台同步、“向我展示这位客户的历史记录”工具,或需要先获取对话线程再回复的 Agent。
GET /v1/messages 返回的是没有关联线程的扁平日志。对话就是线程。
什么是对话
一个联系人的一个线程,涵盖所有渠道。联系人先通过 WhatsApp 联系,之后又通过电子邮件联系时,仍属于同一个对话;channels 会列出该对话涉及的渠道。
| 字段 | 含义 |
|---|---|
id | 线程 ID。与消息、conversation.new 和 message.inbound 中的 conversationId 值相同。 |
contactIdentifier | 用于归档该线程的键。不一定是电话号码——见下文。 |
contactId | 解析到的联系人。群组线程中不存在此字段。 |
channels | 该线程涉及的所有渠道。 |
lastMessage | 冗余存储的预览(id、text、channel、direction、at),因此线程列表无需额外获取数据。 |
senderId | 最后处理该线程的发送方。回复时将其作为 Zavu-Sender 传回。 |
unreadCount | 尚未标记为已读的入站消息数。 |
whatsapp | 联系人采用 WhatsApp 用户名时包含 bsuid 和 username。 |
contactIdentifier 不一定是电话号码
它保存用于标识线程的标识符,可能是 E.164 电话号码、WhatsApp BSUID(US.13491208655302741918)或数字聊天 ID(Telegram/Instagram/Messenger)。将其解析为电话号码会导致所有非 SMS 渠道出错。请使用 channels 决定如何显示。
端点
这些端点尚未纳入生成的 SDK。请使用 REST。
| 方法 | 路径 | 用途 |
|---|---|---|
GET | /v1/conversations | 列出线程,最近活跃的排在最前。 |
GET | /v1/conversations/{conversationId} | 获取一个线程。 |
GET | /v1/conversations/{conversationId}/messages | 获取线程中的消息,最新的排在最前。 |
POST | /v1/conversations/{conversationId}/read | 将 unreadCount 重置为零。 |
列出线程
curl "https://api.zavu.dev/v1/conversations?limit=25" \
-H "Authorization: Bearer $ZAVUDEV_API_KEY"
筛选条件:senderId 将结果限定为一个号码,channel 仅保留涉及该渠道的线程。
curl "https://api.zavu.dev/v1/conversations?senderId=sender_12345&channel=whatsapp" \
-H "Authorization: Bearer $ZAVUDEV_API_KEY"
TypeScript:
const res = await fetch("https://api.zavu.dev/v1/conversations?limit=25", {
headers: { Authorization: `Bearer ${process.env.ZAVUDEV_API_KEY}` },
});
const { items, nextCursor } = await res.json();
搜索线程
search 会根据对话对象查找线程:电话号码、电子邮件地址、WhatsApp 用户名或 BSUID。电话号码格式不影响结果——+1 (555) 123-4567 和 15551234567 都会匹配同一个线程。
curl "https://api.zavu.dev/v1/conversations?search=%2B56912345678" \
-H "Authorization: Bearer $ZAVUDEV_API_KEY"
# by email, by its local part, or by group name
curl "https://api.zavu.dev/v1/conversations?search=maria" \
-H "Authorization: Bearer $ZAVUDEV_API_KEY"
匹配按完整单词进行,并支持最后一项前缀匹配:mar 可找到 [email protected],+1555 可找到 +15551234567。数字中间或末尾的片段(4567)不会匹配——请搜索完整号码或其前缀。
在此基础上构建功能前,请了解以下三点:
- 不会搜索消息正文。
search匹配的是线程身份信息,而不是线程中说过的内容。 - 结果按相关性排序,而非按最近时间排序。 设置
search后,不会采用通常的“最近活跃的排在最前”排序方式。 - 空的
search不会返回任何结果,而非所有结果。若要列出所有线程,请移除该参数。
search 可以与 senderId 和 channel 一起使用,也可以像其他列表一样通过 cursor 分页。
const params = new URLSearchParams({ search: "+56912345678", channel: "whatsapp" });
const res = await fetch(`https://api.zavu.dev/v1/conversations?${params}`, {
headers: { Authorization: `Bearer ${process.env.ZAVUDEV_API_KEY}` },
});
const { items } = await res.json();
分页
nextCursor 是不透明值。请原样传回作为 cursor;切勿手动构造。最后一页的 nextCursor 为 null。
async function allConversations() {
const out = [];
let cursor: string | undefined;
do {
const url = new URL("https://api.zavu.dev/v1/conversations");
url.searchParams.set("limit", "100");
if (cursor) url.searchParams.set("cursor", cursor);
const res = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.ZAVUDEV_API_KEY}` },
});
const page = await res.json();
out.push(...page.items);
cursor = page.nextCursor ?? undefined;
} while (cursor);
return out;
}
每页获取完成后才会应用 channel 筛选,因此筛选后的页面可能少于 limit 条,甚至为空,但 nextCursor 仍可能有值。请一直翻页,直到 nextCursor 为 null;不要因页面条数较少而停止。
区分对话双方
每条消息都带有 direction(inbound 或 outbound)。请使用它——status 无法区分消息方向,因为入站消息也会存储为 delivered。通过比较 to 和 contactIdentifier 来推断方向,适用于一对一线程,但不适用于群组;在群组中,from 指的是参与者,而不是线程键。
for (const m of items) {
const mine = m.direction === "outbound";
console.log(`${mine ? "→" : "←"} ${m.text}`);
}
读取会话
curl "https://api.zavu.dev/v1/conversations/$CONV_ID/messages?limit=50" \
-H "Authorization: Bearer $ZAVUDEV_API_KEY"
返回与 GET /v1/messages 相同的 Message 对象,按时间从新到旧排列,涵盖会话中的所有频道。
标记为已读
curl -X POST "https://api.zavu.dev/v1/conversations/$CONV_ID/read" \
-H "Authorization: Bearer $ZAVUDEV_API_KEY"
这会清除你自己的收件箱计数器,但不会向联系人发送已读回执。若要显示 WhatsApp 已读回执和正在输入指示器,请改为对收到的消息调用 POST /v1/messages/{messageId}/typing。
在会话中回复
使用 POST /v1/messages 发送消息,将会话标识符作为发送对象,并将其 senderId 作为 Zavu-Sender 传入,这样回复就会从联系人已知的号码发出。
const conv = await getConversation(conversationId);
await zavu.messages.send({
to: conv.contactIdentifier,
channel: conv.lastMessage.channel,
text: "Thanks — shipping today.",
"Zavu-Sender": conv.senderId,
});
发送方覆盖参数是发送参数对象中的标头参数,而不是第二个参数。
回复时仍有两项限制:
- WhatsApp 24 小时窗口。 超出此时限后,系统会以
whatsapp_window_closed拒绝自由格式文本;请改为发送已批准的模板。请参阅whatsapp-templates技能。 - 频道可用性。 请通过发送方实际拥有的频道回复。请以
GET /v1/senders/{senderId}返回的channels为准,而不要根据是否存在电话号码来判断。
从 Webhook 获取会话
除回应之外,每个频道的 message.inbound 都包含 data.conversationId。该字段始终有值:Webhook 会在消息归入会话后发送,因此新会话中的第一条消息携带的标识符与 conversation.new 中的相同;将 BSUID 与电话号码关联起来的 WhatsApp 消息也会标明该会话的标识符,该会话之后会继续保留。GET /v1/messages/{messageId} 返回的也是相同标识符。
不要退而求其次,列出会话后再按电话号码匹配:在繁忙的项目中,会话可能暂时还不在列表顶部,而且标识符可能是 BSUID,而不是你预期的电话号码。
构建收件箱
API 提供会话、消息和已读状态。它不会存储分配信息、打开/完成状态、内部备注或保存的回复——这些工作区状态应由你的应用管理。请使用 conversation.id 作为键;它在不同频道间保持稳定,并且在 WhatsApp 身份重新关联后仍然有效(仅有 BSUID 的会话之后即使关联了电话号码,也会保留原有标识符)。
如需实时更新,请为 message.inbound、message.sent、message.delivered、message.read、message.failed 和 conversation.new 订阅 Webhook,然后更新本地视图。如果无法接收 Webhook,则可通过轮询 GET /v1/conversations 作为备用方案。请参阅 webhook-setup 技能。
注意事项
| 陷阱 | 实际情况 |
|---|---|
将 contactIdentifier 当作电话号码 | 它可能是 BSUID 或聊天 ID。 |
| 自行构造游标 | 游标是不透明的;只需原样传回 nextCursor。 |
| 短页时停止分页 | channel 筛选器会缩小页面大小。请在 nextCursor === null 时停止。 |
认为 contactId 总是存在 | 群组会话和未解析的联系人中不会提供此字段。 |
认为 /read 会通知联系人 | 它只会清除你自己的计数器。 |