Files
wordpress-theme-aksrt/inc/classes/extensions/class-aksrt-content-audit.php
T
kate522 ce1c13b947 refactor(classes): organize inc/classes into domain folders
Move ~94 root class shells and root *-traits into domain directories
(core, auth, user, credits, content, payment/gateways, forum, shop,
sms, ai, messages, extensions, widgets, settings, admin). Update
bootstrap require paths, co-locate traits with owners, fix forum/user
relative requires, drop duplicate shop controller requires, and wire
home-modules load. Path-only; class names and init order unchanged.
2026-07-17 11:11:57 +08:00

369 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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;
}
/**
* 审核文本内容
*
* @param string $text 待审文本
* @param bool $force 管理端测试用:跳过启用开关与失败放行策略,直接打服务商
* @return array{passed: bool, reason: string, labels: array, raw: array}
*/
public function audit_text(string $text, bool $force = false): array {
$config = $this->get_config();
if (
!$force
&& (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 (!$force && !empty($config['content_audit_log_enabled'])) {
$this->log_audit('text', $text, $result);
}
// 管理端强制测试保留服务商原始结论,不套用 on_failure=allow
if (
!$force
&& !$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);
}
}