集成

自定义应用 Bridge API

基于持续 HTTP NDJSON 流的自定义应用消息中转接口与字段说明。

自定义应用由外部 runner 主动连接 Mopheus。外发消息通过持续返回的 HTTP NDJSON 事件流投递;入站消息、附件和投递结果通过独立 HTTP 请求提交。Mopheus 不向 runner 发起回调,也不要求回调地址。所有示例路径均相对于 Mopheus API 根地址。

约定与认证

项目约定
Bridge 基础路径/api/v1/channel/bridge
协议版本事件与消息体的 version: 1;事件流请求头 X-Bridge-Protocol-Version: 1
Bridge 认证Authorization: Bearer mcb_<installation-uuid>_<secret>;令牌在创建或轮换时仅返回一次
JSON 响应成功为 {"success":true,"data":{...}};失败为 {"success":false,"code":"...","error":"...","requestId":"..."}。未出现的可选字段不要假设为 null
时间与 ID时间为 RFC 3339;内部 ID 为 UUID;平台 ID 是最长 255 字节的字符串,保持大小写和原值
HTTPS生产环境使用 HTTPS;二进制上传仍使用同一 Bridge Bearer 令牌

Bridge 令牌只授权所属安装,不能代替成员登录。创建、轮换和撤销使用成员认证及工作区权限;其余 /channel/bridge 路由统一由 Bridge 令牌中间件认证。HTTP 400 表示输入错误,401 表示令牌无效,403 表示渠道功能关闭或成员无权操作,404 表示资源不存在或不可访问,409 表示事件流已占用、租约失效或结果冲突,5xx 表示服务端或依赖故障。客户端应按 code 和 HTTP 状态处理错误,不解析 error 文案。

安装管理(成员 API)

以下接口位于 /api/v1/channel,需要成员登录和工作区请求头(与其他工作区 API 相同)。安装需要渠道管理权限。

方法与路径请求成功 data
POST /bridge/installations{"name":"示例应用","platformKey":"example","assigneeType":1,"assigneeId":"<agent-uuid>","externalBotId":"bot-1"}{"installation":{...}}
POST /bridge/installations/{installationId}/credentials空请求体{"bridgeToken":"mcb_..."};首次调用生成令牌,再次调用轮换并使旧令牌失效
DELETE /bridge/installations/{installationId}/credentials/{credentialId}无请求体;V1 的 credentialId 等于安装 IDnull;撤销当前令牌并断开事件流

platformKey 匹配 ^[a-z0-9][a-z0-9_-]{1,49}$;name 最长 100 字节,externalBotId 最长 255 字节。assigneeType 为 1(Agent)或 3(Team),assigneeId 必须属于当前工作区。创建安装不会生成令牌;在安装详情页点击“生成令牌”后,立即写入 runner 的秘密配置。令牌仅显示一次。

持续接收外发事件

GET /api/v1/channel/bridge/stream HTTP/1.1
Authorization: Bearer mcb_...
Accept: application/x-ndjson
X-Bridge-Protocol-Version: 1
X-Bridge-Runner-Id: example-runner
X-Bridge-Version: example/1.0.0
X-Bridge-Platform-Key: example
X-Bridge-Inbound-Capabilities: text,image,file,audio,video,reply,thread,mention
X-Bridge-Outbound-Capabilities: text,image,file,audio,video,reply,thread,mention

成功返回 HTTP 200、Content-Type: application/x-ndjson; charset=utf-8。响应体每行是一个完整 JSON 对象,服务端逐行刷新;连接不会在第一行后结束。必须逐行解析,忽略空行。一个安装同时只允许一条有效连接;冲突时返回 409。代理应关闭响应缓冲并允许长连接。断开后用指数退避重连,不能把流重连理解为投递成功。

{
  "version": 1,
  "type": "hello",
  "id": "<event-uuid>",
  "sentAt": "2026-10-14T10:00:00Z",
  "payload": {
    "connectionId": "<uuid>",
    "heartbeatSeconds": 20,
    "deliveryLeaseSeconds": 60,
    "acceptedCapabilities": {
      "inbound": [
        "text",
        "file"
      ],
      "outbound": [
        "text",
        "file"
      ]
    }
  }
}

{
  "version": 1,
  "type": "heartbeat",
  "id": "<event-uuid>",
  "sentAt": "2026-10-14T10:00:20Z",
  "payload": {
    "connectionId": "<uuid>"
  }
}

{
  "version": 1,
  "type": "delivery",
  "id": "<event-uuid>",
  "sentAt": "2026-10-14T10:00:22Z",
  "envelope": {
    "deliveryId": "<uuid>",
    "attemptId": "<uuid>",
    "attempt": 1,
    "leaseExpiresAt": "2026-10-14T10:01:22Z",
    "completedParts": [],
    "target": {
      "type": "chat",
      "conversationId": "group-1",
      "conversationType": "group",
      "threadId": "topic-1"
    },
    "replyTo": {
      "messageId": "platform-parent-1"
    },
    "content": {
      "text": "报告见附件",
      "format": "plain",
      "parts": [
        {
          "partId": "text:0",
          "type": "text",
          "text": "报告见附件"
        },
        {
          "partId": "file:0",
          "type": "attachment",
          "attachmentId": "<attachment-uuid>",
          "kind": "file",
          "filename": "report.pdf",
          "mimeType": "application/pdf",
          "sizeBytes": 20480,
          "downloadPath": "/api/v1/channel/bridge/deliveries/<delivery-uuid>/assets/<attachment-uuid>"
        }
      ]
    }
  }
}

上面三行代表三条独立事件,不是一个 JSON 数组。deliveryId 跨重试稳定,attemptId 每次派发变化。completedParts 是服务端可识别的已完成 part 列表;V1 服务端不持久化逐 part 结果,runner 必须自行保存 deliveryId + partId -> platformMessageId。target.type 是现有渠道外发目标枚举,常见值为 chat;conversationId、userId、threadId 只在适用目标中出现。平台发送成功后应先持久化真实平台消息 ID,再提交结果。没有收到结果响应时,用同一投递的已保存 part 记录重试。

提交入站消息

POST /api/v1/channel/bridge/messages,Content-Type: application/json,请求体最大 256 KiB:

{
  "version": 1,
  "type": "message.inbound",
  "id": "<request-uuid>",
  "sentAt": "2026-10-14T10:02:00Z",
  "payload": {
    "eventId": "platform-event-1",
    "messageId": "platform-message-1",
    "occurredAt": "2026-10-14T10:01:59Z",
    "conversation": {"id":"group-1","type":"group","title":"研发","threadId":"topic-1"},
    "sender": {"id":"user-1","kind":"user","stableId":"account-1","displayName":"李四"},
    "recipient": {"botId":"bot-1","addressedToBot":true},
    "replyTo": {"messageId":"platform-parent-1","rootMessageId":"platform-root-1"},
    "content": {"kind":"mixed","text":"请看报告","parts":[
      {"type":"text","text":"请看报告"},
      {"type":"mention","id":"user-2","label":"@王五"},
      {"type":"attachment","assetId":"<uploaded-asset-uuid>","kind":"file"}
    ]},
    "extensions": {"platformSpecific":"optional"}
  }
}

conversation.type: direct | group;sender.kind: user | bot | system;content.kind: text | image | file | audio | video | mixed | unknown。unknown 必须有可读 text。parts 保留顺序,类型为 text | mention | attachment | link | code | emoji | quote;attachment 必须提供已上传且属于同一安装、同一 messageId 的 assetId。content.text 与每段文字最多 64 KiB,最多 100 段、其中最多 20 个附件。eventId 与 messageId 都必填;同一平台消息的重试必须保留 messageId 和正文,不得用原 ID 表示编辑。

成功响应示例:

{"success":true,"data":{"messageId":"platform-message-1","status":"ingested","chatSessionId":"<uuid>","mediaState":"pending"}}
data.status处理方式
ingested消息已入库;不代表 Agent 已完成。带附件时 mediaState 可为 pending
duplicate安装内同一 messageId 已处理;终态
dropped按渠道规则丢弃,例如机器人消息或未绑定用户;可查看 reason
rejected时间过期、目标机器人不匹配等;终态
retry临时处理失败;保留本地事件,按 retryAfterMs 重发

发生网络错误或 5xx 时保留原消息并重试。超过 7 天或未来超过 5 分钟的 occurredAt 会返回 rejected。机器人和系统发送者会返回 dropped,防止消息回环。入站队列由 runner 持久化;收到终态前不能删除源事件。V1 去重只按安装和 messageId,不能识别同 ID 的正文变化,也不支持编辑或撤回事件。

图片和附件

入站先申请,再上传原始字节,最后提交引用该资源的消息。平台私有下载、鉴权和解密由 runner 完成;Mopheus 不抓取平台 URL。

POST /api/v1/channel/bridge/assets
Content-Type: application/json

{
  "messageId": "platform-message-1",
  "sourceKey": "file:0",
  "kind": "file",
  "filename": "report.pdf",
  "mimeType": "application/pdf",
  "sizeBytes": 20480,
  "sha256": "<64位十六进制SHA-256>"
}

成功 data 为 {"assetId":"<uuid>","state":"upload","uploadPath":"/api/v1/channel/bridge/assets/<uuid>"};已上传资源的 state 为 ready,此时无需重复 PUT。sourceKey 是同一消息内稳定的附件标识。单个文件最多 25 MiB;文件名不得带路径。sizeBytes 和 sha256 在申请阶段校验格式与上限;V1 不把申请摘要持久化,上传时另按实际字节校验 X-Content-SHA256。

PUT /api/v1/channel/bridge/assets/<assetId>
Content-Type: application/pdf
X-Bridge-Filename: report.pdf
X-Content-SHA256: <本次上传原始字节的SHA-256>

<二进制字节>

成功 data 为 {"state":"ready"}。上传中冲突返回 409;重复上传已就绪资源也返回 ready。V1 不支持分片、Range 和断点续传。提交入站消息前应等到所有附件就绪。

外发附件使用事件中的 downloadPath:

GET /api/v1/channel/bridge/deliveries/<deliveryId>/assets/<attachmentId>
X-Bridge-Attempt-Id: <attemptId>

此接口返回 application/octet-stream 原始字节,不包裹 JSON。只有当前有效投递尝试能下载本安装、当前消息的附件;无权或失效时返回 JSON 错误。

确认、失败与续租

外发所有 part 成功后提交真实平台消息 ID:

POST /api/v1/channel/bridge/deliveries/<deliveryId>/result
Content-Type: application/json

{
  "attemptId": "<uuid>",
  "status": "sent",
  "parts": [
    {
      "partId": "text:0",
      "platformMessageId": "platform-1"
    },
    {
      "partId": "file:0",
      "platformMessageId": "platform-2"
    }
  ]
}

sent 必须包含当前投递的全部 part,partId 不能重复或未知,platformMessageId 不可为空。成功 data 为 {"deliveryId":"<uuid>","attemptId":"<uuid>","status":"sent","completedParts":null};若字段未出现,客户端应按空列表处理。相同成功尝试重复提交会返回 sent。失败请求为 {"attemptId":"<uuid>","status":"failed","category":"rate_limited","platformCode":"429","message":"try later","retryAfterMs":30000,"parts":[]}。可重试分类为 rate_limited | transient | unknown;其他分类或达到最大尝试次数进入 failed。尚可重试时返回 status: "pending"。服务端只保存整条投递状态,不能作为逐 part 成功日志。

长时间上传/平台发送前可续租:POST /api/v1/channel/bridge/deliveries/<deliveryId>/renew,请求体 {"attemptId":"<uuid>"};成功 data 为 {"leaseExpiresAt":"<RFC3339时间>"}。每次延长至当前时间后 60 秒,且不超过任务过期时间。尝试失效、租约已过期或被替换时返回 409;runner 不应再提交旧 attemptId 的结果。平台发送成功但本地日志写入前崩溃可能重复发送,协议只保证至少一次投递。优先使用平台支持的幂等键 deliveryId:partId。

用户绑定与示例

外部用户收到一次性绑定令牌后,由已登录的 Mopheus 成员调用 POST /api/v1/channel/bind,请求体 {"token":"<one-time-binding-token>"};成功 data 为绑定记录。此接口使用成员认证,不使用 Bridge 令牌。

可运行的 runner 和协议检查脚本见代码仓库的 examples/channel-bridge/ 与 scripts/test-channel-bridge.sh。生产 runner 需要持久化入站消息和外发 part 结果,并实现真实平台发送适配器;打印消息并回报成功会造成消息丢失。

示例 runner 的平台 HTTP 接口

这一节是 examples/channel-bridge/runner.py、runner.go、runner.mjs 对平台提供的示例接口,不是 Mopheus 服务端路由。runner 默认监听 127.0.0.1:8765;外网监听需设置 MOPHEUS_BRIDGE_WEBHOOK_TOKEN,平台请求携带 X-Platform-Token。平台事件先持久化,runner 才返回 HTTP 202。

POST http://127.0.0.1:8765/platform/messages
Content-Type: application/json
X-Platform-Token: <仅在配置时必需>

{"messageId":"platform-msg-1","eventId":"event-1","occurredAt":"2026-10-14T10:01:59Z","conversation":{"id":"group-1","type":"group","title":"研发","threadId":"topic-1"},"sender":{"id":"user-1","kind":"user","displayName":"李四"},"recipient":{"botId":"bot-1","addressedToBot":true},"content":{"kind":"mixed","text":"报告","parts":[{"type":"text","text":"报告"},{"type":"attachment","kind":"file","filename":"report.txt","mimeType":"text/plain","dataBase64":"aGVsbG8="}]}}
平台字段必需转成 Mopheus 字段
messageId是payload.messageId;同一平台消息重试必须不变
eventId、occurredAt否payload.eventId、payload.occurredAt;缺省分别为 messageId 和接收时间
conversation是原样映射至 payload.conversation,至少需要 id 和 type
sender是原样映射至 payload.sender,至少需要 id 和 kind
recipient否映射至 payload.recipient;缺省 addressedToBot: true
content.text、content.parts否保留文本与有序 parts;无 parts 时缺省 kind: text
附件 dataBase64附件时必需runner 解码后先调用 POST /assets、PUT /assets/{assetId},再用 assetId 替换原始字节

成功返回 HTTP 202 {"accepted":true,"messageId":"platform-msg-1"},表示 runner 已持久化,不表示 Mopheus 已完成;同 ID 不同请求返回 409,缺字段或非法 JSON 返回 400,令牌错误返回 401。示例请求体最多 36 MiB,单附件最多 25 MiB。收到 202 后由 runner 承担重试。

外发时 runner 将 Mopheus delivery.envelope 的每个 part POST 到 MOPHEUS_BRIDGE_PLATFORM_SEND_URL。示例平台发送 API 请求与响应:

POST http://127.0.0.1:8766/send
Content-Type: application/json

{
  "deliveryId": "<uuid>",
  "partId": "file:0",
  "idempotencyKey": "<uuid>:file:0",
  "target": {
    "type": "chat",
    "conversationId": "group-1",
    "conversationType": "group"
  },
  "replyTo": null,
  "part": {
    "partId": "file:0",
    "type": "attachment",
    "filename": "report.txt",
    "mimeType": "text/plain"
  },
  "assetPath": ".bridge-downloads/<uuid>-file:0",
  "attachment": {
    "filename": "report.txt",
    "mimeType": "text/plain",
    "dataBase64": "aGVsbG8="
  }
}

HTTP/1.1 200 OK
Content-Type: application/json

{"platformMessageId":"sim-123"}

文本 part 没有 attachment。平台应按 idempotencyKey 去重;平台返回非 2xx 或缺少非空 platformMessageId 时,runner 不会向 Mopheus 报告 sent。platform_simulator.py serve 提供上述 /send 和只读 /messages 发件箱,供三种 runner 联调。真实平台可替换该 URL,或用 MOPHEUS_BRIDGE_SEND_COMMAND 包装 SDK。

字段字典

以下“必需”指请求方必须提供;服务端事件字段由 Mopheus 填写。JSON 名称区分大小写,均为 camelCase。<uuid> 表示 UUID 字符串,<time> 表示 RFC 3339 时间字符串。

安装请求与响应

字段类型/必需含义
namestring / 是页面显示的应用名称,最长 100 字节;不参与平台路由
platformKeystring / 是开发者定义的平台标识;必须与事件流头 X-Bridge-Platform-Key 相同
assigneeTypeinteger / 是负责处理消息的对象类型:1 为 Agent,3 为 Team
assigneeIdUUID / 是对应 Agent 或 Team 的工作区内 ID
externalBotIdstring / 否外部平台的机器人 ID;入站 recipient.botId 非空时必须匹配
installation.idUUID / 创建响应自定义应用安装 ID,同时也是 V1 凭据路径中的 credentialId
bridgeTokenstring / 凭据响应在详情页生成或轮换时只返回一次;轮换后旧值立即无效

入站信封 POST /messages

字段类型/必需含义
versioninteger / 是协议版本,目前只能为 1
typestring / 是事件类型,目前只能为 message.inbound
idUUID / 是这次 HTTP 事件的传输 ID;重试可重新生成,不作为消息去重键
sentAttime / 是runner 提交本次 HTTP 请求的时间
payload.eventIdstring / 是平台事件 ID,最长 255 字节;用于追踪该平台事件
payload.messageIdstring / 是平台消息 ID,最长 255 字节;同一安装的入站去重键
payload.occurredAttime / 是平台消息真实发生时间;超过 7 天或未来超过 5 分钟会拒绝
payload.conversation.idstring / 是平台会话 ID;区分私聊/群聊的会话身份
payload.conversation.typestring / 是direct 私聊或 group 群聊
payload.conversation.titlestring / 否群聊显示名称,不参与去重
payload.conversation.threadIdstring / 否平台话题/线程 ID;用于回复到同一话题
payload.sender.idstring / 是平台发送者 ID;用于账号绑定与消息归属
payload.sender.kindstring / 是user、bot、system;后两者丢弃以避免回环
payload.sender.stableIdstring / 否平台跨会话稳定的账号 ID,可供身份匹配
payload.sender.displayNamestring / 否发送者的显示名称
payload.recipient.botIdstring / 否目标机器人 ID;非空时与安装的 externalBotId 核对
payload.recipient.addressedToBotboolean / 是此消息是否明确发给机器人;影响群聊消息路由
payload.replyTo.messageIdstring / 有引用时被引用的外部平台消息 ID
payload.replyTo.rootMessageIdstring / 否引用链的根消息 ID
payload.extensionsobject / 否平台专有的扩展数据;不要把秘密放在这里

正文与有序 parts

字段类型/必需含义
content.kindstring / 是总体内容类型:text、image、file、audio、video、mixed、unknown;unknown 必须有可读文本
content.textstring / 是可读正文或降级文本;最多 64 KiB;有 parts 时按 parts 顺序生成展示文本
content.partsarray / 否按平台显示顺序排列的片段,最多 100 个,附件最多 20 个
parts[].typestring / 是text、mention、attachment、link、code、emoji、quote
parts[].textstring / 文本类时文本、代码或引用的内容;每段最多 64 KiB
parts[].idstring / mention 时被提及的平台用户 ID,最长 255 字节
parts[].labelstring / mention/link/emoji 时展示标签,例如 @李四 或链接标题
parts[].assetIdUUID / attachment 时通过 Bridge 附件申请和上传后取得的 ID;不是平台文件 ID
parts[].kindstring / 否附件类型,例如 image 或 file
parts[].urlstring / link 时链接地址;平台私有附件 URL 不应放在这里代替上传
parts[].languagestring / 否代码块语言提示
parts[].unicode、platformCodestring / 否表情的 Unicode 或平台自定义编码
parts[].authorLabelstring / 否引用作者的展示名称

parts 中各类型使用适用字段;未使用的字段可省略。mention 必须有 id 与 label,link 必须有 url 与 label,emoji 必须有 label。Mopheus 展示正文从有序 parts 计算,而 slash 命令只从 text part 读取,避免引用、提及等意外触发命令。

入站结果与附件

字段类型/必需含义
data.messageIdstring / 响应对应平台消息 ID
data.statusstring / 响应ingested、duplicate、dropped、rejected、retry;含义见上文状态表
data.reasonstring / 否丢弃/拒绝原因,例如 needs_binding、expired_event、bot_mismatch
data.chatSessionIdUUID / 响应已建立的 Mopheus 会话 ID;无会话时可能为全零 UUID
data.mediaStatestring / 响应none 或 pending;pending 表示附件绑定仍由异步流程完成
data.retryAfterMsinteger / retry 时runner 下次重试前建议等待的毫秒数
assets.messageIdstring / 是附件所属平台消息 ID,必须与后续 /messages 一致
assets.sourceKeystring / 是消息内稳定的附件位置标识,例如 file:0;重试时保持不变
assets.kindstring / 是图片、文件、音频或视频等类型提示
assets.filenamestring / 是原文件名,不含目录路径,最长 255 字节
assets.mimeTypestring / 是MIME 类型,最长 100 字节
assets.sizeBytesinteger / 是原始二进制大小,必须大于零且不超过 25 MiB
assets.sha256string / 是原始二进制 SHA-256 的 64 位十六进制值;申请时只校验格式
data.assetIdUUID / 响应Mopheus 附件 ID,供入站 part 的 assetId 引用
data.statestring / 响应upload 需继续 PUT,ready 可直接发送消息
data.uploadPathstring / 响应PUT 原始字节的相对路径
X-Content-SHA256header / PUT 必需本次 PUT 实际字节的 SHA-256;服务端逐字节校验
X-Bridge-Filenameheader / PUT 必需本次上传的文件名

外发事件、结果与续租

字段类型/必需含义
version、type、id、sentAt事件顶层 / 服务端协议版本、hello/heartbeat/delivery 类型、事件 UUID 和发送时间
hello.connectionIdUUID / 服务端当前事件流连接 ID,不是投递 ID
hello.heartbeatSecondsinteger / 服务端预计心跳间隔,单位秒
hello.deliveryLeaseSecondsinteger / 服务端初始投递租约时长,单位秒
hello.acceptedCapabilities.inbound/outboundstring[] / 服务端服务端接受的收发能力列表
heartbeat.connectionIdUUID / 服务端对应的流连接 ID
envelope.deliveryIdUUID / 服务端外发任务 ID,跨重试不变
envelope.attemptIdUUID / 服务端当前派发尝试 ID;续租、附件下载和结果提交必须使用它
envelope.attemptinteger / 服务端当前任务第几次派发,从 1 开始
envelope.leaseExpiresAttime / 服务端当前尝试的租约截止时间
envelope.expiresAttime / 否整条外发任务的最终过期时间
envelope.completedPartsstring[] / 服务端服务端已知完成的 part ID;V1 通常为空,不能替代 runner 本地日志
envelope.target.typestring / 服务端chat 发往会话,user 发往用户
envelope.target.conversationIdstring / chat 时平台会话 ID
envelope.target.conversationTypestring / chat 时direct 或 group
envelope.target.userIdstring / user 时平台目标用户 ID
envelope.target.threadIdstring / 否平台话题/线程 ID
envelope.replyTo.messageIdstring / 否应引用的外部平台消息 ID
envelope.content.textstring / 服务端整条消息的可读文本快照
envelope.content.formatstring / 服务端V1 为 plain
envelope.content.parts[].partIdstring / 服务端片段稳定 ID;与 deliveryId 组成平台幂等键
parts[].type、textstring / 服务端片段类型与文本内容
parts[].attachmentIdUUID / 附件时Mopheus 附件 ID
parts[].kind、filename、mimeType、sizeBytes附件元数据 / 附件时平台发送附件所需的类型、文件名、MIME 和字节大小
parts[].downloadPathstring / 附件时当前尝试可 GET 的附件相对路径;需带 X-Bridge-Attempt-Id
result.attemptIdUUID / 是本次结果对应的有效尝试
result.statusstring / 是sent 全部成功,或 failed 本次发送失败
result.parts[].partIdstring / sent 时已发送片段 ID;成功结果需覆盖全部片段
result.parts[].platformMessageIdstring / sent 时平台实际返回的消息 ID,不能为空
result.platformRequestIdstring / 否平台请求追踪 ID,便于排障
result.category、platformCode、messagestring / failed 时可选错误分类、平台错误码、可读错误信息
result.retryAfterMsinteger / 否可重试失败后的建议等待毫秒数
renew.attemptIdUUID / 是要延长的有效尝试 ID
renew.data.leaseExpiresAttime / 响应续租后的新截止时间

结果响应中的 data.deliveryId、data.attemptId、data.status 分别回显任务、尝试和当前状态。status 可能为 sent、pending 或 failed。data.completedParts 在 V1 可能为 null 或空列表,客户端应按空列表处理。失败后的新尝试会产生新的 attemptId,旧尝试不应继续写入。