refactor: 重构抖音内容审核适配器使用SDK v3并集成OAuth认证
主要变更: - 集成抖音OpenAPI SDK v3替代原有的HTTP请求方式 - 实现OAuth客户端令牌自动获取和刷新机制 - 统一环境变量配置,使用BYTEDANCE_APP_ID/APP_SECRET同时支持小程序和开放平台API - 添加智能token缓存和过期检测 - 更新响应解析逻辑适配SDK v3格式 - 添加测试接口验证token获取功能 技术改进: - 自动token管理,无需手动维护访问令牌 - 提前5分钟刷新token避免过期 - 完善的错误处理和类型安全 - 移除重复配置,简化环境变量管理
This commit is contained in:
@@ -21,17 +21,19 @@ JWT_SECRET=a6e4d948295ec0ae53921d67d4ce97cfff0565073e370d6094e43705cd48914b68d38
|
||||
WECHAT_APP_ID=wxb51f0b0c3aad7cdf
|
||||
WECHAT_APP_SECRET=your_wechat_app_secret
|
||||
|
||||
# 抖音小程序配置
|
||||
# 抖音小程序配置(同时用于V3 SDK)
|
||||
BYTEDANCE_APP_ID=ttbfd9c96420ec8f8201
|
||||
BYTEDANCE_APP_SECRET=04a026867fbd1ba52174c7c21d94cbc7361ec378
|
||||
BYTEDANCE_GRANT_TYPE=client_credential
|
||||
|
||||
# 内容审核配置
|
||||
# 图片审核回调地址(重要:抖音审核完成后会调用此接口)
|
||||
AUDIT_CALLBACK_URL=https://sd2s2bl25ni4n75na2bog.apigateway-cn-beijing.volceapi.com/api/v1/content-moderation/bytedance/callback
|
||||
TEST_SERVER_URL=https://sd2s2bl25ni4n75na2bog.apigateway-cn-beijing.volceapi.com
|
||||
PROD_SERVER_URL=https://sd2s2bl25ni4n75na2bog.apigateway-cn-beijing.volceapi.com
|
||||
# 抖音内容审核API地址
|
||||
BYTEDANCE_AUDIT_API_URL=https://developer.toutiao.com/api/apps/v2/content/audit/image
|
||||
# 抖音内容审核API地址(已更新为V3版本)
|
||||
# 旧版本API(已弃用): https://developer.toutiao.com/api/apps/v2/content/audit/image
|
||||
# 新版本V3 API通过SDK内置端点调用
|
||||
|
||||
# 微信内容审核API地址
|
||||
WECHAT_AUDIT_API_URL=https://api.weixin.qq.com/wxa/img_sec_check
|
||||
@@ -38,6 +38,8 @@
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@nestjs/swagger": "^11.2.0",
|
||||
"@nestjs/typeorm": "^11.0.0",
|
||||
"@open-dy/open_api_credential": "^1.0.0",
|
||||
"@open-dy/open_api_sdk": "^1.1.3",
|
||||
"axios": "^1.11.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.2",
|
||||
|
||||
68
pnpm-lock.yaml
generated
68
pnpm-lock.yaml
generated
@@ -32,6 +32,12 @@ importers:
|
||||
'@nestjs/typeorm':
|
||||
specifier: ^11.0.0
|
||||
version: 11.0.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.26(mysql2@3.14.4)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.18.0)(typescript@5.9.2)))
|
||||
'@open-dy/open_api_credential':
|
||||
specifier: ^1.0.0
|
||||
version: 1.0.0
|
||||
'@open-dy/open_api_sdk':
|
||||
specifier: ^1.1.3
|
||||
version: 1.1.3
|
||||
axios:
|
||||
specifier: ^1.11.0
|
||||
version: 1.11.0
|
||||
@@ -126,6 +132,9 @@ importers:
|
||||
|
||||
packages:
|
||||
|
||||
'@alicloud/tea-typescript@1.8.0':
|
||||
resolution: {integrity: sha512-CWXWaquauJf0sW30mgJRVu9aaXyBth5uMBCUc+5vKTK1zlgf3hIqRUjJZbjlwHwQ5y9anwcu18r48nOZb7l2QQ==}
|
||||
|
||||
'@ampproject/remapping@2.3.0':
|
||||
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
@@ -807,6 +816,15 @@ packages:
|
||||
engines: {node: ^14.18.0 || >=16.10.0, npm: '>=5.10.0'}
|
||||
hasBin: true
|
||||
|
||||
'@open-dy/open_api_credential@1.0.0':
|
||||
resolution: {integrity: sha512-MmLrJL2QFYHV3RNJ2/hQEXcjzBCa6mtklW+AJXG+xS/x8cJ7/AgW3wQjvNwJ0oMRyxch1nQ10h9EnOGr1kew0g==}
|
||||
|
||||
'@open-dy/open_api_sdk@1.1.3':
|
||||
resolution: {integrity: sha512-N04K4zkfa5tadbo+iz0ZCdHvNq+9sdzwWZzP4OX0rEmba0VXVlYstzjFpvYsTp+nQcJqab2DvyaXhShJfO1/rg==}
|
||||
|
||||
'@open-dy/open_api_util@1.0.1':
|
||||
resolution: {integrity: sha512-vgUTsyNqB0mtn0LclEr7J2E0eoXTHym1Xh4hO5gXXGYS4MoZGpu9IbYlkR4LMVUSOmJs5ZhRxmNqvEt4D/5fLg==}
|
||||
|
||||
'@paralleldrive/cuid2@2.2.2':
|
||||
resolution: {integrity: sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==}
|
||||
|
||||
@@ -918,6 +936,12 @@ packages:
|
||||
'@types/mime@1.3.5':
|
||||
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
|
||||
|
||||
'@types/node@12.20.55':
|
||||
resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==}
|
||||
|
||||
'@types/node@20.19.13':
|
||||
resolution: {integrity: sha512-yCAeZl7a0DxgNVteXFHt9+uyFbqXGy/ShC4BlcHkoE0AfGXYv/BUiplV72DjMYXHDBXFjhvr6DD1NiRVfB4j8g==}
|
||||
|
||||
'@types/node@22.18.0':
|
||||
resolution: {integrity: sha512-m5ObIqwsUp6BZzyiy4RdZpzWGub9bqLJMvZDD0QMXhxjqMHMENlj+SqF5QxoUwaQNFe+8kz8XM8ZQhqkQPTgMQ==}
|
||||
|
||||
@@ -2013,6 +2037,9 @@ packages:
|
||||
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
httpx@2.3.3:
|
||||
resolution: {integrity: sha512-k1qv94u1b6e+XKCxVbLgYlOypVP9MPGpnN5G/vxFf6tDO4V3xpz3d6FUOY/s8NtPgaq5RBVVgSB+7IHpVxMYzw==}
|
||||
|
||||
human-signals@2.1.0:
|
||||
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
|
||||
engines: {node: '>=10.17.0'}
|
||||
@@ -3370,6 +3397,13 @@ packages:
|
||||
|
||||
snapshots:
|
||||
|
||||
'@alicloud/tea-typescript@1.8.0':
|
||||
dependencies:
|
||||
'@types/node': 12.20.55
|
||||
httpx: 2.3.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ampproject/remapping@2.3.0':
|
||||
dependencies:
|
||||
'@jridgewell/gen-mapping': 0.3.13
|
||||
@@ -4223,6 +4257,27 @@ snapshots:
|
||||
dependencies:
|
||||
consola: 3.4.2
|
||||
|
||||
'@open-dy/open_api_credential@1.0.0':
|
||||
dependencies:
|
||||
axios: 1.11.0
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
'@open-dy/open_api_sdk@1.1.3':
|
||||
dependencies:
|
||||
'@alicloud/tea-typescript': 1.8.0
|
||||
'@open-dy/open_api_credential': 1.0.0
|
||||
'@open-dy/open_api_util': 1.0.1
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
- supports-color
|
||||
|
||||
'@open-dy/open_api_util@1.0.1':
|
||||
dependencies:
|
||||
'@alicloud/tea-typescript': 1.8.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@paralleldrive/cuid2@2.2.2':
|
||||
dependencies:
|
||||
'@noble/hashes': 1.8.0
|
||||
@@ -4353,6 +4408,12 @@ snapshots:
|
||||
|
||||
'@types/mime@1.3.5': {}
|
||||
|
||||
'@types/node@12.20.55': {}
|
||||
|
||||
'@types/node@20.19.13':
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
|
||||
'@types/node@22.18.0':
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
@@ -5516,6 +5577,13 @@ snapshots:
|
||||
statuses: 2.0.1
|
||||
toidentifier: 1.0.1
|
||||
|
||||
httpx@2.3.3:
|
||||
dependencies:
|
||||
'@types/node': 20.19.13
|
||||
debug: 4.4.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
human-signals@2.1.0: {}
|
||||
|
||||
iconv-lite@0.6.3:
|
||||
|
||||
@@ -50,11 +50,13 @@ export function setupSwagger(app: INestApplication): void {
|
||||
.addTag('扩展服务', '预留的扩展功能接口')
|
||||
.addServer(`http://localhost:${process.env.PORT || 3003}`, '开发环境')
|
||||
.addServer(
|
||||
process.env.TEST_SERVER_URL || 'https://sd2s2bl25ni4n75na2bog.apigateway-cn-beijing.volceapi.com',
|
||||
process.env.TEST_SERVER_URL ||
|
||||
'https://sd2s2bl25ni4n75na2bog.apigateway-cn-beijing.volceapi.com',
|
||||
'测试环境',
|
||||
)
|
||||
.addServer(
|
||||
process.env.PROD_SERVER_URL || 'https://sd2s2bl25ni4n75na2bog.apigateway-cn-beijing.volceapi.com',
|
||||
process.env.PROD_SERVER_URL ||
|
||||
'https://sd2s2bl25ni4n75na2bog.apigateway-cn-beijing.volceapi.com',
|
||||
'生产环境',
|
||||
)
|
||||
.build();
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
import { Injectable, BadRequestException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { HttpService } from '@nestjs/axios';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Repository } from 'typeorm';
|
||||
import Client, {
|
||||
CensorImageRequest,
|
||||
CensorImageResponse,
|
||||
OauthClientTokenRequest,
|
||||
} from '@open-dy/open_api_sdk';
|
||||
import CredentialClient from '@open-dy/open_api_credential';
|
||||
import { BaseContentAdapter } from './base-content.adapter';
|
||||
import { ContentAuditLogEntity } from '../entities/content-audit-log.entity';
|
||||
import { PlatformType } from '../../entities/platform-user.entity';
|
||||
import {
|
||||
ImageAuditRequest,
|
||||
@@ -10,37 +21,43 @@ import {
|
||||
AuditSuggestion,
|
||||
} from '../interfaces/content-moderation.interface';
|
||||
|
||||
interface DouyinAuditResponse {
|
||||
err_no: number;
|
||||
err_tips: string;
|
||||
log_id: string;
|
||||
data: {
|
||||
task_id: string;
|
||||
status: number; // 0: 审核中, 1: 审核完成
|
||||
conclusion: number; // 1: 合规, 2: 不合规, 3: 疑似, 4: 审核失败
|
||||
confidence: number; // 置信度 0-100
|
||||
details: Array<{
|
||||
type: string; // 检测类型
|
||||
label: string; // 具体标签
|
||||
confidence: number; // 该项置信度
|
||||
description: string; // 描述
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class DouyinContentAdapter extends BaseContentAdapter {
|
||||
platform = PlatformType.BYTEDANCE;
|
||||
|
||||
private readonly douyinConfig = {
|
||||
appId: this.configService.get('BYTEDANCE_APP_ID'),
|
||||
appSecret: this.configService.get('BYTEDANCE_APP_SECRET'),
|
||||
auditApiUrl: this.configService.get(
|
||||
'BYTEDANCE_AUDIT_API_URL',
|
||||
'https://developer.toutiao.com/api/apps/v2/content/audit/image',
|
||||
),
|
||||
private douyinConfig: {
|
||||
clientKey: string;
|
||||
clientSecret: string;
|
||||
appId: string;
|
||||
grantType: string;
|
||||
};
|
||||
|
||||
private douyinClient: Client;
|
||||
private accessToken: string | null = null;
|
||||
private tokenExpiresAt: Date | null = null;
|
||||
|
||||
constructor(
|
||||
protected readonly httpService: HttpService,
|
||||
protected readonly configService: ConfigService,
|
||||
@InjectRepository(ContentAuditLogEntity)
|
||||
protected readonly auditLogRepository: Repository<ContentAuditLogEntity>,
|
||||
) {
|
||||
super(httpService, configService, auditLogRepository);
|
||||
|
||||
// 抖音开放平台配置:clientKey/clientSecret 实际使用的是 APP_ID/APP_SECRET
|
||||
this.douyinConfig = {
|
||||
clientKey: this.configService.get('BYTEDANCE_APP_ID') || '',
|
||||
clientSecret: this.configService.get('BYTEDANCE_APP_SECRET') || '',
|
||||
appId: this.configService.get('BYTEDANCE_APP_ID') || '',
|
||||
grantType: this.configService.get('BYTEDANCE_GRANT_TYPE') || 'client_credential',
|
||||
};
|
||||
|
||||
this.douyinClient = new Client({
|
||||
clientKey: this.douyinConfig.clientKey,
|
||||
clientSecret: this.douyinConfig.clientSecret,
|
||||
});
|
||||
}
|
||||
|
||||
async auditImage(auditData: ImageAuditRequest): Promise<ContentAuditResult> {
|
||||
try {
|
||||
// 1. 创建审核日志
|
||||
@@ -150,79 +167,141 @@ export class DouyinContentAdapter extends BaseContentAdapter {
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用抖音审核API
|
||||
* 获取访问令牌
|
||||
*/
|
||||
private async getAccessToken(): Promise<string> {
|
||||
// 检查token是否有效
|
||||
if (this.accessToken && this.tokenExpiresAt && new Date() < this.tokenExpiresAt) {
|
||||
return this.accessToken;
|
||||
}
|
||||
|
||||
// 获取新的token
|
||||
const params = new OauthClientTokenRequest({
|
||||
clientKey: this.douyinConfig.clientKey,
|
||||
clientSecret: this.douyinConfig.clientSecret,
|
||||
grantType: this.douyinConfig.grantType,
|
||||
});
|
||||
|
||||
const response = await this.douyinClient.oauthClientToken(params);
|
||||
|
||||
if (response.errNo && response.errNo !== 0) {
|
||||
throw new Error(`获取访问令牌失败: ${response.errMsg || '未知错误'}`);
|
||||
}
|
||||
|
||||
// 存储token和过期时间
|
||||
this.accessToken = response.accessToken || '';
|
||||
if (response.expiresIn) {
|
||||
// 提前5分钟刷新token
|
||||
const expiresIn = parseInt(response.expiresIn.toString()) - 300;
|
||||
this.tokenExpiresAt = new Date(Date.now() + expiresIn * 1000);
|
||||
}
|
||||
|
||||
if (!this.accessToken) {
|
||||
throw new Error('获取访问令牌失败: 返回的accessToken为空');
|
||||
}
|
||||
|
||||
return this.accessToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查并刷新token
|
||||
*/
|
||||
private async ensureValidToken(): Promise<string> {
|
||||
if (!this.accessToken || !this.tokenExpiresAt || new Date() >= this.tokenExpiresAt) {
|
||||
return this.getAccessToken();
|
||||
}
|
||||
return this.accessToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试token获取功能 - 公共方法用于测试
|
||||
*/
|
||||
async testTokenAcquisition(): Promise<{
|
||||
success: boolean;
|
||||
accessToken?: string;
|
||||
expiresAt?: Date;
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const token = await this.getAccessToken();
|
||||
return {
|
||||
success: true,
|
||||
accessToken: token.substring(0, 10) + '...', // 只显示前10位
|
||||
expiresAt: this.tokenExpiresAt || undefined,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '未知错误',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用抖音审核API (使用SDK v3)
|
||||
*/
|
||||
private async callDouyinAuditAPI(
|
||||
auditData: ImageAuditRequest,
|
||||
): Promise<ContentAuditResult> {
|
||||
const requestData = {
|
||||
app_id: this.douyinConfig.appId,
|
||||
image_url: auditData.imageUrl,
|
||||
task_id: auditData.taskId,
|
||||
callback_url: this.configService.get('AUDIT_CALLBACK_URL') || '', // 异步回调URL
|
||||
};
|
||||
// 确保有有效的access token
|
||||
const accessToken = await this.ensureValidToken();
|
||||
|
||||
const params = new CensorImageRequest({
|
||||
accessToken: accessToken,
|
||||
appId: this.douyinConfig.appId,
|
||||
image: auditData.imageUrl,
|
||||
imageData: auditData.imageBase64, // base64编码的图片数据(可选)
|
||||
});
|
||||
|
||||
const response = await this.httpService.axiosRef.post(
|
||||
this.douyinConfig.auditApiUrl,
|
||||
requestData,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
// 添加认证头部
|
||||
},
|
||||
},
|
||||
);
|
||||
const response = await this.douyinClient.censorImage(params);
|
||||
|
||||
if (response.data.err_no !== 0) {
|
||||
throw new Error(`抖音审核API错误: ${response.data.err_tips}`);
|
||||
if (response.errNo && response.errNo !== 0) {
|
||||
throw new Error(`抖音审核API错误: ${response.errMsg || '未知错误'}`);
|
||||
}
|
||||
|
||||
return this.parseDouyinResponse(response.data, auditData.taskId || '');
|
||||
return this.parseDouyinSDKResponse(response, auditData.taskId || '');
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询抖音审核状态
|
||||
* 查询抖音审核状态 (使用SDK v3)
|
||||
*/
|
||||
private async queryDouyinAuditStatus(
|
||||
taskId: string,
|
||||
): Promise<ContentAuditResult> {
|
||||
const queryData = {
|
||||
app_id: this.douyinConfig.appId,
|
||||
task_id: taskId,
|
||||
};
|
||||
// SDK v3暂不提供查询接口,直接返回数据库中的结果
|
||||
// 实际项目中可以使用SDK的其他方法或直接调用API
|
||||
const auditLog = await this.auditLogRepository.findOne({
|
||||
where: { taskId, platform: this.platform },
|
||||
});
|
||||
|
||||
const response = await this.httpService.axiosRef.post(
|
||||
`${this.douyinConfig.auditApiUrl}/query`,
|
||||
queryData,
|
||||
{
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
},
|
||||
);
|
||||
|
||||
if (response.data.err_no !== 0) {
|
||||
throw new Error(`查询抖音审核状态失败: ${response.data.err_tips}`);
|
||||
if (!auditLog) {
|
||||
throw new Error('审核任务不存在');
|
||||
}
|
||||
|
||||
return this.parseDouyinResponse(response.data, taskId);
|
||||
return auditLog.auditResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析抖音响应
|
||||
* 解析抖音SDK响应
|
||||
*/
|
||||
private parseDouyinResponse(
|
||||
responseData: DouyinAuditResponse,
|
||||
private parseDouyinSDKResponse(
|
||||
responseData: CensorImageResponse,
|
||||
taskId: string,
|
||||
): ContentAuditResult {
|
||||
const data = responseData.data;
|
||||
// SDK v3的响应格式不同,需要根据实际返回结果进行适配
|
||||
const isHit =
|
||||
responseData.predicts?.some((predict) => predict.hit) || false;
|
||||
const conclusion = isHit ? 2 : 1; // 2: 不合规, 1: 合规
|
||||
const confidence = 80; // SDK v3暂不提供具体置信度,使用默认值
|
||||
|
||||
return {
|
||||
taskId: taskId,
|
||||
status: this.mapDouyinStatus(data.status),
|
||||
conclusion: this.mapDouyinConclusion(data.conclusion),
|
||||
confidence: data.confidence,
|
||||
details: this.mapDouyinDetails(data.details),
|
||||
riskLevel: this.calculateRiskLevel(data.conclusion, data.confidence),
|
||||
suggestion: this.getSuggestion(data.conclusion),
|
||||
status: AuditStatus.COMPLETED, // SDK v3直接返回结果
|
||||
conclusion: this.mapDouyinConclusion(conclusion),
|
||||
confidence: confidence,
|
||||
details: this.mapDouyinSDKDetails(responseData.predicts || []),
|
||||
riskLevel: this.calculateRiskLevel(conclusion, confidence),
|
||||
suggestion: this.getSuggestion(conclusion),
|
||||
platformData: responseData,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
@@ -273,6 +352,20 @@ export class DouyinContentAdapter extends BaseContentAdapter {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 映射抖音SDK v3详细信息
|
||||
*/
|
||||
private mapDouyinSDKDetails(predicts: any[]): any[] {
|
||||
return (
|
||||
predicts?.map((predict) => ({
|
||||
type: predict.modelName || 'unknown',
|
||||
label: predict.hit ? 'violation' : 'safe',
|
||||
confidence: predict.hit ? 80 : 20, // SDK v3暂不提供具体置信度
|
||||
description: predict.hit ? '检测到违规内容' : '内容安全',
|
||||
})) || []
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算风险等级
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user