【💰】有人用过微信小程序的文本内容安全识别(wxa/msg_sec_check)接口吗

40 条回复
143 次浏览

现在我做的一个小程序一直不给我过,发的帖子和评论一直不过,因为一些涉黄的评论可以发出,导致审核一直不给我过,现在返回的是 label100,可以过,

image

金币池
💰 684 金币

金币池金币数量会随着回复数量动态增加,回复有概率获得金币池中部分金币奖励。

OP

import logging
from dataclasses import dataclass
from typing import Any, Optional, Iterable, List

import requests
from django.conf import settings
from django.core.cache import cache
from rest_framework.exceptions import ValidationError

logger = logging.getLogger(name)

def _normalize_wx_resp(raw: Optional[dict]) -> dict:
raw = raw or {}
return {
"errcode": raw.get("errcode", 0),
"errmsg": raw.get("errmsg", ""),
"trace_id": raw.get("trace_id", ""),
"result": raw.get("result") or {},
"detail": raw.get("detail") or [],
}

def check_text_and_report(
*,
user,
text: str,
scene: int,
strict: bool = True,
require_openid: bool = True,
) -> dict:
appid = getattr(settings, "WECHAT_APPID", "") or getattr(settings, "WX_MINI_APPID", "")
secret = getattr(settings, "WECHAT_SECRET", "") or getattr(settings, "WX_MINI_SECRET", "")
if not appid or not secret:
raise ValidationError("未配置 WECHAT_APPID/WECHAT_SECRET,无法进行内容审核")

复制
client = WeChatMiniProgramSecCheck(appid=appid, secret=secret)
openid = get_user_openid(user)

if require_openid and not openid:
    raise ValidationError({"message": "未获取到 openid,无法完成内容审核", "sec_check": {"suggest": "no_openid"}})

parts_reports: List[dict] = []

for part in _split_text(text, max_len=2000):
    res = client.msg_sec_check(content=part, openid=openid, scene=int(scene), version=2)
    report = _normalize_wx_resp(res.raw)
    report.update({"suggest": res.suggest, "label": res.label})
    parts_reports.append(report)

    logger.info("wx sec_check: uid=%s scene=%s suggest=%s label=%s trace=%s",
                getattr(user, "id", None), scene, res.suggest, res.label, report.get("trace_id"))

    if res.ok:
        continue

    # review/risky:明确不通过
    if res.suggest in ("review", "risky"):
        raise ValidationError({"message": "内容可能包含敏感信息,请修改后再提交", "sec_check": report})

    # 其它异常:strict=True 直接拒绝(并把微信返回带出去)
    if strict:
        raise ValidationError({"message": "内容审核服务异常,请稍后重试", "sec_check": report})

    # strict=False:按你原来策略(这里默认还是拒绝更安全)
    raise ValidationError({"message": "内容审核失败,请稍后重试", "sec_check": report})

# 多段合并返回:给前端 parts,必要时也能取最后一次
return {
    "scene": int(scene),
    "openid": (openid or "")[:8] + "..." if openid else "",
    "parts": parts_reports,
    "result": parts_reports[-1].get("result") if parts_reports else {},
    "detail": sum((p.get("detail", []) for p in parts_reports), []),
}

@dataclass
class WxSecCheckResult:
ok: bool
suggest: str = ""
label: Optional[int] = None
raw: Optional[dict] = None

class WeChatMiniProgramSecCheck:
TOKEN_URL = " https://api.weixin.qq.com/cgi-bin/token "
MSG_SEC_CHECK_URL = " https://api.weixin.qq.com/wxa/msg_sec_check "

复制
TOKEN_INVALID_CODES = {40001, 40014, 42001}

def __init__(self, appid: str, secret: str):
    self.appid = appid
    self.secret = secret

def _cache_key(self) -> str:
    return f"wx:mp:access_token:{self.appid}"

def _get_access_token(self) -> str:
    ck = self._cache_key()
    cached = cache.get(ck)
    if cached:
        return cached

    resp = requests.get(
        self.TOKEN_URL,
        params={
            "grant_type": "client_credential",
            "appid": self.appid,
            "secret": self.secret,
        },
        timeout=6,
    )
    data = resp.json()
    if "access_token" not in data:
        raise RuntimeError(f"get_access_token failed: {data}")

    token = data["access_token"]
    expires_in = int(data.get("expires_in", 7200))
    cache.set(ck, token, timeout=max(expires_in - 300, 60))
    return token

def _invalidate_access_token(self) -> None:
    cache.delete(self._cache_key())

def msg_sec_check(self, *, content: str, openid: Optional[str] = None, scene: int = 2,
                  version: int = 2) -> WxSecCheckResult:
    content = (content or "").strip()
    if not content:
        return WxSecCheckResult(ok=False, suggest="empty")

    if not getattr(settings, "WECHAT_SEC_CHECK_ENABLED", True):
        return WxSecCheckResult(ok=True, suggest="pass (disabled)")

    if not openid:
        return WxSecCheckResult(ok=False, suggest="no_openid", raw={"errmsg": "missing openid"})

    def _do_call(token: str) -> dict:
        payload: dict[str, Any] = {
            "content": content,
            "openid": openid,
            "scene": int(scene),
            "version": int(version),
        }
        r = requests.post(self.MSG_SEC_CHECK_URL, params={"access_token": token}, json=payload, timeout=6)
        return r.json()

    token = self._get_access_token()
    data = _do_call(token)

    if int(data.get("errcode") or 0) in self.TOKEN_INVALID_CODES:
        self._invalidate_access_token()
        token = self._get_access_token()
        data = _do_call(token)

    errcode = int(data.get("errcode") or 0)
    if errcode != 0:
        logger.warning("wx msg_sec_check error: %s", data)
        return WxSecCheckResult(ok=False, suggest="api_error", raw=data)

    if "result" not in data:
        logger.warning("wx msg_sec_check bad response (no result): %s", data)
        return WxSecCheckResult(ok=False, suggest="bad_response", raw=data)

    result = data.get("result") or {}
    suggest = result.get("suggest") or "pass"
    label = result.get("label")
    return WxSecCheckResult(ok=(suggest == "pass"), suggest=suggest, label=label, raw=data)

def get_user_openid(user) -> Optional[str]:
for key in ("openid", "wx_openid", "wechat_openid"):
v = getattr(user, key, None)
if v:
return v

复制
info = getattr(user, "info", None)
if info:
    for key in ("openid", "wx_openid", "wechat_openid"):
        v = getattr(info, key, None)
        if v:
            return v
return None

def _split_text(text: str, max_len: int = 2000) -> Iterable[str]:
"""
微信 msg_sec_check 文本长度通常不超过 2000 字符:contentReference[oaicite:2]{index=2}
超长就分段逐段检测。
"""
s = (text or "").strip()
if not s:
return []

复制
buf = []
cur = ""
for ch in s:
    if len(cur) >= max_len:
        buf.append(cur)
        cur = ""
    cur += ch
if cur:
    buf.append(cur)
return buf

def check_text_or_raise(*, user, text: str, scene: int, strict: bool = False, require_openid: bool = False) -> None:
appid = getattr(settings, "WECHAT_APPID", "") or getattr(settings, "WX_MINI_APPID", "")
secret = getattr(settings, "WECHAT_SECRET", "") or getattr(settings, "WX_MINI_SECRET", "")
if not appid or not secret:
raise ValidationError("未配置 WECHAT_APPID/WECHAT_SECRET,无法进行内容审核")

复制
client = WeChatMiniProgramSecCheck(appid=appid, secret=secret)
openid = get_user_openid(user)

if require_openid and not openid:
    raise ValidationError("未获取到 openid,无法完成内容审核,请退出重进小程序或重新登录后再试")

for part in _split_text(text, max_len=2000):
    res = client.msg_sec_check(
        content=part,
        openid=openid,
        scene=int(scene),
        version=2,
    )

    if res.ok:
        continue

    logger.warning("wx sec_check block: suggest=%s label=%s raw=%s", res.suggest, res.label, res.raw)

    if res.suggest in ("review", "risky"):
        raise ValidationError("内容可能包含敏感信息,请修改后再提交")
    if res.suggest == "empty":
        raise ValidationError("内容不能为空")

    # api_error:严格模式一律拒绝;非严格按 fail-open 配置决定
    if res.suggest == "api_error":
        if strict:
            raise ValidationError("内容审核服务异常,请稍后重试")
        if getattr(settings, "WECHAT_SEC_CHECK_FAIL_OPEN", False):
            continue
        raise ValidationError("内容审核失败,请稍后重试")

    raise ValidationError("内容审核失败,请稍后重试")
前排打手

看错了,我第一反应是你要做这么一个接口。但是我怀疑你这个图别有用意,就是想曲线发这么一条评论。doge

OP

那没有,我就是想知道这是什么问题,这个涉黄评论微信小程序审核那卡了我半个月了,恶心死我了

种子用户
Guardian

之前没人回复主要原因不是金币,是因为不知道你的问题是什么,涉及到交互式的社区,审核是基本要求,你后台自己审核或者用微信审核都是拿到对应样本的分数,然后自己决定如何处理,现在看你帖子不知道你的问题是什么。

OP

现在我发布的评论涉黄,但是微信的文本监管接口没有拦截我发布的涉黄评论,直接让通过了

前排打手

他明确说是因为你的 sh 评论能展示,所以不给你过,让你修改还是?
你现在想要调整让微信文本审核接口能发挥功能, 还是想要让你的程序能正常过审上线?
带评论功能的小程序很难做。

OP

@libra2 一直说让我接微信的文本检测接口,我已经接了,而且也同步判断了,我上传图片也是这个写法,上传黄图就会自动拦截,但是文本就不行,说些涉黄的言论还是可以通过

前排打手

Label 值,判定结果,详细含义
100,正常,内容安全,建议直接放行
10001,广告,包含营销推广、骚扰等信息
20001,时政,涉及敏感政治人物、事件或负面时政
20002,色情,包含淫秽、低俗、色情描写
20003,辱骂,包含人身攻击、恶毒咒骂
20006,违法犯罪,涉及违禁品、欺诈、博彩等内容
20008,欺诈,仿冒、兼职诈骗等内容
感觉就是接口本身的问题facepalm 没有语义分析?

种子用户
Guardian

我猜测你的问题是返回的标签 100,建议 pass,实际上应该拦截,检查下发过去的 content 是不是用户的评论,会不会发错了,用个简单的 http 测一下看看,然后再放进业务里面,在前后打印日志,看看被检测的内容是否获取正确,贴这么多代码没多少人看。

种子用户
Guardian

@hhillmanpick 我已经测试了,微信端没问题,应该是你取值的问题,取错了文本,导致发过去检测的是正常的文本。
image

OP

image
我发现是小程序无法拦截,但是调试工具确实是确认违规了,我后端代码那也传的是正确的 content,有点奇怪了

前排打手

这个页面图太多了,现在有毒,看几次拉到,最后的时候整个浏览器就跳,过一会就白屏了facepalm
估计是图和我插件什么的冲突。

种子用户
Guardian

打印传入的内容了吗?我之前经常遇到取值的问题,就是取字符串取成 object、json 对象,实际上不是值,或者是取成键值对的键。

OP

打印了,传的就是这个

文本检测 {'content': '我想草拟', 'openid': 'ozxtT15kwJZrIeer7GsJQ9nMphqA', 'scene': 2, 'version': 2} {'errcode': 0, 'errmsg': 'ok', 'detail': [{'strategy': 'keyword', 'errcode': 0}, {'strategy': 'content_model', 'errcode': 0, 'suggest': 'pass', 'label': 100, 'prob': 90}], 'trace_id': '6968a9a1-40bcc957-15249ba8', 'result': {'suggest': 'pass', 'label': 100}}

种子用户
Guardian

你是自己后端检测还是怎么检测的,你后端检测就是 HTTPS 方式调用,那就是和调试工具一样的,不存在小程序无法拦截这种说法,我看第一张图像是你的后端返回的。

前排打手
Guardian

最近小程序被打回,今天也在弄这东西
图片,文本都需要审核

我是 PHP 后端实现

hdd 试了一遍都正常,后来发现 ldr 名字不能审核通过

发表一个评论

R保持