File size: 3,771 Bytes
9de7b70
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
// 豆维 · 内容安全云函数
// 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" };
};