// 豆维 · 内容安全云函数 // type: checkMessage | checkMessages | checkImage // 返回约定:{ risk: boolean, skipped?: boolean };skipped 表示检测未能完成,前端必须拦截 const cloud = require("wx-server-sdk"); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }); const MAX_TEXT = 2500; const MAX_IMAGE_BYTES = 1024 * 1024; const MAX_BATCH = 5; const extractErrCode = (error) => { if (!error) return undefined; if (typeof error.errCode === "number") return error.errCode; if (typeof error.code === "number") return error.code; const match = typeof error.errMsg === "string" ? error.errMsg.match(/-?\d{3,}/) : null; return match ? Number(match[0]) : undefined; }; // msgSecCheck v2:scene 1 资料 / 2 评论 / 3 论坛 / 4 社交日志 async function checkOneMessage(content, scene, openid) { const value = String(content || "").slice(0, MAX_TEXT); if (!value.trim()) return { risk: false }; const safeScene = [1, 2, 3, 4].includes(Number(scene)) ? Number(scene) : 2; try { const response = await cloud.openapi.security.msgSecCheck({ version: 2, scene: safeScene, openid, content: value, }); const suggest = response && response.result && response.result.suggest; if (!suggest) return { risk: false, skipped: true }; // review(疑似)按违规处理,宁严勿松 return { risk: suggest === "risky" || suggest === "review", verdict: suggest }; } catch (e) { if (extractErrCode(e) === 87014) return { risk: true, verdict: "risky" }; return { risk: false, skipped: true, errCode: extractErrCode(e) }; } } // 通过文件头判断真实格式,不信任客户端传入的类型 function sniffType(buffer) { if (buffer.length > 3 && buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) return "image/jpeg"; if (buffer.length > 8 && buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47) return "image/png"; return ""; } async function checkImage(fileID) { // 只允许检测本小程序上传到 sec-check/ 目录的临时文件,防止借接口删除/读取其他文件 if (typeof fileID !== "string" || !/^cloud:\/\/[^/]+\/sec-check\/[\w.-]+\.jpg$/.test(fileID)) { return { risk: false, skipped: true, reason: "INVALID_FILE" }; } try { const download = await cloud.downloadFile({ fileID }); const buffer = download && download.fileContent; const contentType = buffer ? sniffType(buffer) : ""; if (!buffer || !buffer.length || buffer.length > MAX_IMAGE_BYTES || !contentType) { return { risk: false, skipped: true, reason: "BAD_IMAGE" }; } await cloud.openapi.security.imgSecCheck({ media: { contentType, value: buffer }, }); return { risk: false }; } catch (e) { if (extractErrCode(e) === 87014) return { risk: true, verdict: "risky" }; return { risk: false, skipped: true, errCode: extractErrCode(e) }; } finally { try { await cloud.deleteFile({ fileList: [fileID] }); } catch (e) { // 清理失败不影响检测结果 } } } exports.main = async (event) => { const { OPENID } = cloud.getWXContext(); if (!OPENID) return { risk: false, skipped: true, reason: "NO_OPENID" }; const type = event && event.type; if (type === "checkMessage") { return checkOneMessage(event.content, event.scene, OPENID); } if (type === "checkMessages") { const items = Array.isArray(event.items) ? event.items.slice(0, MAX_BATCH) : []; const results = await Promise.all(items.map((it) => checkOneMessage(it && it.value, it && it.scene, OPENID))); return { results }; } if (type === "checkImage") { return checkImage(event.fileID); } return { risk: false, skipped: true, reason: "UNKNOWN_TYPE" }; };