场景教程:内容审核拦截

内容审核有三道闸门,按拦截时机从早到晚排列:敏感词本地判定(同步、零网络)、发送前同步回调(阻塞、默认 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 时把命中词替换为掩码后放行,同时生成一条待审任务供人工复核。

JavaScript
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',
});
replace 的客户端表现

发送方会收到 code=0,但消息正文已被掩码;接收方看到掩码文本。若业务需要“提示发送方被替换”,请在发送成功后对比回包 content.text 与原始文本。

第二道:发送前同步回调(应用自己裁决)​

配置回调时开启同步事件 message.publish.before。IM 服务会在消息入库前把消息体 POST 给你的 App Server,阻塞等待决策:pass 放行、replace 用返回的 content 替换后放行、block 拒绝并让发送方收到 1035。

JavaScript
// 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。

JavaScript
// 拉取待审任务(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',
});
JavaScript
// 客户端:处理被拦截的系统通知(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)

相关页面​