内容审核指南

内容审核分两层:发送时同步过滤(敏感词 + 处置动作)决定消息能否放行;审核任务对已放行(replace 掩码)或需复核的消息做事后裁决。裁决结果通过 message.audit_result 回调推给应用服务器。本页说明两者的区别、处置动作、任务状态机、回调契约与验签,并给出可直接执行的命令。

与“控制台内容审核页”的关系

本页讲概念与端到端流程;控制台逐按钮的操作说明见 控制台 · 内容审核。两者引用同一组 Admin API 与同一份状态。

概念​

敏感词与审核任务的区别​

维度敏感词(同步)审核任务(异步)
触发时机消息发送路径,写入/投递之前敏感词命中且消息已放行(replace)后生成
作用拦截(block)或掩码(replace)人工/系统对已入库消息做二次裁决
结果block:1010,消息不入库;replace:入库且正文掩码approved 保留;rejected 标记 moderation=blocked 并通知发送者
数据应用级词表 + 一个全局处置动作内存环缓冲,上限 2000 条,重启不保留
回调不触发审核回调message.audit_result

处置动作​

动作发送时行为审核任务客户端表现
block(默认)命中即拒绝,返回 1010 SENSITIVE_WORD不生成任务(无 messageId)发送失败,消息不出现
replace消息入库,命中词替换为 ***生成 action=replaced 任务,带 messageId正常收到消息,正文已掩码

审核任务状态机​

状态进入条件可执行动作终态
pendingreplace 命中后自动创建approve / reject否
approveddecide{decision:"approve"}无(再次裁决返回 1001)是
rejecteddecide{decision:"reject"}无(再次裁决返回 1001)是

状态只允许 pending → approved 或 pending → rejected;没有回退或重新打开。任务本身是内存环缓冲(上限 2000),但裁决对消息的标记会写回消息并持久化。

与消息状态的关系

approve 不会改变消息正文(replace 模式下保留掩码);reject 会把消息正文标记 moderation=blocked,并向发送者投递 message.blocked 系统通知,但不会物理删除消息。

前置条件​

  • 服务端已启动,Admin API 可达:curl -sS http://127.0.0.1:8080/health 返回 {"ok":true}。
  • 已有应用与 appkey;测试用户可通过控制台或 admin.users.register 创建。
  • 应用已配置回调地址、订阅 message.audit_result,并能校验 HMAC 签名(否则收不到/无法确认裁决结果)。
  • 测试消息走客户端 WS 或 Open API 发布均可;建议使用隔离测试应用,避免污染生产审核队列。
  • 自托管版无“审核套餐/人工审核服务购买”;审核能力不产生费用(不适用项)。

步骤​

  1. 配置词表与处置动作

    在控制台「敏感词」写入词表,或调用 POST /api/admin/apps/{appkey}/sensitive-words。需要保留原文做人工复核时用 replace;强合规直接拦截用 block。

  2. 发送测试消息

    用客户端或 Open API 发送含敏感词的消息。block 应返回 1010;replace 应成功且正文出现 ***,同时在审核任务列表出现 pending 记录。

  3. 拉取待审任务
    GET /api/admin/apps/{appkey}/moderation/tasks?status=pending。注意 status 缺省就是 pending,all 会被归一到 pending;要查历史请分别传 approved/rejected。
  4. 裁决
    POST /api/admin/apps/{appkey}/moderation/tasks/{taskId}/decide,body {decision:"approve"|"reject", reason?}。已裁决任务再次裁决返回 1001。
  5. 验证回调

    裁决成功后服务端向回调地址投递 message.audit_result;检查 X-IM-Signature 与 decision/messageId/taskId。失败可在「回调服务」的投递日志中重试。

参数表​

敏感词​

操作方法与路径请求体 / 参数
读取词表GET /api/admin/apps/{appkey}/sensitive-words—(返回 words/action)
整体设置POST /api/admin/apps/{appkey}/sensitive-words{words:string[],action?:block|replace}
Open API 整体设置POST /im/v2/sensitive-word/set{words,action}
Open API 增量添加POST /im/v2/sensitiveword/add{word,replaceWord?};带 replaceWord 即切到 replace

审核任务​

操作方法与路径请求体 / 参数
任务列表GET /api/admin/apps/{appkey}/moderation/tasksstatus(pending/approved/rejected/all)、offset、limit
裁决POST /api/admin/apps/{appkey}/moderation/tasks/{taskId}/decide{decision:approve|reject,reason?}

审核回调 message.audit_result​

字段类型说明
messageIdstring被裁决消息 ID(replace 模式必有,block 无任务)
conversationTypeConversationType1 单聊 / 2 群 / 3 聊天室 / 4 系统
targetIdstring会话目标
senderIdstring发送者
decisionapprove / reject裁决结果
reasonstring?拒绝原因(透传裁决入参)
operatorstring?操作者(当前为 admin)
taskIdstring审核任务 ID
timestampint64回调事件时间(毫秒)

回调请求头与投递语义​

项值
请求方法POST,application/json; charset=utf-8
X-IM-Timestampepoch 毫秒;时间窗 300000ms
X-IM-Nonce8–64 字符随机串
X-IM-Signaturehex(hmac_sha256(secret, timestamp + "." + nonce + "." + rawBody)) 小写
超时 / 体积单次 5s;请求体 ≤256KB
重试最多 3 次,退避 1s / 5s / 30s;至少一次投递
幂等eventId 重试不变,应用服务器需按 eventId 去重

示例​

1. 配置 replace 词表并发送命中消息​

Shell
curl -sS -X POST "http://127.0.0.1:8080/api/admin/apps/demo/sensitive-words" \
  -H 'X-Admin-Secret: dev-admin' \
  -H 'content-type: application/json' \
  -d '{"words":["审核演示"],"action":"replace"}'

# 通过控制台调试器/客户端发送「这是审核演示消息」后:
curl -sS -X GET "http://127.0.0.1:8080/api/admin/apps/demo/moderation/tasks?status=pending&limit=20" \
  -H 'X-Admin-Secret: dev-admin'

2. 拒绝任务并查回调投递​

Shell
curl -sS -X POST "http://127.0.0.1:8080/api/admin/apps/demo/moderation/tasks/mt_1001/decide" \
  -H 'X-Admin-Secret: dev-admin' \
  -H 'content-type: application/json' \
  -d '{"decision":"reject","reason":"违反社区规范"}'

# 查看审核结果回调是否投递成功
curl -sS -X GET "http://127.0.0.1:8080/api/admin/apps/demo/callbacks/deliveries?eventType=message.audit_result&limit=20" \
  -H 'X-Admin-Secret: dev-admin'

# 失败可重试(用 items[].deliveryId)
curl -sS -X POST "http://127.0.0.1:8080/api/admin/apps/demo/callbacks/deliveries/dlv_3001/retry" \
  -H 'X-Admin-Secret: dev-admin'

3. 回调验签(Node.js,使用原始请求体)​

JavaScript
import crypto from 'node:crypto';

// 必须使用 raw body(不要用 JSON.parse 后再 stringify,字段顺序/空格会变)
export function verifyCallback(rawBody, headers, secret) {
  const timestamp = headers['x-im-timestamp'];
  const nonce = headers['x-im-nonce'];
  const signature = headers['x-im-signature'];
  if (!timestamp || !nonce || !signature) return false;

  // 时间窗 5 分钟,防重放
  if (Math.abs(Date.now() - Number(timestamp)) > 5 * 60 * 1000) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(String(timestamp) + '.' + String(nonce) + '.' + rawBody)
    .digest('hex');

  const a = Buffer.from(expected, 'utf8');
  const b = Buffer.from(signature, 'utf8');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// 幂等:同一 eventId 只处理一次
export async function handle(rawBody, headers, secret, seen) {
  if (!verifyCallback(rawBody, headers, secret)) return 401;
  const event = JSON.parse(rawBody);
  if (event.eventType !== 'message.audit_result') return 200;
  if (seen.has(event.eventId)) return 200;
  seen.add(event.eventId);
  // event.data: { messageId, decision, reason, taskId, ... }
  return 200;
}

4. 回调报文示例​

JSON
{
  "eventType": "message.audit_result",
  "eventId": "evt_9f2c1a",
  "appkey": "demo",
  "timestamp": 1750000000000,
  "nonce": "6b1d0f3a",
  "data": {
    "messageId": "m_1001",
    "conversationType": 1,
    "targetId": "bob",
    "senderId": "alice",
    "decision": "reject",
    "reason": "违反社区规范",
    "operator": "admin",
    "taskId": "mt_1001",
    "timestamp": 1750000000000
  }
}

常见问题​

block 会生成审核任务吗?​

不会。block 在入库前拒绝,没有 messageId,也不会触发 message.audit_result。

reject 后消息会被删除吗?​

不会。消息正文被标记 moderation=blocked,并给发送者发送 message.blocked 系统通知;客户端应按标记隐藏或折叠。

为什么任务列表默认只有 pending?​

status 缺省为 pending,且 all 会被归一到 pending;要查已裁决任务请显式传 approved 或 rejected。

任务重启后还在吗?​

不在。任务队列是内存环缓冲(上限 2000);已裁决消息的标记会持久化,但任务记录本身重启后不保留。

回调一直失败怎么办?​

检查 URL 可达、HTTPS 证书、5 秒内返回 2xx、请求体 ≤256KB;在「回调服务 → 投递日志」按 eventType=message.audit_result 查看响应码并手动重试。

验签失败但签名看起来对?​

最常见原因是用了重新序列化的 JSON。必须用原始 raw body;同时确认 secret 与服务端配置一致、时间戳未超出 5 分钟窗口。

block 与 replace 能按词分别配置吗?​

整体设置接口的 action 对整表生效;增量接口里带 replaceWord 的词会切到 replace。

相关链接​