后台监听与通知桥接
本文档说明第三方后台如何接收 dispatch-web 的会议、呼叫、软手柄通知,并通过 @scooper/meeting-sdk 转发给第三方页面。
什么时候需要后台桥接
默认接入方式是前端直接加载 scooper.dispatch.js,由它连接 dispatch-web 的 CometD/SSE 并触发 SDK callback。以下场景建议使用后台桥接:
- 第三方页面不能直连 dispatch-web,存在跨域、网络隔离或统一网关限制。
- 第三方希望后台统一做鉴权、日志、消息过滤、脱敏或多端转发。
- 第三方移动端、桌面端、H5 需要共用同一套通知出口。
桥接后,前端收到的仍是 SDK 统一事件,例如 meeting:status、meeting:member、call:status、dispatch:disconnect。
监听链路
dispatch-web CometD channel
-> 第三方后台 CometD client
-> createScooperBackendListener(...)
-> createSseHandler(...) / attachWebSocketServer(...)
-> SDK backendListener
-> sdk.on(...) / callbacks.onX(...)
如果前端同时启用了 scooper.dispatch.js 直连和 backendListener 桥接,部分事件会收到两次。生产环境通常二选一:内网直连用默认方式,跨域/网关场景用后台桥接。
后台订阅计划
SDK 服务端模 块复用前端 SDK 的 DISPATCH_EVENT_MAP,默认会订阅以下类型的 channel:
| 类型 | 示例 channel | SDK 事件 |
|---|---|---|
| 连接 | /conn, /cdispatch/disconnect | dispatch:connectionChanged, dispatch:disconnect |
| 呼叫 | /{accId}/call/in, /{accId}/call/status, /{accId}/call/hold | call:incomingQueue, call:status, call:hold |
| 录音/记录 | /{accId}/call/record, /{accId}/call/recordStatus, /{accId}/meet/record | call:recordNotify, call:recordStatusNotify, meeting:record |
| 会议 | /{accId}/meet/status, /{accId}/meet/join, /{accId}/meet/leave, /{accId}/meet/memsts | meeting:status, meeting:member, meeting:list |
| 会议扩展 | /meetScreenSetIndex, /meetMixScreen, /meetSplitScreen, /meetShareTel | meeting:screenSetIndex, meeting:mixScreen, meeting:splitScreen, meeting:shareTel |
| 软手柄 | /shandlePreemptRegister | dispatch:softHandlerRegister |
运行时可以查看后台订阅计划:
const { createBackendSubscriptionPlan } = require("@scooper/meeting-sdk/server");
console.table(createBackendSubscriptionPlan({ accountId: "1001" }));
Express + SSE 示例
const express = require("express");
const { createScooperBackendListener, createSseHandler } = require("@scooper/meeting-sdk/server");
const { createCometDTransport } = require("@scooper/meeting-sdk/server/transports");
const cometdClient = createYourCometDClientSomehow();
const listener = createScooperBackendListener({
accountId: "1001",
transport: createCometDTransport({
client: cometdClient,
url: "https://dispatch.example.com/dispatch-web/cometd",
requestHeaders: {
"Sc-Token": process.env.DISPATCH_TOKEN
}
})
});
listener.on("message", (message) => {
console.log("[dispatch notify]", message.channel, message.event);
});
await listener.start();
const app = express();
app.get("/scooper-events", createSseHandler(listener, {
eventName: "scooper-event",
heartbeatMs: 25000,
allowOrigin: "*"
}));
app.listen(3000);
前端接入:
const sdk = ScooperMeetingSDK.create({
backendListener: {
enabled: true,
transport: "sse",
url: "/scooper-events"
},
callbacks: {
onMeetingMember(member) {
console.log("成员变化", member);
},
onCallIncomingQueue(queue) {
console.log("呼入队列", queue);
}
}
});
await sdk.init();
WebSocket 转发示例
const http = require("http");
const WebSocket = require("ws");
const { createScooperBackendListener, attachWebSocketServer } = require("@scooper/meeting-sdk/server");
const listener = createScooperBackendListener({
accountId: "1001",
transport: yourTransport
});
await listener.start();
const server = http.createServer();
const wss = new WebSocket.Server({ server, path: "/scooper-events" });
attachWebSocketServer(listener, wss);
server.listen(3000);
前端配置:
const sdk = ScooperMeetingSDK.create({
backendListener: {
enabled: true,
transport: "websocket",
url: "ws://localhost:3000/scooper-events"
}
});
转发消息格式
SSE/WebSocket 发给前端的消息格式固定:
{
"source": "scooper-dispatch",
"event": "meeting:status",
"channel": "/1001/meet/status",
"accountId": "1001",
"payload": {
"meetId": "meet-001",
"status": "created"
},
"meta": {
"constKey": "MEET_STS",
"category": "meeting",
"description": "会场状态变化:新增、销毁、锁定、录音、编辑等"
},
"receivedAt": "2026-07-06T10:00:00.000Z"
}
SDK 会先触发:
callbacks.onBackendListenerMessage(message)
sdk.on(ScooperMeetingSDK.EVENT.BACKEND_LISTENER_MESSAGE, handler)
随后按 message.event 再触发业务事件:
callbacks.onMeetingStatus(message.payload)
sdk.on(ScooperMeetingSDK.EVENT.MEETING_STATUS, handler)
自定义 transport
如果第三方已有消息中间件,不想使用 CometD client,可以直接实现 transport:
const listener = createScooperBackendListener({
accountId: "1001",
transport: {
async connect() {},
async subscribe(channel, handler) {
const off = mq.subscribe(channel, handler);
return off;
},
async disconnect() {}
}
});
前端也支持自定义 transport:
const sdk = ScooperMeetingSDK.create({
backendListener: {
enabled: true,
transport: {
connect({ onOpen, onMessage, onClose }) {
const off = myBus.on("dispatch-message", onMessage);
onOpen();
return () => {
off();
onClose();
};
}
}
}
});
过滤订阅
只监听会议事件:
const listener = createScooperBackendListener({
accountId: "1001",
categories: ["meeting"],
transport
});
只监听指定 SDK 事件:
const listener = createScooperBackendListener({
accountId: "1001",
events: [
"meeting:status",
"meeting:member",
"call:status"
],
transport
});
对接注意事项
accountId是订阅账号级 channel 必需参数,会被替换成/{accId}前缀。- 后台桥接建议只转发当前业务账号有权接收的 channel,不要把所有账号消息广播给所有前端。
- SSE 不能自定义请求头,token 可以放在 query 中;生产环境建议后台校验当前登录态后再建立连接。
- WebSocket/SSE 断线重连策略通常放在第三方网关或页面层;SDK 会通过
backend:error、backend:disconnected通知调用方。 - 需要扩展定制 channel 时,在
extraEvents中增加{ event, backendChannels, category, description },前端会收到同名 SDK 事件。