admin = $admin; } public function register_routes(): void { // Extensions overview register_rest_route(self::API_NAMESPACE, '/extensions/overview', [ 'methods' => 'GET', 'callback' => [$this, 'get_overview'], 'permission_callback' => [$this, 'check_permission'], ]); // Extensions service status summary (aggregated) register_rest_route(self::API_NAMESPACE, '/settings/extensions', [ 'methods' => 'GET', 'callback' => [$this, 'get_extensions_status'], 'permission_callback' => [$this, 'check_permission'], ]); // IP geo test tool register_rest_route(self::API_NAMESPACE, '/extensions/ip-test', [ 'methods' => 'POST', 'callback' => [$this, 'test_ip_geo'], 'permission_callback' => [$this, 'check_permission'], ]); // Content audit test tool register_rest_route(self::API_NAMESPACE, '/extensions/audit-test', [ 'methods' => 'POST', 'callback' => [$this, 'test_content_audit'], 'permission_callback' => [$this, 'check_permission'], ]); } /** * GET /extensions/overview — real extension status aggregation. */ public function get_overview(): \WP_REST_Response { $services = $this->collect_service_statuses(); $installed = 0; $available = count($services); $unconfigured = 0; foreach ($services as $svc) { if (!empty($svc['enabled'])) $installed++; if (!empty($svc['enabled']) && !empty($svc['configured']) && empty($svc['verified'])) { $unconfigured++; } } // Build message if ($installed === 0) { $message = sprintf(__('已注册 %d 项扩展,均未启用。', 'aksrt'), $available); } elseif ($unconfigured > 0) { $message = sprintf(__('已启用 %d/%d 项扩展,%d 项待补全配置。', 'aksrt'), $installed, $available, $unconfigured); } else { $message = sprintf(__('已启用 %d/%d 项扩展,全部就绪。', 'aksrt'), $installed, $available); } return $this->success([ 'data' => [ 'installed' => $installed, 'available' => $available, 'message' => $message, 'services' => $services, ], ]); } public function get_docs_stats(): \WP_REST_Response { return $this->success([ 'data' => [ 'total' => 0, 'nav_count' => 0, ], ]); } /** * GET /docs/search — search document pages by keyword. */ public function search_docs(\WP_REST_Request $request): \WP_REST_Response { return $this->success([ 'data' => [ 'results' => [], 'total' => 0, ], ]); } /** * GET /settings/extensions — aggregated extension service status. */ public function get_extensions_status(): \WP_REST_Response { $services = $this->collect_service_statuses(); $flat = []; foreach ($services as $key => $svc) { $flat["{$key}_enabled"] = !empty($svc['enabled']); $flat["{$key}_configured"] = !empty($svc['configured']); $flat["{$key}_available"] = !empty($svc['available']); $flat["{$key}_verified"] = !empty($svc['verified']); $flat["{$key}_provider"] = (string) ($svc['provider'] ?? ''); } return $this->success([ 'data' => $flat, 'services' => $services, ]); } /** * POST /extensions/ip-test — test current IP provider with a given IP. * * Admin tests force a live lookup (skip enable-gate + cache) so operators can * verify credentials before flipping the runtime switch. */ public function test_ip_geo(\WP_REST_Request $request): \WP_REST_Response { $params = $this->get_json_params($request); $ip = trim((string) ($params['ip'] ?? '')); if ($ip === '' || !filter_var($ip, FILTER_VALIDATE_IP)) { return $this->error(__('请输入有效的 IP 地址', 'aksrt'), 422); } if (!class_exists('\AKSRT\Extensions\IPGeo')) { return $this->error(__('IP 归属地服务不可用', 'aksrt'), 500); } $geo = \AKSRT\Extensions\IPGeo::get_instance(); $status = $geo->get_status(); $result = $geo->lookup($ip, true); $reason = (string) ($result['reason'] ?? ''); $message = ''; $ok = !empty($result['country']) || !empty($result['province']) || !empty($result['city']); // Persist "verified" once a live lookup succeeds so overview cards leave "待补全". if ($ok) { $all = \AKSRT\Settings\Settings::get_all(); if (empty($all['ip_geo_verified'])) { $all['ip_geo_verified'] = true; update_option('aksrt_settings', $all); if (method_exists('\AKSRT\Settings\Settings', 'flush_settings_cache')) { \AKSRT\Settings\Settings::flush_settings_cache(); } $status = $geo->get_status(); } } if (!$ok) { $detail = trim((string) ($result['detail'] ?? '')); switch ($reason) { case 'private_ip': $message = __('该地址为私有/保留 IP(如 127.0.0.1 / 内网),无法查询。请用公网 IP 测试,例如 114.114.114.114。', 'aksrt'); break; case 'invalid_ip': $message = __('IP 格式无效。', 'aksrt'); break; case 'missing_key': $message = __('当前服务商 AppKey 未填写,请在「扩展 → IP归属地」补全凭据。', 'aksrt'); break; case 'provider_error': // e.g. USERKEY_PLAT_NOMATCH / INVALID_USER_KEY from Amap if (stripos($detail, 'USERKEY_PLAT_NOMATCH') !== false) { $message = __('高德返回 USERKEY_PLAT_NOMATCH:当前 Key 平台类型不匹配。请在高德开放平台新建「Web服务」类型 Key,并开通 IP 定位,不要用 Web端(JS)/Android/iOS Key。', 'aksrt'); } elseif (stripos($detail, 'INVALID_USER_KEY') !== false) { $message = __('高德返回 INVALID_USER_KEY:AppKey 无效或未启用,请核对 Key 是否复制完整、是否已启用。', 'aksrt'); } else { $message = $detail !== '' ? sprintf( /* translators: %s: provider error code/message */ __('服务商拒绝请求:%s。请检查 Key 权限(IP定位)与配额。', 'aksrt'), $detail ) : __('服务商返回错误,请检查 Key 权限与配额。', 'aksrt'); } break; case 'http_error': $message = $detail !== '' ? sprintf( /* translators: %s: transport error */ __('无法访问归属地接口:%s。请检查服务器出站网络/防火墙。', 'aksrt'), $detail ) : __('无法访问归属地接口,请检查服务器出站网络。', 'aksrt'); break; case 'provider_empty': if (!empty($status['missing_fields'])) { $message = sprintf( /* translators: %s: missing config field keys */ __('当前服务商凭据不完整(缺少:%s)。', 'aksrt'), implode(', ', (array) $status['missing_fields']) ); } else { $message = __('服务商未返回有效归属地,请换公网 IP 或检查 Key 权限。', 'aksrt'); } break; case 'disabled': $message = __('IP 归属地功能未启用。', 'aksrt'); break; default: $message = $detail !== '' ? $detail : __('未查询到归属地信息。', 'aksrt'); } } return $this->success([ 'data' => [ 'ip' => $ip, 'ok' => $ok, 'message' => $message, 'status' => $status, 'result' => $result, 'display' => $ok ? $geo->format($result) : '', ], ]); } /** * POST /extensions/audit-test — test text content against current audit provider. * * Admin tests force a live provider call (skip enable gates + failure allow policy) * so operators can verify credentials before flipping runtime switches. */ public function test_content_audit(\WP_REST_Request $request): \WP_REST_Response { $params = $this->get_json_params($request); $text = trim((string) ($params['text'] ?? '')); if ($text === '') { return $this->error(__('请输入要测试的文本内容', 'aksrt'), 422); } if (!class_exists('\AKSRT\Extensions\ContentAudit')) { return $this->error(__('内容审核服务不可用', 'aksrt'), 500); } $audit = \AKSRT\Extensions\ContentAudit::get_instance(); $status = $audit->get_status(); $result = $audit->audit_text($text, true); $reason = (string) ($result['reason'] ?? ''); $message = ''; $ok = !empty($result['passed']); // config/api 错误时 passed=false 且 reason 是机器码,需要可读提示 $error_reasons = ['config_missing', 'api_error', 'file_error', 'disabled']; $is_error = in_array($reason, $error_reasons, true) || (str_starts_with($reason, 'config_') && $reason !== ''); if ($is_error || (!$ok && $reason !== '')) { switch ($reason) { case 'config_missing': if (!empty($status['missing_fields'])) { $message = sprintf( /* translators: %s: missing config field keys */ __('当前审核服务商凭据不完整(缺少:%s),请先在「扩展 → 内容审核」补全配置。', 'aksrt'), implode(', ', (array) $status['missing_fields']) ); } else { $message = __('审核服务商凭据缺失,请先补全 API Key / Secret。', 'aksrt'); } $ok = false; break; case 'api_error': $message = __('调用审核接口失败,请检查凭据、网络出口或服务商状态。', 'aksrt'); $ok = false; break; case 'disabled': $message = __('内容审核功能未启用。', 'aksrt'); $ok = false; break; case 'non_compliant': $message = __('审核结论:不合规(已拦截)。', 'aksrt'); break; case 'suspected': $message = __('审核结论:疑似违规。', 'aksrt'); break; case 'review_failed': $message = __('审核结论:服务商审核失败。', 'aksrt'); break; default: if (!$ok) { $message = $reason !== '' ? sprintf(/* translators: %s: provider reason */ __('审核未通过:%s', 'aksrt'), $reason) : __('审核未通过。', 'aksrt'); } } } elseif ($ok) { if ($reason === 'passthrough' || $reason === '') { $message = __('审核通过。', 'aksrt'); } else { $message = sprintf(/* translators: %s: provider reason */ __('审核通过(%s)。', 'aksrt'), $reason); } } return $this->success([ 'data' => [ 'text' => $text, 'ok' => $ok, 'message' => $message, 'status' => $status, 'result' => $result, ], ]); } /** * 收集所有扩展服务的运行时状态 */ private function collect_service_statuses(): array { $services = []; // SMS if (class_exists('\AKSRT\Sms\SMS')) { $services['sms'] = \AKSRT\Sms\SMS::get_instance()->get_provider_status(); } else { $services['sms'] = ['enabled' => false, 'provider' => 'none', 'configured' => false, 'available' => false, 'verified' => false, 'transport' => '', 'missing_fields' => []]; } // WeChat if (class_exists('\AKSRT\Payment\WeChatNotify')) { $services['wechat'] = \AKSRT\Payment\WeChatNotify::get_instance()->get_status(); } else { $services['wechat'] = ['enabled' => false, 'provider' => '', 'configured' => false, 'available' => false, 'verified' => false, 'transport' => '', 'missing_fields' => []]; } // Captcha — 唯一真相源 aksrt_captcha_config $captcha = function_exists('aksrt_get_captcha_status_summary') ? aksrt_get_captcha_status_summary() : ['enabled' => false, 'provider' => '', 'configured' => false, 'scenes' => []]; $services['captcha'] = [ 'enabled' => !empty($captcha['enabled']) && !empty($captcha['configured']), 'provider' => (string) ($captcha['provider'] ?? ''), 'configured' => !empty($captcha['configured']), 'available' => !empty($captcha['configured']), 'verified' => false, 'transport' => 'http_api', 'missing_fields' => [], 'scenes' => $captcha['scenes'] ?? [], ]; // IP Geo if (class_exists('\AKSRT\Extensions\IPGeo')) { $services['ip_geo'] = \AKSRT\Extensions\IPGeo::get_instance()->get_status(); } else { $services['ip_geo'] = ['enabled' => false, 'provider' => '', 'configured' => false, 'available' => false, 'verified' => false, 'transport' => '', 'missing_fields' => []]; } // Content Audit if (class_exists('\AKSRT\Extensions\ContentAudit')) { $services['audit'] = \AKSRT\Extensions\ContentAudit::get_instance()->get_status(); } else { $services['audit'] = ['enabled' => false, 'provider' => '', 'configured' => false, 'available' => false, 'verified' => false, 'transport' => '', 'missing_fields' => []]; } // Search Push if (class_exists('\AKSRT\Extensions\SearchPush')) { $services['push'] = \AKSRT\Extensions\SearchPush::get_instance()->get_status(); } else { $services['push'] = ['enabled' => false, 'provider' => '', 'configured' => false, 'available' => false, 'verified' => false, 'transport' => '', 'missing_fields' => []]; } // Docs Mode (compat path is retired from rebuilt mainline) $services['docs_mode'] = [ 'enabled' => false, 'provider' => 'builtin', 'configured' => false, 'available' => false, 'verified' => true, 'transport' => 'builtin', 'missing_fields' => [], ]; return $services; } }