358 lines
14 KiB
PHP
358 lines
14 KiB
PHP
<?php
|
||
/**
|
||
* AKSRT Content Audit Service
|
||
*
|
||
* 第三方内容审核统一入口。支持多 provider(百度内容审核、阿里云等)。
|
||
* 文本审核 + 图片审核 + 日志 + 失败策略。
|
||
*
|
||
* @package AKSRT
|
||
*/
|
||
|
||
defined('ABSPATH') || exit;
|
||
|
||
class AKSRT_Content_Audit {
|
||
|
||
private static $instance = null;
|
||
|
||
public static function get_instance(): self {
|
||
if (null === self::$instance) {
|
||
self::$instance = new self();
|
||
}
|
||
return self::$instance;
|
||
}
|
||
|
||
/**
|
||
* 审核文本内容
|
||
*
|
||
* @return array{passed: bool, reason: string, labels: array, raw: array}
|
||
*/
|
||
public function audit_text(string $text): array {
|
||
$config = $this->get_config();
|
||
|
||
if (empty($config['content_audit_enabled']) || empty($config['content_audit_text_enabled'])) {
|
||
return $this->ok('disabled');
|
||
}
|
||
|
||
$provider = sanitize_key((string) ($config['content_audit_provider'] ?? 'baidu'));
|
||
$result = $this->call_text_api($provider, $text, $config);
|
||
|
||
if (!empty($config['content_audit_log_enabled'])) {
|
||
$this->log_audit('text', $text, $result);
|
||
}
|
||
|
||
if (!$result['passed'] && ($config['content_audit_on_failure'] ?? 'allow') === 'allow') {
|
||
$result['passed'] = true;
|
||
$result['reason'] .= ' (failure_policy: allow)';
|
||
}
|
||
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* 审核图片
|
||
*
|
||
* @param string $image_path 本地文件路径或 URL
|
||
* @return array{passed: bool, reason: string, labels: array, raw: array}
|
||
*/
|
||
public function audit_image(string $image_path): array {
|
||
$config = $this->get_config();
|
||
|
||
if (empty($config['content_audit_enabled']) || empty($config['content_audit_image_enabled'])) {
|
||
return $this->ok('disabled');
|
||
}
|
||
|
||
$provider = sanitize_key((string) ($config['content_audit_provider'] ?? 'baidu'));
|
||
$result = $this->call_image_api($provider, $image_path, $config);
|
||
|
||
if (!empty($config['content_audit_log_enabled'])) {
|
||
$this->log_audit('image', $image_path, $result);
|
||
}
|
||
|
||
if (!$result['passed'] && ($config['content_audit_on_failure'] ?? 'allow') === 'allow') {
|
||
$result['passed'] = true;
|
||
$result['reason'] .= ' (failure_policy: allow)';
|
||
}
|
||
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* 获取服务运行时状态
|
||
*/
|
||
public function get_status(): array {
|
||
$config = $this->get_config();
|
||
$enabled = !empty($config['content_audit_enabled']);
|
||
$provider = sanitize_key((string) ($config['content_audit_provider'] ?? 'baidu'));
|
||
|
||
$missing = [];
|
||
if ($enabled && $provider === 'baidu') {
|
||
if (empty($config['content_audit_api_key'])) $missing[] = 'content_audit_api_key';
|
||
if (empty($config['content_audit_api_secret'])) $missing[] = 'content_audit_api_secret';
|
||
}
|
||
|
||
return [
|
||
'enabled' => $enabled,
|
||
'provider' => $provider,
|
||
'configured' => $enabled && ($provider === 'null' || empty($missing)),
|
||
'available' => $enabled,
|
||
'verified' => !empty($config['content_audit_verified']),
|
||
'transport' => 'http_api',
|
||
'missing_fields' => $missing,
|
||
'text_enabled' => !empty($config['content_audit_text_enabled']),
|
||
'image_enabled' => !empty($config['content_audit_image_enabled']),
|
||
'on_failure' => (string) ($config['content_audit_on_failure'] ?? 'allow'),
|
||
'log_enabled' => !empty($config['content_audit_log_enabled']),
|
||
];
|
||
}
|
||
|
||
/* ============================================================
|
||
* Provider dispatch
|
||
* ============================================================ */
|
||
|
||
private function call_text_api(string $provider, string $text, array $config): array {
|
||
switch ($provider) {
|
||
case 'baidu':
|
||
return $this->baidu_text_censor($text, $config);
|
||
default:
|
||
return $this->local_pattern_check($text, $provider);
|
||
}
|
||
}
|
||
|
||
private function call_image_api(string $provider, string $image_path, array $config): array {
|
||
switch ($provider) {
|
||
case 'baidu':
|
||
return $this->baidu_image_censor($image_path, $config);
|
||
default:
|
||
return $this->ok('no_image_provider');
|
||
}
|
||
}
|
||
|
||
/* ============================================================
|
||
* 百度内容审核 Provider
|
||
* ============================================================ */
|
||
|
||
/**
|
||
* 百度文本审核
|
||
*/
|
||
private function baidu_text_censor(string $text, array $config): array {
|
||
$api_key = trim((string) ($config['content_audit_api_key'] ?? ''));
|
||
$api_secret = trim((string) ($config['content_audit_api_secret'] ?? ''));
|
||
if ($api_key === '' || $api_secret === '') {
|
||
return ['passed' => false, 'reason' => 'config_missing', 'labels' => [], 'raw' => []];
|
||
}
|
||
|
||
$token = $this->baidu_get_access_token($api_key, $api_secret, $config);
|
||
if ($token === '') {
|
||
return ['passed' => false, 'reason' => 'api_error', 'labels' => [], 'raw' => []];
|
||
}
|
||
|
||
$censor_url = trim((string) ($config['content_audit_baidu_text_url'] ?? ''));
|
||
if ($censor_url === '') {
|
||
$censor_url = 'https://aip.baidubce.com/rest/2.0/solution/v1/text_censor/v2/user_defined';
|
||
}
|
||
|
||
$request_url = add_query_arg(['access_token' => $token], $censor_url);
|
||
$response = wp_remote_post($request_url, [
|
||
'timeout' => 15,
|
||
'headers' => ['Content-Type' => 'application/x-www-form-urlencoded'],
|
||
'body' => http_build_query(['text' => $text]),
|
||
]);
|
||
|
||
if (is_wp_error($response)) {
|
||
return ['passed' => false, 'reason' => 'api_error', 'labels' => [], 'raw' => ['error' => $response->get_error_message()]];
|
||
}
|
||
|
||
$status_code = (int) wp_remote_retrieve_response_code($response);
|
||
if ($status_code !== 200) {
|
||
return ['passed' => false, 'reason' => 'api_error', 'labels' => [], 'raw' => ['status' => $status_code]];
|
||
}
|
||
|
||
$payload = json_decode((string) wp_remote_retrieve_body($response), true);
|
||
if (!is_array($payload) || isset($payload['error_code'])) {
|
||
return ['passed' => false, 'reason' => 'api_error', 'labels' => [], 'raw' => $payload ?: []];
|
||
}
|
||
|
||
return $this->parse_baidu_result($payload);
|
||
}
|
||
|
||
/**
|
||
* 百度图片审核
|
||
*/
|
||
private function baidu_image_censor(string $image_path, array $config): array {
|
||
$api_key = trim((string) ($config['content_audit_api_key'] ?? ''));
|
||
$api_secret = trim((string) ($config['content_audit_api_secret'] ?? ''));
|
||
if ($api_key === '' || $api_secret === '') {
|
||
return ['passed' => false, 'reason' => 'config_missing', 'labels' => [], 'raw' => []];
|
||
}
|
||
|
||
$token = $this->baidu_get_access_token($api_key, $api_secret, $config);
|
||
if ($token === '') {
|
||
return ['passed' => false, 'reason' => 'api_error', 'labels' => [], 'raw' => []];
|
||
}
|
||
|
||
// 支持本地路径和 URL
|
||
if (filter_var($image_path, FILTER_VALIDATE_URL)) {
|
||
$image_data = @file_get_contents($image_path);
|
||
} else {
|
||
$image_data = @file_get_contents($image_path);
|
||
}
|
||
if ($image_data === false) {
|
||
return ['passed' => false, 'reason' => 'file_error', 'labels' => [], 'raw' => []];
|
||
}
|
||
|
||
$censor_url = trim((string) ($config['content_audit_baidu_img_url'] ?? ''));
|
||
if ($censor_url === '') {
|
||
$censor_url = 'https://aip.baidubce.com/rest/2.0/solution/v1/img_censor/v2/user_defined';
|
||
}
|
||
|
||
$request_url = add_query_arg(['access_token' => $token], $censor_url);
|
||
$response = wp_remote_post($request_url, [
|
||
'timeout' => 30,
|
||
'headers' => ['Content-Type' => 'application/x-www-form-urlencoded'],
|
||
'body' => http_build_query(['image' => base64_encode($image_data)]),
|
||
]);
|
||
|
||
if (is_wp_error($response)) {
|
||
return ['passed' => false, 'reason' => 'api_error', 'labels' => [], 'raw' => ['error' => $response->get_error_message()]];
|
||
}
|
||
|
||
$status_code = (int) wp_remote_retrieve_response_code($response);
|
||
if ($status_code !== 200) {
|
||
return ['passed' => false, 'reason' => 'api_error', 'labels' => [], 'raw' => ['status' => $status_code]];
|
||
}
|
||
|
||
$payload = json_decode((string) wp_remote_retrieve_body($response), true);
|
||
if (!is_array($payload) || isset($payload['error_code'])) {
|
||
return ['passed' => false, 'reason' => 'api_error', 'labels' => [], 'raw' => $payload ?: []];
|
||
}
|
||
|
||
return $this->parse_baidu_result($payload);
|
||
}
|
||
|
||
/**
|
||
* 百度 access_token 获取(带缓存)
|
||
*/
|
||
private function baidu_get_access_token(string $api_key, string $api_secret, array $config): string {
|
||
$cache_key = 'aksrt_baidu_token_' . md5($api_key);
|
||
$cached = get_transient($cache_key);
|
||
if ($cached) return (string) $cached;
|
||
|
||
$token_url = trim((string) ($config['content_audit_baidu_token_url'] ?? ''));
|
||
if ($token_url === '') {
|
||
$token_url = 'https://aip.baidubce.com/oauth/2.0/token';
|
||
}
|
||
|
||
$request_url = add_query_arg([
|
||
'grant_type' => 'client_credentials',
|
||
'client_id' => $api_key,
|
||
'client_secret' => $api_secret,
|
||
], $token_url);
|
||
|
||
$response = wp_remote_get($request_url, ['timeout' => 10]);
|
||
if (is_wp_error($response)) return '';
|
||
|
||
$status_code = (int) wp_remote_retrieve_response_code($response);
|
||
if ($status_code !== 200) return '';
|
||
|
||
$payload = json_decode((string) wp_remote_retrieve_body($response), true);
|
||
if (!is_array($payload) || isset($payload['error'])) return '';
|
||
|
||
$token = (string) ($payload['access_token'] ?? '');
|
||
$expires = max(3600, (int) ($payload['expires_in'] ?? 3600) - 300);
|
||
if ($token !== '') {
|
||
set_transient($cache_key, $token, $expires);
|
||
}
|
||
return $token;
|
||
}
|
||
|
||
/**
|
||
* 解析百度审核结果 → 统一格式
|
||
*/
|
||
private function parse_baidu_result(array $payload): array {
|
||
$conclusion_type = isset($payload['conclusionType']) ? (int) $payload['conclusionType'] : 1;
|
||
$conclusion = (string) ($payload['conclusion'] ?? '');
|
||
$top_category = $this->baidu_extract_top_category($payload);
|
||
|
||
// conclusionType: 1=合规, 2=不合规, 3=疑似, 4=审核失败
|
||
if ($conclusion_type === 1) {
|
||
return [
|
||
'passed' => true,
|
||
'reason' => '',
|
||
'labels' => ['compliant'],
|
||
'raw' => $payload,
|
||
];
|
||
}
|
||
|
||
$score_map = [2 => 95, 3 => 70, 4 => 60];
|
||
$reason_map = [2 => 'non_compliant', 3 => 'suspected', 4 => 'review_failed'];
|
||
|
||
return [
|
||
'passed' => false,
|
||
'reason' => $reason_map[$conclusion_type] ?? 'unknown',
|
||
'labels' => [$top_category ?: ('baidu_type_' . $conclusion_type)],
|
||
'raw' => $payload,
|
||
];
|
||
}
|
||
|
||
private function baidu_extract_top_category(array $payload): string {
|
||
if (!isset($payload['data']) || !is_array($payload['data']) || empty($payload['data'][0])) {
|
||
return '';
|
||
}
|
||
$first = $payload['data'][0];
|
||
if (!is_array($first)) return '';
|
||
if (!empty($first['msg'])) return (string) $first['msg'];
|
||
if (!empty($first['type'])) return 'type_' . (string) $first['type'];
|
||
if (!empty($first['subType'])) return 'sub_' . (string) $first['subType'];
|
||
return '';
|
||
}
|
||
|
||
/* ============================================================
|
||
* 本地基础过滤(无 API 时的 fallback)
|
||
* ============================================================ */
|
||
|
||
private function local_pattern_check(string $text, string $provider): array {
|
||
$forbidden_patterns = ['/赌博/i', '/色情/i', '/枪支/i', '/毒品/i'];
|
||
foreach ($forbidden_patterns as $pattern) {
|
||
if (preg_match($pattern, $text, $m)) {
|
||
return [
|
||
'passed' => false,
|
||
'reason' => sprintf('本地敏感词匹配: %s', $m[0]),
|
||
'labels' => ['forbidden'],
|
||
'raw' => ['provider' => $provider, 'method' => 'local_pattern'],
|
||
];
|
||
}
|
||
}
|
||
return $this->ok('passthrough');
|
||
}
|
||
|
||
/* ============================================================
|
||
* Helpers
|
||
* ============================================================ */
|
||
|
||
private function ok(string $reason = ''): array {
|
||
return ['passed' => true, 'reason' => $reason, 'labels' => [], 'raw' => []];
|
||
}
|
||
|
||
private function get_config(): array {
|
||
if (class_exists('AKSRT_Settings')) {
|
||
return AKSRT_Settings::get_content_audit_settings();
|
||
}
|
||
return ['content_audit_enabled' => false];
|
||
}
|
||
|
||
private function log_audit(string $type, string $content, array $result): void {
|
||
$log = get_option('aksrt_content_audit_log', []);
|
||
$log[] = [
|
||
'type' => $type,
|
||
'content' => mb_substr($content, 0, 100),
|
||
'passed' => $result['passed'],
|
||
'reason' => $result['reason'] ?? '',
|
||
'time' => current_time('mysql'),
|
||
];
|
||
if (count($log) > 500) {
|
||
$log = array_slice($log, -500);
|
||
}
|
||
update_option('aksrt_content_audit_log', $log);
|
||
}
|
||
}
|