mcpField
C
conversations

conversations

github.com/zavudev生产力167 次安装今天更新
代码仓库

关于 conversations

面向使用 Zavu 对话 REST API 构建收件箱及相关工具的技能,涵盖跨渠道线程、搜索、消息读取、回复和 Webhook 更新。

主要功能

  1. 01跨渠道对话线程
  2. 02线程列表与身份搜索
  3. 03消息读取和已读状态管理
  4. 04使用线程发送方回复
  5. 05通过 Webhook 更新

适用场景

  1. 01构建共享收件箱线程列表
  2. 02查看联系人的对话历史
  3. 03通过线程原发送方回复
  4. 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 会通知联系人它只会清除你自己的计数器。

评论

支持 Markdown