dsh-im-ops/PROACTIVE_DELIVERY.md
2026-08-30 11:52:01 +08:00

15 KiB
Raw Blame History

主动投递使用指南

简体中文 · English

主动投递让应用在没有用户新消息的情况下,通过已经接入 DSH-IM 的机器人发送文字消息。调用方只需保存一组稳定的 botId + targetId,不需要保存 Harness sessionId、聊天引用、消息 ID 或临时 Webhook。

九个内置渠道均支持主动投递:微信、飞书、钉钉、企业微信、QQ、Slack、Telegram、Discord 和 WhatsApp。

快速开始

  1. 打开「设置 → IM机器人」,找到需要发消息的机器人。
  2. 点击机器人卡片右上角的齿轮图标。
  3. 在「调用标识」中复制 Bot ID。
  4. 点击「新建目标」,从已聊会话中选择,或点击「手动填写(高级)」填写平台原生 ID。
  5. 填写或确认 Target ID、目标类型和平台原生 ID。
  6. 点击「测试」。目标收到 DSH-IM 主动投递测试成功。 后,再点击「保存目标」。
  7. 在已保存目标上点击「复制调用参数」,得到可供应用保存的 { botId, targetId }。
  8. 使用 HTTP POST、同 Host 的 ctx.dshIm.send() 或 Connection RPC 的 message.send 发送消息。

配置投递目标

1. 获取 Bot ID

botId 是当前已接入机器人的真实调用标识。请从设置页复制并将它视为不透明字符串,不要根据前缀推断渠道,也不要使用机器人名称、平台 App ID 或卡片中的脱敏 ID 代替。

目标配置跟随当前机器人保存。移除机器人时,它的投递目标也会被清理;重新接入后应重新复制 botId 并配置目标。

2. 新建目标

点击「新建目标」后,默认显示「从已聊过的会话选择」下拉框:

  • 候选来自该机器人已经持久化的会话映射,不是平台通讯录,也不是严格按时间排序的完整最近会话列表。
  • 候选只包含投递所需的目标类型和平台原生 ID,不包含 Harness sessionId、消息正文、消息 ID、会话名称或最后活跃时间。
  • 已配置的相同目标会显示「已添加」并禁用。
  • 没有候选时,先在对应平台与机器人聊一条消息,再点击「刷新」。仍未出现时可使用「手动填写(高级)」。

选择候选只会预填表单,不会自动保存。

3. 理解 Target ID

targetId 是你为调用方定义的稳定别名,不是平台用户 ID、群 ID 或频道 ID。

  • 它只需在同一个机器人下唯一;不同机器人可以使用相同的 targetId。
  • 可使用大小写字母、数字、点、下划线、冒号、@ 或连字符,长度为 1–128 个字符。
  • 新建时页面默认生成 tgt_ 加 16 位十六进制随机串,例如 tgt_7f3a91c8d2e64b10。
  • 首次保存前可以修改,例如改成 daily-report 或 release-alerts。
  • 保存后 targetId 不可修改,但名称、目标类型和平台原生路由可以修改;调用方仍使用原来的 botId + targetId。

4. 测试、保存和复制

从会话新建、手动新建和编辑目标时,表单底部都会显示「测试」按钮:

  • 机器人在线且当前目标所需的平台 ID 已填写完整时,测试按钮才可用。
  • 测试使用当前表单中的目标类型和平台 ID,不会先创建、更新或保存目标。
  • 修改测试过的平台 ID 后,旧的成功提示会清除,需要重新测试。
  • 已保存目标列表中的「测试」按钮测试该目标当前保存的路由。
  • 测试成功表示平台发送接口已接受请求或 SDK 成功返回,不代表消息已经被阅读。

保存后点击「复制调用参数」,页面会复制如下 JSON:

{
  "botId": "bot_9577c8572d454122a4ef7fb4d8420a91",
  "targetId": "release-alerts"
}

配置示例:飞书告警群

假设飞书机器人已经在告警群中收到过消息:

  1. 打开该机器人的设置页并复制 Bot ID。
  2. 点击「新建目标」,在下拉框中选择告警群。
  3. 将自动生成的 Target ID 改为 release-alerts,显示名称填为「发布告警群」。
  4. 确认目标类型为「群聊」,群 Chat ID 已自动填入。
  5. 点击「测试」,到飞书群中确认测试消息。
  6. 点击「保存目标」,再点击「复制调用参数」。

以后即使编辑并更换群 Chat ID,调用方仍可继续使用同一组 botId + release-alerts。

九渠道手动填写字段

优先从已聊会话中选择。只有目标未出现在候选中时,才需要手动取得以下平台原生 ID。

渠道 目标类型 需要填写的字段 示例或说明
微信 user 微信用户 ID(toUserId) 填写接收消息的微信用户 ID
飞书 user Open ID(openId) 例如 ou_xxx
飞书 group 群 Chat ID(chatId) 例如 oc_xxx
钉钉 user 用户 ID(userId) 填写钉钉用户 ID
钉钉 group 群 Open Conversation ID(openConversationId) 主动投递不使用临时 sessionWebhook
企业微信 user 用户 ID(路由字段为 chatId) 私聊填写用户 ID
企业微信 group 群 Chat ID(chatId) 群聊填写群 chatid
QQ user 用户 Open ID(userOpenId) 平台提供的 user_openid
QQ group 群 Open ID(groupOpenId) 平台提供的 group_openid
Slack conversation Channel ID(channelId) 例如 C0123456789
Slack thread Channel ID + Thread 时间戳(channelId, threadTs) 例如 1712345678.123456
Telegram chat Chat ID(chatId) 十进制字符串,例如 -1001234567890
Telegram topic Chat ID + Topic ID(chatId, messageThreadId) Topic ID 必须是正整数
Discord channel Channel ID(channelId) 私信、频道和 Thread 都使用可发消息的 Channel ID
WhatsApp user 用户 JID(jid) 例如 8613800000000@s.whatsapp.net
WhatsApp group 群 JID(jid) 例如 1234567890-123456@g.us

平台 ID 字符串不能为空或带首尾空格。一个目标只接受所选渠道和类型要求的字段,额外字段会被拒绝。

通过 HTTP POST 发送

普通外部应用可以直接调用 Host 的主动投递接口:

curl --request POST \
  http://127.0.0.1:3080/api/dsh-im/delivery/messages \
  --header 'Content-Type: application/json' \
  --data '{
    "botId": "bot_9577c8572d454122a4ef7fb4d8420a91",
    "targetId": "release-alerts",
    "text": "构建已经完成。"
  }'

成功返回:

{ "sent": true }

请求体严格只接受 botId、targetId 和 text,JSON 总大小不能超过 1 MiB。不要附加平台原生路由、sessionId、chatRef、临时 Webhook 或 idempotencyKey。

接口路径固定为 POST /api/dsh-im/delivery/messages,复用当前 DSH Host 的 WebServer,不会另开端口。示例中的 3080 是 Web profile 的默认端口;实际地址以 Host 启动时显示的地址为准。

当前 HTTP 接口不包含鉴权,也不提供 CORS。只应在本机或可信网络中使用,不要直接暴露到公网。

在同一 Host 的插件中发送

消费插件声明 dshIm 注入后,可以直接调用共享服务,不经过 Connection RPC。

下面是一个最小示例;插件加载时会发送一次消息:

export const inject = ['dshIm'];

export async function apply(ctx) {
  const result = await ctx.dshIm.send(
    'bot_9577c8572d454122a4ef7fb4d8420a91',
    'release-alerts',
    '构建已经完成。',
  );

  if (result.sent !== true) {
    throw new Error('主动投递没有返回成功结果');
  }
}

实际使用时,把 ctx.dshIm.send() 放进你的定时任务、构建回调或业务事件处理函数中。可选的第四个参数当前支持取消信号:

await ctx.dshIm.send(botId, targetId, text, { signal });

同 Host 插件也可以列出某个机器人的已保存目标:

const targets = await ctx.dshIm.listTargets(botId);
// [{ targetId, name?, kind, route }, ...]

调用失败时 Promise 会拒绝,error.code 使用本文后面的公共错误码。

通过 Connection RPC 发送

Connection RPC 适合已经持有当前 DSH Host connection 客户端的调用方,也是设置页管理目标所使用的接口。普通外部应用优先使用上面的 HTTP POST。

先封装 RPC 成功与错误包络:

const DELIVERY_CHANNEL = '/dsh-im-delivery';

async function callDelivery(connection, endpoint, payload, signal) {
  const result = await connection.rpc.call(
    DELIVERY_CHANNEL,
    endpoint,
    payload,
    signal,
  );

  if (result?.ok !== true) {
    const error = new Error(result?.error?.message || 'delivery-failed');
    error.code = result?.error?.code || 'delivery-failed';
    throw error;
  }
  return result.value;
}

然后使用已复制的 botId + targetId 发送文字:

const result = await callDelivery(connection, 'message.send', {
  botId: 'bot_9577c8572d454122a4ef7fb4d8420a91',
  targetId: 'release-alerts',
  text: '构建已经完成。',
});

// result: { sent: true }

message.send 只接受 { botId, targetId, text }。不要附加平台路由、sessionId、chatRef、临时 Webhook 或 idempotencyKey。

示例:投递每日报告

async function sendDailyReport(connection, summary) {
  try {
    await callDelivery(connection, 'message.send', {
      botId: 'bot_9577c8572d454122a4ef7fb4d8420a91',
      targetId: 'daily-report',
      text: `今日运行摘要\n\n${summary}`,
    });
  } catch (error) {
    if (error.code === 'bot-not-connected') {
      // 等待机器人恢复连接后,由业务决定是否重试。
      return { delivered: false, reason: 'offline' };
    }
    throw error;
  }
  return { delivered: true };
}

管理 RPC 参考

设置页使用同一个 Connection RPC 通道管理目标。普通调用方通常只需要 message.send;需要自行管理目标时再使用其他端点。

所有响应均为 { ok: true, value } 或 { ok: false, error: { code, message, details } }。

端点 Payload 成功时的 value
message.send { botId, targetId, text } { sent: true }
target.list { botId } { botId, channel, targets }
target.suggestion.list { botId } { botId, channel, suggestions }
target.create { botId, target: { targetId, name?, kind, route } } 已创建的完整目标
target.update { botId, targetId, target: { name?, kind, route } } 更新后的完整目标
target.delete { botId, targetId } { deleted: true }
target.test { botId, targetId } { sent: true }
target.test { botId, target: { kind, route } } { sent: true };测试草稿,不保存

接口严格校验字段。target.update 的内部 target 不能包含 targetId;草稿测试不能包含 targetId 或 name。

错误处理

HTTP 失败响应格式为 { "error": { "code", "message", "details" } }。同 Host 和 RPC 使用相同错误码,但没有 HTTP 状态码。

错误码 HTTP 状态 含义与处理建议
bad-request 400 请求结构、ID 格式、JSON 或文字无效;检查字段名并移除额外字段
unknown-bot 404 botId 不属于当前 Host;重新从机器人设置页复制
unknown-target 404 该机器人下不存在 targetId;检查是否复制错误或目标已被删除
target-conflict 409 同一机器人下已经存在相同 targetId;更换别名
invalid-target 422 目标类型或平台原生 ID 不符合当前渠道规则;重新选择类型并核对 ID
bot-not-connected 503 机器人当前离线;恢复连接后由调用方决定是否重试
target-rejected 422 平台明确拒绝目标或机器人缺少发送权限;检查平台权限和目标 ID
delivery-failed 502 网络、平台或其他无法安全细分的发送失败;检查连接状态和 Host 日志
cancelled 408 调用被取消;按业务需要结束或重新发起

HTTP 协议层还可能返回 method-not-allowed(405)、unsupported-media-type(415)或 payload-too-large(413)。

投递语义与限制

  • 当前主动投递只发送非空文字,不支持在该接口中发送图片、文件、卡片或富文本。
  • HTTP JSON 请求体上限为 1 MiB。
  • { sent: true } 表示平台发送接口接受请求或 SDK 成功返回,不承诺最终送达或已读。
  • DSH-IM 不保存主动投递历史,不生成 deliveryHandle 或 idempotencyKey,也不自动重试。
  • 调用方超时后重试可能产生重复消息;需要业务幂等时,由调用方保存自己的业务事件 ID 和处理结果。
  • 正式发送只使用已保存的 botId + targetId。平台原生路由保存在目标配置中,不应随每次消息发送。
  • 一个调用只发送到一个目标。需要通知多个目标时,应分别调用并分别处理结果。
  • 机器人离线时仍可编辑目标,但不能测试或主动发送。

HTTP 与 RPC 可达范围

HTTP 接口只在当前 Host 提供 WebServer 时注册,并使用同一个监听地址和端口。默认 Web profile 地址通常是 127.0.0.1:3080,只能由本机访问。若要从其他机器调用,需要在对应 profile 的 cordis.patch.yml 中把 WebServer 绑定到可达地址并重启 Host,例如:

- id: webserver
  config:
    host: '0.0.0.0'

这会同时扩大该 WebServer 上其他页面和路由的网络可达范围。当前主动投递 HTTP 接口没有鉴权,因此只能配合可信局域网、防火墙或反向代理使用,不能直接暴露到公网。

Connection RPC 默认只允许当前 Host 的回环调用。若 Web profile 明确运行在受信任局域网,可在该 profile 的 cordis.patch.yml 中复用现有 Host authority:

- id: xmanrui-dsh-im
  config:
    rpcAuthority: trusted-host

trusted-host 只是 Host/Origin 可达性边界,不是用户认证。启用后,能访问该受信网络 authority 的调用方也能访问机器人管理接口;只应在可信网络中使用。

常见问题

下拉框找不到目标会话

先在对应平台向该机器人发送一条消息,再返回设置页刷新。候选不是平台的完整会话目录;仍然找不到时使用「手动填写(高级)」。

测试按钮不可点击

确认机器人在线,并填写当前目标类型要求的全部平台原生 ID。Slack Thread 和 Telegram Topic 都需要两个字段。

保存后能否修改 Target ID

不能。你可以编辑名称、类型和平台路由而保持调用参数不变;若必须更换 targetId,请新建目标并让调用方切换后再删除旧目标。

为什么不用 sessionId

sessionId 标识 Harness 会话,不是九个平台统一、稳定的消息投递地址。主动投递只使用机器人和已保存目标的稳定组合。

测试成功但对方没有看到消息

测试成功只证明平台接口接受发送。请继续检查机器人权限、平台限制、目标是否正确,以及客户端侧的消息过滤或归档设置。