refactor: flatten src/AKSRT → src (drop theme-identifier dir)
Move all domain folders from src/AKSRT/{Domain} to src/{Domain}; remap
Composer PSR-4 "AKSRT\\": "src/"; update audit script + README + ADR
paths; remove dead tools/build-psr4-map.php (scanned deleted inc/classes).
This commit is contained in:
@@ -0,0 +1,365 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Extensions;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class ContentAudit {
|
||||
|
||||
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\Settings')) {
|
||||
return \AKSRT\Settings\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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user