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); } }