场景教程:内容审核拦截
内容审核有三道闸门,按拦截时机从早到晚排列:敏感词本地判定(同步、零网络)、发送前同步回调(阻塞、默认 500ms 超时)、发送后异步审核任务(人工/策略裁决 + 回推)。本教程给出三道闸门的取舍、真实字段与可运行代码,并明确哪些结论“不可伪造”。
| 闸门 | 触发点 | 能力 | 代价 / 限制 |
|---|---|---|---|
| 敏感词 | 发送路径(文本消息) | block(默认,返回 1010)或 replace(掩码后放行) | 仅匹配文本;大小写不敏感子串匹配;只做词表,不做语义模型 |
发送前同步回调 message.publish.before | 消息入库前,逐条阻塞询问 App Server | 应用返回 pass / replace / block(1035) | 默认超时 500ms(可配 100–2000ms);超时按 syncDefaultAction 降级;每发送最多一次,不异步重试 |
| 异步审核任务 | 命中敏感词且动作是 replace 时生成待审任务 | 运营裁决 approve/reject;reject 标记消息 moderation=blocked 并发送 message.blocked 系统通知 + message.audit_result 回调 | 任务环形上限 2000;只覆盖文本命中词表的消息 |
第一道:敏感词(同步、无网络)
控制台「敏感词」或 Admin API 配置词表与动作。命中文本消息时:动作为 block(默认)直接返回 1010;动作为 replace 时把命中词替换为掩码后放行,同时生成一条待审任务供人工复核。
import { AdminClient } from '@ni-paas-im/server-sdk';
const admin = new AdminClient({ baseUrl: 'http://127.0.0.1:8080', adminSecret: process.env.IM_ADMIN_SECRET });
// 词表 + 处置动作;block=直接拒绝,replace=掩码放行并生成待审任务
await admin.request('POST', '/apps/demo/sensitive-words', {
words: ['违规词A', '违规词B'],
action: 'replace',
});发送方会收到 code=0,但消息正文已被掩码;接收方看到掩码文本。若业务需要“提示发送方被替换”,请在发送成功后对比回包 content.text 与原始文本。
第二道:发送前同步回调(应用自己裁决)
配置回调时开启同步事件 message.publish.before。IM 服务会在消息入库前把消息体 POST 给你的 App Server,阻塞等待决策:pass 放行、replace 用返回的 content 替换后放行、block 拒绝并让发送方收到 1035。
// App Server:接收发送前回调(Node / 原生 http 均可)
import { createServer } from 'node:http';
import { createHmac, timingSafeEqual } from 'node:crypto';
const SECRET = process.env.IM_CALLBACK_SECRET;
createServer(async (req, res) => {
const chunks = [];
for await (const chunk of req) chunks.push(chunk);
const raw = Buffer.concat(chunks).toString('utf8');
// 1) 验签:hex(hmac_sha256(secret, timestamp + '.' + nonce + '.' + rawBody))
const ts = req.headers['x-im-timestamp'];
const nonce = req.headers['x-im-nonce'];
const sig = req.headers['x-im-signature'];
const expected = createHmac('sha256', SECRET).update(`${ts}.${nonce}.${raw}`).digest('hex');
if (!sig || sig.length !== expected.length || !timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
res.writeHead(401).end('bad signature');
return;
}
const event = JSON.parse(raw);
if (event.eventType !== 'message.publish.before') {
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ code: 0 }));
return;
}
const text = event.data?.content?.text ?? '';
let decision = { action: 'pass' };
if (await hitRiskModel(event.data.senderId, text)) {
decision = { action: 'block', reason: 'risk_model' };
} else if (await needMask(text)) {
// replace:返回完整 Content,客户端与历史都看到替换后的内容
decision = { action: 'replace', content: { type: 'text', text: maskSensitive(text) } };
}
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ code: 0, data: decision }));
}).listen(9090);| 决策 | 返回体 | 发送方结果 | 服务端行为 |
|---|---|---|---|
pass | {code:0,data:{action:"pass"}} | code=0,正常入库/投递 | 按原内容处理 |
replace | {code:0,data:{action:"replace",content:{…}}} | code=0,消息内容被替换 | 用返回 content 入库并投递 |
block | {code:0,data:{action:"block",reason:"…"}} | 1035 消息被拒绝 | 不入库、不投递;记录同步回调投递日志 |
| 超时/故障 | — | 按 syncDefaultAction:默认 pass 放行(可配 block) | 记录同步回调投递日志(failed) |
默认超时 500ms(可配 100–2000ms),每次发送最多调用一次且不异步重试;超时即按降级动作处理,等于放弃裁决权。
回调必须校验 X-IM-Signature(HMAC-SHA256 + 时间戳窗口 300s),否则任何人都能伪造放行/拒绝。
决策要幂等/无副作用:同一消息可能因客户端重试再次到达。
第三道:异步审核任务(人工/策略裁决)
当敏感词动作为 replace 时,发送路径会生成一条审核任务。运营在控制台或通过 Admin API 裁决:approve 保留消息;reject 把消息正文标记 moderation=blocked,并向发送者发送 message.blocked 系统通知,同时回调 message.audit_result。
// 拉取待审任务(status: pending | approved | rejected | all;环形上限 2000)
const tasks = await admin.request('GET', '/apps/demo/moderation/tasks?status=pending&offset=0&limit=20');
// 裁决:reject 会标记消息并向发送者发送 message.blocked 系统通知
await admin.request('POST', `/apps/demo/moderation/tasks/${tasks.items[0].taskId}/decide`, {
decision: 'reject',
reason: 'policy_violation',
});// 客户端:处理被拦截的系统通知(conversationType=4 / targetId="system")
client.addEventListener(Events.MESSAGES, ({ messages }) => {
for (const message of messages) {
if (message.conversationType === ConversationType.SYSTEM) showSystemNotice(message.content);
}
});
// 拉取系统通知历史
const { messages } = await client.getSystemMessages({ limit: 20 });错误码与用户提示
| 错误码 | 来源 | 建议文案 |
|---|---|---|
1010 | 敏感词 block | “内容包含不支持的信息,请修改后再发送” |
1035 | 发送前回调 block | 展示回调 reason 对应的业务提示,不要暴露风控规则细节 |
1008 | 审核/发送频率 | 退避后重试 |
1021 | 未开通审核相关服务 | 检查控制台服务开通 |
限制与诚实边界
| 项 | 事实 |
|---|---|
| 敏感词匹配 | 仅文本消息;大小写不敏感子串;无分词/变体还原/语义模型 |
| 词表规模 | 服务端未设硬上限;词表过大将线性扫描,建议控制在业务可维护范围内 |
| 同步回调 | 每消息最多一次;默认超时 500ms;超时按降级动作,不重试 |
| 异步任务 | 环形上限 2000;只覆盖 replace 命中;reject 只标记消息与通知,不删除历史 |
| 图片/语音/文件 | 不参与敏感词;需要审核请用同步回调或业务侧自行处理媒体 |
| 审核回调 | message.audit_result 走标准回调签名与重试(最多 3 次,退避 1s/5s/30s) |
相关页面
- 内容审核指南(概念与配置)
- 控制台 · 内容审核 · 控制台 · 敏感词
- 回调服务(签名 / 重试 / 事件)
- 状态码与错误码
- 教程:直播房 / 聊天室场景