内容审核指南
内容审核分两层:发送时同步过滤(敏感词 + 处置动作)决定消息能否放行;审核任务对已放行(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 | 正常收到消息,正文已掩码 |
审核任务状态机
| 状态 | 进入条件 | 可执行动作 | 终态 |
|---|---|---|---|
pending | replace 命中后自动创建 | approve / reject | 否 |
approved | decide{decision:"approve"} | 无(再次裁决返回 1001) | 是 |
rejected | decide{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 发布均可;建议使用隔离测试应用,避免污染生产审核队列。
- 自托管版无“审核套餐/人工审核服务购买”;审核能力不产生费用(不适用项)。
步骤
- 配置词表与处置动作
在控制台「敏感词」写入词表,或调用
POST /api/admin/apps/{appkey}/sensitive-words。需要保留原文做人工复核时用replace;强合规直接拦截用block。 - 发送测试消息
用客户端或 Open API 发送含敏感词的消息。block 应返回 1010;replace 应成功且正文出现
***,同时在审核任务列表出现 pending 记录。 - 拉取待审任务
GET /api/admin/apps/{appkey}/moderation/tasks?status=pending。注意status缺省就是 pending,all会被归一到 pending;要查历史请分别传approved/rejected。 - 裁决
POST /api/admin/apps/{appkey}/moderation/tasks/{taskId}/decide,body{decision:"approve"|"reject", reason?}。已裁决任务再次裁决返回 1001。 - 验证回调
裁决成功后服务端向回调地址投递
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/tasks | status(pending/approved/rejected/all)、offset、limit |
| 裁决 | POST /api/admin/apps/{appkey}/moderation/tasks/{taskId}/decide | {decision:approve|reject,reason?} |
审核回调 message.audit_result
| 字段 | 类型 | 说明 |
|---|---|---|
messageId | string | 被裁决消息 ID(replace 模式必有,block 无任务) |
conversationType | ConversationType | 1 单聊 / 2 群 / 3 聊天室 / 4 系统 |
targetId | string | 会话目标 |
senderId | string | 发送者 |
decision | approve / reject | 裁决结果 |
reason | string? | 拒绝原因(透传裁决入参) |
operator | string? | 操作者(当前为 admin) |
taskId | string | 审核任务 ID |
timestamp | int64 | 回调事件时间(毫秒) |
回调请求头与投递语义
| 项 | 值 |
|---|---|
| 请求方法 | POST,application/json; charset=utf-8 |
X-IM-Timestamp | epoch 毫秒;时间窗 300000ms |
X-IM-Nonce | 8–64 字符随机串 |
X-IM-Signature | hex(hmac_sha256(secret, timestamp + "." + nonce + "." + rawBody)) 小写 |
| 超时 / 体积 | 单次 5s;请求体 ≤256KB |
| 重试 | 最多 3 次,退避 1s / 5s / 30s;至少一次投递 |
| 幂等 | eventId 重试不变,应用服务器需按 eventId 去重 |
示例
1. 配置 replace 词表并发送命中消息
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. 拒绝任务并查回调投递
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,使用原始请求体)
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. 回调报文示例
{
"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。