File size: 4,927 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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
// 内容安全链路(微信平台规范)
//  文本:security.msgSecCheck v2(二维码内容 / 扫码导入内容 / 水印)
//  图片:先按官方建议压缩到 750×1334 内、1MB 内的 JPG,再上传云存储由云函数调用 security.imgSecCheck
// 约定返回:{ status: 'pass' | 'risky' | 'error' }
//  - risky:命中违规,必须拦截
//  - error:检测服务不可用,同样拦截(禁止静默放行)
// 通过结果按内容缓存,导出时无需重复请求,显著提升导出速度

const FUNCTION_NAME = 'quickstartFunctions'
const MAX_W = 750
const MAX_H = 1334
const textCache = new Map() // value -> 'pass' | 'risky'
const pending = new Map() // value -> Promise

function callFunction(data) {
  return new Promise((resolve) => {
    if (!wx.cloud) { resolve(null); return }
    wx.cloud.callFunction({
      name: FUNCTION_NAME,
      data,
      success: (res) => resolve((res && res.result) || null),
      fail: () => resolve(null),
    })
  })
}

function normalize(result) {
  if (!result) return 'error'
  if (result.risk) return 'risky'
  if (result.skipped) return 'error'
  return 'pass'
}

function checkText(content, scene = 2) {
  const value = String(content || '').trim().slice(0, 2500)
  if (!value) return Promise.resolve({ status: 'pass' })
  const cached = textCache.get(value)
  if (cached) return Promise.resolve({ status: cached, cached: true })
  if (pending.has(value)) return pending.get(value)
  const task = callFunction({ type: 'checkMessage', content: value, scene }).then((result) => {
    const status = normalize(result)
    if (status !== 'error') textCache.set(value, status)
    pending.delete(value)
    return { status }
  })
  pending.set(value, task)
  return task
}

// 导出前一次性校验多段文本(已缓存的直接复用,未缓存的合并为一次云函数调用)
function checkTexts(items) {
  const list = items.map((it) => ({ value: String(it.value || '').trim().slice(0, 2500), scene: it.scene || 2 })).filter((it) => it.value)
  const results = list.map((it) => textCache.get(it.value))
  if (results.includes('risky')) return Promise.resolve({ status: 'risky' })
  const missing = list.filter((it, i) => !results[i])
  if (!missing.length) return Promise.resolve({ status: 'pass' })
  return callFunction({ type: 'checkMessages', items: missing }).then((res) => {
    if (!res || !Array.isArray(res.results)) return { status: 'error' }
    let status = 'pass'
    res.results.forEach((r, i) => {
      const s = normalize(r)
      if (s !== 'error') textCache.set(missing[i].value, s)
      if (s === 'risky') status = 'risky'
      else if (s === 'error' && status === 'pass') status = 'error'
    })
    return { status }
  })
}

function isTextPassed(value) {
  const v = String(value || '').trim()
  return !v || textCache.get(v) === 'pass'
}

// 按官方尺寸建议压缩(同时加快上传)
function compressForCheck(canvas, image, path) {
  return new Promise((resolve) => {
    if (!canvas || !image) { resolve(path); return }
    try {
      const w0 = image.width || image._w
      const h0 = image.height || image._h
      const scale = Math.min(1, MAX_W / w0, MAX_H / h0)
      canvas.width = Math.max(1, Math.round(w0 * scale))
      canvas.height = Math.max(1, Math.round(h0 * scale))
      const ctx = canvas.getContext('2d')
      ctx.fillStyle = '#ffffff'
      ctx.fillRect(0, 0, canvas.width, canvas.height)
      ctx.drawImage(image, 0, 0, canvas.width, canvas.height)
      wx.canvasToTempFilePath({
        canvas,
        fileType: 'jpg',
        quality: 0.82,
        destWidth: canvas.width,
        destHeight: canvas.height,
        success: (res) => resolve(res.tempFilePath),
        fail: () => resolve(path),
      })
    } catch (e) {
      resolve(path)
    }
  })
}

function upload(filePath) {
  return new Promise((resolve) => {
    if (!wx.cloud) { resolve(''); return }
    wx.cloud.uploadFile({
      cloudPath: `sec-check/${Date.now()}-${Math.random().toString(36).slice(2, 10)}.jpg`,
      filePath,
      success: (res) => resolve((res && res.fileID) || ''),
      fail: () => resolve(''),
    })
  })
}

// image:已加载的 CanvasImage(用于压缩);path:本地临时路径
function checkImage(path, image, canvas) {
  if (!path) return Promise.resolve({ status: 'error' })
  return compressForCheck(canvas, image, path)
    .then(upload)
    .then((fileID) => {
      if (!fileID) return { status: 'error' }
      // 云函数检测完毕后会立即删除该临时文件
      return callFunction({ type: 'checkImage', fileID }).then((result) => ({ status: normalize(result) }))
    })
    .catch(() => ({ status: 'error' }))
}

const MESSAGES = {
  risky: '内容含违规信息,请修改后再试',
  error: '安全检测暂不可用,请检查网络后重试',
}

module.exports = { checkText, checkTexts, isTextPassed, checkImage, MESSAGES }