Skip to content

宿主接口契约 ​

宿主是你的业务后端,IM 服务端靠它知道用户是谁、谁和谁是好友、每个人叫什么。两者之间只通过 HTTP 交谈,接口写在仓库的 contract/host-api/ 里,共三个文件:

文件内容
host.openapi.yaml宿主实现、IM 服务端调用的端点
im-internal.openapi.yamlIM 服务端暴露、宿主调用的内部端点
uid-vectors.json宿主用户 id 到线上 uid 的黄金向量,见 签发 IM 令牌

两份 OpenAPI 都是 3.1 版。Node 宿主可以直接用 Node 宿主 SDK;用别的语言实现时看 用其他语言实现。

宿主负责什么 ​

IM 服务端只管连接、消息、群和推送,其余都归宿主:

  • 身份:宿主签发用户登录 IM 用的令牌,见 签发 IM 令牌。
  • 关系:谁和谁是已接受的好友,以及各自给对方设的备注。
  • 资料:每个人的显示名和头像。
  • 媒体:消息里的文件 URL 由宿主签发,文件也存在宿主那里;IM 只存 URL。
  • 业务:积分、红包、通话计费等都在宿主,IM 只转发宿主给的结果。
  • 运行配置:群人数上限、推送文案等运行期设置由宿主下发。

宿主是一个 HTTP 服务时,IM 服务端设 TALKWIRE_HOST_MODE=http(默认),用 MAIN_API_BASE_URL(默认 http://127.0.0.1:6001)找到它。不需要宿主进程时可以用 TALKWIRE_HOST_MODE=standalone,好友、资料和运行配置改由 TALKWIRE_STANDALONE_CONFIG 指向的文件提供,详见 环境变量。

鉴权:内部令牌 ​

两个方向的调用都带同一种内部令牌:Authorization: Bearer <令牌>,令牌是用双方共享的 JWT_SECRET 签名的 HS256 JWT。用户登录 IM 用的是另一种令牌,与此无关。

claim值
roleinternal_service
exp签发时刻加有效期。IM 服务端签 1 分钟的令牌;契约说参考宿主签 1 小时的,1 分钟已经够用
  • IM 调宿主:宿主收到的每个请求都要验签(只认 HS256)、验 role、验过期,不通过就答 401 和 JSON {"error": ...}。
  • 宿主调 IM:宿主自己签一个同样的令牌。IM 拒绝时答 401,正文是纯文本 unauthorized;方法不对时答 405,正文也是纯文本;其余错误是 JSON {"error": "..."}。

注意

IM 的内部端点与客户端的 /ws/im 在同一个端口(IM_PORT,默认 6062)上。不要让公网反向代理转发 /api/v1/internal/ 下的路径;内部令牌校验只是第二道防线。

契约里的 x-talkwire ​

每个操作的 x-talkwire 写明 IM 服务端对它的期待。IM 服务端从不重试失败的调用:超时、传输错误和 200 以外的任何状态都算失败,按 onFailure 处理。

键含义
requirementrequired:核心功能需要;optional:没有也能运行;module:<名>:只有装了这个可选模块才会调用
timeoutSecondsIM 服务端等多久
onFailure失败时 IM 服务端怎么办
cacheSecondsIM 服务端缓存应答多久(只有 profile)
refreshSecondsIM 服务端多久重新拉一次(两份配置文档)

200 的应答必须是 JSON,IM 不认识的字段会被忽略。宿主用户 id 是宿主自己选的不透明字符串,最长 128 个字符。

宿主实现的端点 ​

这些端点由宿主实现,IM 服务端在 MAIN_API_BASE_URL 后面加路径调用,路径都以 /api/v1/internal/im/ 开头:

端点要求IM 何时调用超时失败时 IM
GET friendship?a=&b=required单聊、邀请入群、建群之前3 s拒绝这个动作
GET profile?viewer=&sender=required给离线推送定标题和头像3 s用兜底名字、不带头像
GET runtime-configoptional启动时,之后每 60 秒3 s沿用上一次的值
GET e2ee-configmodule:e2ee启动时,之后每 60 秒3 s沿用上一次的值
POST delete-mediaoptional撤回或清空之后,对不再被任何消息引用的文件3 s只记日志,文件留着
POST calls/createmodule:call发起通话之前3 s拒绝邀请(500)
POST calls/answermodule:call被叫接听时8 s双方的通话以 failed 结束
POST calls/settlemodule:call通话进入 IM 判定的终态时3 s只记日志,宿主要自己清扫
POST calls/upgrademodule:call双方同意把语音升级为视频时3 s拒绝升级(500),通话继续是语音
POST group-ai-qamodule:groupai群成员 @ 群助手提问时10 s当作超时,群里收到兜底文案

宿主不提供某个模块,就对该模块的端点答 404,并且不要在 IM 上装这个模块(TALKWIRE_MODULES)。装了而宿主不答,每次使用都会按 onFailure 失败。

friendship ​

friendship 回答两个宿主用户之间是否有已接受的关系,应答是 {"friends": true} 或 {"friends": false}。关系是对称的:(a, b) 和 (b, a) 必须得到同一个答案。失败时,需要这次检查的动作得到错误帧 cannot verify relationship 或 relationship check failed。

profile ​

profile 回答 viewer 看到的 sender 是什么样子,应答是 {"displayName", "avatarUrl"},用作离线推送的标题和头像。displayName 先给 viewer 自己为 sender 设的备注,没有备注才给 sender 自己的显示名。

  • 备注只在两人是已接受的好友时才给。备注是按方向存的私人数据:sender 给 viewer 设的备注绝不能返回。
  • 宿主不认识 sender 时答 200 和两个空串,IM 改用运行配置里的 pushText.sender。
  • avatarUrl 可以是绝对 URL 或相对宿主的路径;没有头像时给空串或 null。
  • IM 服务端把同一对 viewer 与 sender 的应答缓存 300 秒。

注意

备注的方向是最容易写反的地方。用两个账号各给对方设一个不同的备注,分别核对两边推送里的名字。

runtime-config 与 e2ee-config ​

两份配置文档在启动时拉一次,之后每 60 秒拉一次。非 200 的应答一律保留上一次的值,第一次成功之前用内置默认值;404 表示宿主没有这份文档。

运行配置的解码规则要特别注意:缺失或不是正数的整数字段保留内置默认值,但缺失的布尔字段就是 false,所以每个布尔字段都要显式给出。字段如下(全部可选):

字段含义默认值
maxMembers群人数上限500
maxAdmins每个群的管理员数10
nameMaxLen群名长度(码点)30
messageMaxLen消息长度(字节)20000
forwardBundleMax合并转发的消息条数50
forwardBroadcastMax一次转发的目标数20
historyPageSize历史消息一页的最大条数50
fcmEnabled离线推送开关true,字段缺失时为 false
fcmConcurrency并行推送请求数16
readReceiptEnabled群已读游标false
qrTtlSeconds群二维码有效期604800
publishRateLimitPer10s一个用户每 10 秒能发的消息数20
callRingTimeoutSeconds振铃多久判为无人接听45
callMaxDurationMinutesIM 服务端读取但不执行60
pushText离线推送文案:image、video、audio、file、sender、assistant、joinRequest({group} 换成群名)、joinApproved、joinRejected空或缺失的条目保留默认值
groupAi群助手的名字 name(默认“小桃子”)和答不了时的文案:insufficientCredits、quotaExceeded、notConfigured、ownerInactive、rateLimited、busy空或缺失的条目保留默认值;文案里的 {name} 换成助手名

端到端加密配置有三个字段:enabled、groupEnabled、envelopeTtlHours(至少 1)。内置默认值是 enabled=true、groupEnabled=true、envelopeTtlHours=336,见 端到端加密。

delete-media ​

delete-media 让宿主永久删除一批文件,请求体是 {"urls": [...]},最多 500 个。IM 只在没有任何剩余消息引用这些 URL 时才调用它,宿主直接删除即可。

  • 宿主只删自己签发的 URL 背后的文件,不认识的 URL 忽略。
  • 删除不存在的文件不算错。
  • 应答可以带 {"deleted": <数量>},IM 不读应答体。

通话模块 ​

通话模块的四个端点把是否允许通话、媒体凭证和计费交给宿主,详见 音视频通话。

端点请求体宿主答什么
calls/createcallSessionId、callerAppUserId、calleeAppUserId、callType(voice 或 video){"ok", "reason"}。IM 先生成 callSessionId、占住双方再问宿主;ok=false 时主叫收到带宿主 reason 的错误帧,not_friends 为 401,其余(例如 insufficient_credits)为 409
calls/answercallSessionId、effectiveTypeok、reason、channelId、callerToken、calleeToken、videoQuality、denoiseEnabled、appId。effectiveType 是此后的类型:被叫把视频邀请当语音接听时为 voice。两个令牌必须不同,各自编码自己的参与者;appId 对双方相同。ok=false 时通话以 failed 结束,被叫的错误帧带宿主的 reason
calls/settlecallSessionId、reason(user_hangup、no_answer、rejected、cancelled、disconnected、failed)任意 JSON 对象,IM 不读
calls/upgradecallSessionId、toType(固定为 video){"ok", "reason", "videoQuality"}。ok=false 时升级以 409 和宿主的 reason 被拒,通话继续是语音

group-ai-qa ​

group-ai-qa 是群助手的钩子:群成员写“@”加助手的名字(运行配置 groupAi.name)提问,IM 先在群里发 ai_typing,再把问题交给宿主。回答什么、扣不扣费都由宿主决定,IM 把回答以助手身份发进群,详见 群助手钩子。

请求体有 groupTopic(以 grp_ 开头)、askerImUid、question、groupOwnerAppUserId、kbIds、docIds。应答规则:

  • 能回答时给 {"ok": true, "answer": "..."}。
  • 业务上答不了时仍然答 200,给 ok=false 和 reason:insufficient_credits、quota_exceeded、not_configured、owner_inactive、rate_limited,其他原因一律用 busy 文案。
  • 可以再带 message 作为宿主自己的说法,IM 把它发进群代替配置的文案;ok=true 时 message 被忽略。
  • 200 以外的状态、超时和传输错误都按技术故障处理,群里收到 busy 文案。

IM 服务端暴露的端点 ​

这些端点由 IM 服务端在 IM_PORT(默认 6062)上提供,宿主调用:

端点要求作用
POST /api/v1/internal/im/contact-syncrequired关系被接受或解除
POST /api/v1/internal/im/media-ref-countoptional查一个媒体 URL 还被多少条消息引用
POST /api/v1/internal/im/topic-members-resolveoptional按宿主用户 id 查群成员身份,按 uid 反查宿主用户 id
POST /api/v1/internal/im/custom-broadcastoptional把宿主卡片的新状态推给一个会话或一个用户
POST /api/v1/internal/im/red-packet-broadcastoptional,已弃用参考部署的旧写法,新宿主用 custom-broadcast
POST /api/v1/internal/im/call-broadcastmodule:call把宿主决定的通话帧推给一个用户的所有设备
GET /api/v1/internal/admin/im/topicsoptional后台:一个用户的会话列表
GET /api/v1/internal/admin/im/topicoptional后台:一个会话
GET /api/v1/internal/admin/im/messagesoptional后台:一个会话的消息
POST /api/v1/internal/admin/im/clearoptional后台:为双方清空一个单聊

contact-sync ​

宿主必须在关系被接受或解除时调用 contact-sync,请求体是 {"appUserIdA", "appUserIdB", "event"},event 为 accepted 或 removed。应答是 {"topic", "uidA", "uidB"},topic 是两人单聊的规范 topic(p2p_…)。

  • accepted:建好两人的单聊会话,把两人设为成员,并告诉两个在线的用户。
  • removed:把两人移出这个会话,历史记录保留。
  • 两个用户在 IM 这边不存在时会先建出来。

不调用的后果是:新好友之间收不到彼此的在线状态,第一次聊天还要等一次好友校验。

custom-broadcast ​

custom-broadcast 把宿主卡片的状态变化或宿主事件实时推给客户端,例如红包被领了、有人加你好友。请求体有 kind、payload,以及 topic 和 targetUid 二者之一(不能同时给):

  • 给 topic:订阅了这个会话的所有连接收到帧。
  • 给 targetUid:这个用户的所有连接收到帧,帧里没有 topic;应答里的 delivered 为 false 表示该用户当时没有连接。

IM 原样转发,帧是 {"action":"custom_update","topic":...,"what":<kind>,"extra":<payload>}。kind 匹配 ^[a-z][a-z0-9_.-]{0,63}$,payload 可以是除 null 外的任何 JSON 值,整个请求不超过 64 KiB(超出答 413)。

这个推送只发给此刻在线的连接,不进离线队列,所以客户端打开卡片时还要向宿主拉一次当前状态。完整的例子是 examples/red-packet/,客户端怎么显示自定义卡片见 自己的消息类型。

call-broadcast ​

call-broadcast 用于通话模块里宿主自己做出的决定,例如通话中的余额更新、宿主结束的通话(credit_exhausted、max_duration_reached)。请求体是 {"targetUid", "payload"},payload 必须已经是客户端认识的完整帧,IM 逐字节发出。

payload 的 type 为 call_ended 时,IM 也会释放这通通话。应答里的 delivered 为 false 表示该用户没有在线设备,不会排队。

media-ref-count 与 topic-members-resolve ​

media-ref-count 回答一个 URL 还被多少条消息引用({"count"}),已撤回的消息不计。转发的消息与原消息共用 URL,所以宿主自己删文件之前(例如用户放弃了一个没发出去的上传)要先确认引用数为 0。

topic-members-resolve 一次回答两类问题,请求体是 topic 加可选的 checkAppUserIds、resolveUids:

  • members:checkAppUserIds 里每个宿主用户 id 是否是这个会话的成员。
  • resolved:resolveUids 里每个 uid 背后的宿主用户 id(IM 从没见过这个 uid 时为 null)以及是否是成员。

后台端点 ​

后台四个端点供宿主的管理后台查看和清空聊天记录,前端页面见 后台页面。

端点参数应答
topicsuid、limit(1–100,默认 50)这个用户的会话,最近的在前
topictopic一个会话:成员 uid、最后一条消息、消息数、清空版本等
messagestopic,可选 beforeSeq、limit(1–100,默认 50)、type、keyword、fromMs、toMs一页消息,新的在前,用 beforeSeq 往前翻;已撤回的消息不返回
clear{"topic"}deletedMessages、deletedMedia、clearVersion、clearBeforeSeq

clear 为两个人永久删除一个单聊的全部消息,请宿主删除不再被引用的媒体,并告诉双方(在线的立刻,离线的在下次登录时);对群答 409。清空前要求操作者确认、记录审计都是宿主的事:宿主应要求超级管理员、输入确认语、写明理由,用幂等键防止重复执行,审计只记元数据、不记消息内容。

核心以 Apache-2.0 发布;端到端加密模块为 AGPLv3。