*/ public static function run(): array { $out = []; $connected = get_account_id() !== '' && get_api_key() !== ''; $gate = class_exists('\\WordPress\\AI\\Connector_Approval\\Approvals_Store'); $connect = [__('Settings > Connection', 'aipcf-ai-provider-for-cloudflare'), SettingsPage::settingsUrl('connection')]; $out[] = self::result('credentials', __('Account ID and API token are saved', 'aipcf-ai-provider-for-cloudflare'), $connected ? 'ok' : 'bad', $connected ? '' : __('Enter both to connect.', 'aipcf-ai-provider-for-cloudflare'), $connected ? null : $connect); if ($gate) { $ok = is_connector_approved(); $out[] = self::result('approval', __('The AI plugin allows AIPCF', 'aipcf-ai-provider-for-cloudflare'), $ok ? 'ok' : 'bad', $ok ? '' : __('Until it is allowed, every AI request is blocked.', 'aipcf-ai-provider-for-cloudflare'), $ok ? null : [__('Connector Approvals', 'aipcf-ai-provider-for-cloudflare'), SettingsPage::connectorApprovalUrl()]); } if ($connected && (!$gate || is_connector_approved())) { TaskContext::pin(self::TASK); // so a failure shows up as Diagnostics in Recent errors $test = SettingsPage::testCredentials(get_account_id(), get_api_key()); TaskContext::clear(); $out[] = self::result('answers', __('Cloudflare accepts the token', 'aipcf-ai-provider-for-cloudflare'), $test === true ? 'ok' : 'bad', $test === true ? '' : (string) $test, $test === true ? null : $connect); if ($test === true) { $out[] = self::chat(); if (EmbeddingSettings::enabled()) { $out[] = self::embedding(); $out[] = self::store(); } } } if (!empty(\WordPress\CloudflareWorkersAiProvider\Features\Settings::get('agents_enabled'))) { $out[] = self::agents(); } $out[] = self::cron(); $out[] = self::budget(); $out[] = self::aiPlugin(); $recent = ErrorLog::countSince(DAY_IN_SECONDS); $out[] = self::result( 'errors', __('No failed requests in the last day', 'aipcf-ai-provider-for-cloudflare'), $recent === 0 ? 'ok' : 'warn', /* translators: %d: number of failed requests */ $recent === 0 ? '' : sprintf(_n('%d failed request in the last day. See Recent errors.', '%d failed requests in the last day. See Recent errors.', $recent, 'aipcf-ai-provider-for-cloudflare'), $recent), null ); set_transient(self::TRANSIENT . get_current_user_id(), ['at' => time(), 'results' => $out], HOUR_IN_SECONDS); return $out; } /** * The last run's results for this user, or null. * * @return array{at: int, results: list}|null */ public static function last(): ?array { $last = get_transient(self::TRANSIENT . get_current_user_id()); return is_array($last) && isset($last['results']) ? $last : null; } /** * @param array{0: string, 1: string}|null $fix * @return array{key: string, label: string, state: string, detail: string, fix: array{0: string, 1: string}|null} */ private static function result(string $key, string $label, string $state, string $detail, ?array $fix): array { return ['key' => $key, 'label' => $label, 'state' => $state, 'detail' => Redact::text($detail), 'fix' => $fix]; } /** * One tiny request to the writing model, through the same path as every feature. * * @return array{key: string, label: string, state: string, detail: string, fix: array{0: string, 1: string}|null} */ private static function chat(): array { $model = get_default_model_id(); /* translators: %s: model name */ $label = sprintf(__('%s answers', 'aipcf-ai-provider-for-cloudflare'), pretty_model_name($model)); if (!class_exists('\\WordPress\\AiClient\\AiClient')) { return self::result('chat', $label, 'skip', __('The WordPress AI Client is not available.', 'aipcf-ai-provider-for-cloudflare'), null); } $start = microtime(true); try { TaskContext::pin(self::TASK); $provider = \WordPress\AiClient\AiClient::defaultRegistry()->getProviderModel('cloudflare-workers-ai', $model); $text = trim(\WordPress\AiClient\AiClient::prompt('Reply with the single word OK.')->usingModel($provider)->usingMaxTokens(5)->generateTextResult()->toText()); } catch (\Throwable $e) { return self::result('chat', $label, 'bad', $e->getMessage(), [__('Settings > Models', 'aipcf-ai-provider-for-cloudflare'), SettingsPage::settingsUrl('models')]); } finally { TaskContext::clear(); } /* translators: %s: milliseconds */ $took = sprintf(__('in %s ms', 'aipcf-ai-provider-for-cloudflare'), number_format_i18n((microtime(true) - $start) * 1000)); return self::result('chat', $label, $text !== '' ? 'ok' : 'warn', $text !== '' ? $took : __('It answered with nothing.', 'aipcf-ai-provider-for-cloudflare'), null); } /** * @return array{key: string, label: string, state: string, detail: string, fix: array{0: string, 1: string}|null} */ private static function embedding(): array { $r = EmbeddingClient::embed(['AIPCF diagnostics'], null, self::TASK); $ok = !is_wp_error($r); return self::result('embedding', __('The content index can read text', 'aipcf-ai-provider-for-cloudflare'), $ok ? 'ok' : 'bad', $ok ? '' : $r->get_error_message(), $ok ? null : [__('Content index', 'aipcf-ai-provider-for-cloudflare'), SettingsPage::settingsUrl('content-index')]); } /** * @return array{key: string, label: string, state: string, detail: string, fix: array{0: string, 1: string}|null} */ private static function store(): array { $fix = [__('Content index', 'aipcf-ai-provider-for-cloudflare'), SettingsPage::settingsUrl('content-index')]; $label = __('The content index is reachable', 'aipcf-ai-provider-for-cloudflare'); $store = StoreFactory::reading(); if (is_wp_error($store)) { return self::result('store', $label, 'bad', $store->get_error_message(), $fix); } $status = $store->status(); if (is_wp_error($status)) { return self::result('store', $label, 'bad', $status->get_error_message(), $fix); } if (empty($status['exists'])) { return self::result('store', $label, 'bad', __('The index does not exist yet. Create it on Settings > Content index.', 'aipcf-ai-provider-for-cloudflare'), $fix); } /* translators: 1: store name, 2: number of entries */ return self::result('store', $label, 'ok', sprintf(__('%1$s, %2$s entries.', 'aipcf-ai-provider-for-cloudflare'), StoreFactory::available()[StoreFactory::currentId()] ?? StoreFactory::currentId(), number_format_i18n((int) ($status['vector_count'] ?? 0))), null); } /** * Whether the background work runs: WordPress's scheduler must reach the hourly run. * * @return array{key: string, label: string, state: string, detail: string, fix: array{0: string, 1: string}|null} */ private static function cron(): array { $label = __('Background work runs', 'aipcf-ai-provider-for-cloudflare'); $next = wp_next_scheduled(Ingest::HOOK); if ($next === false) { return self::result('cron', $label, 'warn', __('Nothing is scheduled. Deactivating and activating AIPCF schedules it again.', 'aipcf-ai-provider-for-cloudflare'), null); } $late = time() - (int) $next; if ($late > HOUR_IN_SECONDS) { $detail = defined('DISABLE_WP_CRON') && \DISABLE_WP_CRON ? __('WordPress\'s scheduler is off (DISABLE_WP_CRON) and no server cron runs wp-cron.php.', 'aipcf-ai-provider-for-cloudflare') /* translators: %s: how late, e.g. "3 hours" */ : sprintf(__('The hourly run is %s late. On quiet sites WordPress runs it only when someone visits.', 'aipcf-ai-provider-for-cloudflare'), human_time_diff((int) $next)); return self::result('cron', $label, 'warn', $detail, null); } $last = (int) (State::get()['last_run'] ?? 0); /* translators: %s: time since, e.g. "5 mins" */ return self::result('cron', $label, 'ok', $last > 0 ? sprintf(__('Last run %s ago.', 'aipcf-ai-provider-for-cloudflare'), human_time_diff($last)) : '', null); } /** * @return array{key: string, label: string, state: string, detail: string, fix: array{0: string, 1: string}|null} */ private static function budget(): array { $used = (float) (UsageTracker::todayTotals()['neurons'] ?? 0); $budget = UsageTracker::budgetEnabled() ? UsageTracker::budget() : UsageTracker::FREE_TIER; $pct = (int) round(100 * $used / max(1, $budget)); $state = $pct >= 100 ? 'bad' : ($pct >= 80 ? 'warn' : 'ok'); /* translators: 1: neurons used today, 2: the daily budget, 3: percentage */ $detail = sprintf(__('%1$s of %2$s neurons today (%3$d%%).', 'aipcf-ai-provider-for-cloudflare'), number_format_i18n($used), number_format_i18n($budget), $pct); return self::result('budget', __('Today\'s budget has room', 'aipcf-ai-provider-for-cloudflare'), $state, $detail, $state === 'ok' ? null : [__('Settings > Budget', 'aipcf-ai-provider-for-cloudflare'), SettingsPage::settingsUrl('budget')]); } /** * AI agents: the abilities are registered (so the MCP adapter can offer them). * * @return array{key: string, label: string, state: string, detail: string, fix: array{0: string, 1: string}|null} */ private static function agents(): array { $label = __('AI agents can reach the site', 'aipcf-ai-provider-for-cloudflare'); $fix = [__('Features > AI agents', 'aipcf-ai-provider-for-cloudflare'), \WordPress\CloudflareWorkersAiProvider\Admin\FeaturesPage::url('agents')]; $missing = []; foreach (array_keys(Agents::names()) as $name) { if (!function_exists('wp_has_ability') || !wp_has_ability($name)) { $missing[] = $name; } } if ($missing !== []) { $why = Agents::enabled() ? implode(', ', $missing) : __('The content index is off.', 'aipcf-ai-provider-for-cloudflare'); /* translators: %s: ability names, or the reason */ return self::result('agents', $label, 'bad', sprintf(__('Not registered: %s', 'aipcf-ai-provider-for-cloudflare'), $why), $fix); } if (!Agents::mcpAdapter()) { return self::result('agents', $label, 'warn', __('The abilities work, but the MCP adapter plugin is not active, so agents cannot connect yet.', 'aipcf-ai-provider-for-cloudflare'), $fix); } /* translators: %d: number of abilities */ return self::result('agents', $label, 'ok', sprintf(__('%d abilities, through the MCP adapter.', 'aipcf-ai-provider-for-cloudflare'), count(Agents::names())), null); } /** * @return array{key: string, label: string, state: string, detail: string, fix: array{0: string, 1: string}|null} */ private static function aiPlugin(): array { $ai = ai_plugin_status(); return self::result('ai_plugin', __('The WordPress AI plugin is on', 'aipcf-ai-provider-for-cloudflare'), $ai['status'] === 'active' ? 'ok' : 'skip', $ai['status'] === 'active' ? '' : __('Optional: it adds AI to the block editor.', 'aipcf-ai-provider-for-cloudflare'), $ai['status'] === 'active' ? null : [$ai['action_label'], $ai['action_url']]); } }