byok > trial > none *───────────────────────────────────────────────────────────────*/ function aish_active_mode(): string { $opt = get_option( AISH_OPT_KEY, [] ); if ( ! empty( $opt['site_token'] ) ) return 'pro'; if ( ! empty( $opt['openai_key'] ) ) return 'byok'; if ( ! empty( $opt['trial_token'] ) ) return 'trial'; return 'none'; } /*─────────────────────────────────────────────────────────────── * BYOK: Direct OpenAI call for SEO snippets *───────────────────────────────────────────────────────────────*/ function aish_call_openai_direct( string $api_key, array $post_data ): array { $post_title = $post_data['post_title'] ?? ''; $post_excerpt = $post_data['post_excerpt'] ?? ''; $post_type = $post_data['post_type'] ?? 'post'; $slug = $post_data['slug'] ?? ''; $prompt = "You are an expert SEO specialist. Analyze the following content and generate optimized SEO metadata.\n\n" . "Post Type: {$post_type}\n" . "Title: {$post_title}\n" . "Content Summary: {$post_excerpt}\n" . "Current URL Slug: {$slug}\n\n" . "Generate a JSON response with these exact fields:\n" . "- \"keyword\": A concise focus keyword (2-4 words) that best represents the content\n" . "- \"title\": An SEO-optimized page title (50-60 characters) that includes the focus keyword\n" . "- \"description\": A compelling meta description (120-155 characters) that includes the focus keyword\n" . "- \"slug\": A short, keyword-rich URL slug using only lowercase letters, numbers, and hyphens\n\n" . "Rules:\n" . "1. The focus keyword MUST appear in the title, description, AND slug\n" . "2. Title should be engaging with the keyword placed near the beginning\n" . "3. Description should summarize the content and encourage clicks\n" . "4. Slug should be concise (3-5 words) and SEO-friendly\n" . "5. All text must be in the same language as the original content\n\n" . "Return ONLY valid JSON, no other text."; $body = [ 'model' => 'gpt-4.1-mini', 'messages' => [ [ 'role' => 'user', 'content' => $prompt ], ], 'max_tokens' => 500, 'temperature' => 0.3, ]; $res = wp_remote_post( 'https://api.openai.com/v1/chat/completions', [ 'headers' => [ 'Authorization' => 'Bearer ' . $api_key, 'Content-Type' => 'application/json', ], 'body' => wp_json_encode( $body ), 'timeout' => 30, ] ); if ( is_wp_error( $res ) ) { return [ 'error' => $res->get_error_message() ]; } $code = (int) wp_remote_retrieve_response_code( $res ); $raw = wp_remote_retrieve_body( $res ); $json = json_decode( $raw, true ); if ( $code === 401 ) { return [ 'error' => 'Invalid OpenAI API key. Please check your key in Settings.' ]; } if ( $code === 429 ) { return [ 'error' => 'OpenAI rate limit reached. Please try again in a moment.' ]; } if ( $code !== 200 ) { $msg = $json['error']['message'] ?? "OpenAI HTTP {$code}"; return [ 'error' => $msg ]; } $content = $json['choices'][0]['message']['content'] ?? ''; $parsed = json_decode( $content, true ); if ( ! is_array( $parsed ) || empty( $parsed['keyword'] ) ) { if ( preg_match( '/\{.*\}/s', $content, $m ) ) { $parsed = json_decode( $m[0], true ); } } if ( ! is_array( $parsed ) ) { return [ 'error' => 'Could not parse AI response.' ]; } return [ 'keyword' => $parsed['keyword'] ?? '', 'title' => $parsed['title'] ?? '', 'description' => $parsed['description'] ?? '', 'slug' => $parsed['slug'] ?? '', ]; } /*─────────────────────────────────────────────────────────────── * Trial: Call backend /trial-snippet endpoint *───────────────────────────────────────────────────────────────*/ function aish_call_trial_api( string $trial_token, array $post_data ): array { $data = [ 'trial_token' => $trial_token, 'post_title' => $post_data['post_title'] ?? '', 'post_excerpt' => $post_data['post_excerpt'] ?? '', 'post_type' => $post_data['post_type'] ?? 'post', 'slug' => $post_data['slug'] ?? '', 'domain' => home_url(), 'plugin' => 'ai-snippet-seo-helper', ]; $res = wp_remote_post( 'https://rankpilotai.com/wp-json/rankpilotai/v1/trial-snippet', [ 'headers' => [ 'Content-Type' => 'application/json' ], 'body' => wp_json_encode( $data ), 'timeout' => 30, ] ); if ( is_wp_error( $res ) ) { return [ 'error' => $res->get_error_message() ]; } $code = (int) wp_remote_retrieve_response_code( $res ); $json = json_decode( wp_remote_retrieve_body( $res ), true ); if ( $code !== 200 ) { $msg = isset( $json['error'] ) && is_scalar( $json['error'] ) ? (string) $json['error'] : "Trial API HTTP {$code}"; return [ 'error' => $msg, 'exhausted' => ! empty( $json['exhausted'] ) ]; } return $json; } /** Minimal multibyte polyfill (for environments without mbstring) */ if ( ! function_exists( 'mb_strlen' ) ) { function mb_strlen( $s, $enc = '' ) { return strlen( $s ); } } /** * Turn an API error value into a readable message. * APIs may return a plain string or a structured object such as * {"error":{"message":"..."}}; anything else falls back. */ if ( ! function_exists( 'aish_error_text' ) ) { function aish_error_text( $value, string $fallback = '' ) : string { if ( is_scalar( $value ) && '' !== (string) $value ) return (string) $value; if ( is_array( $value ) ) { foreach ( [ 'message', 'detail', 'error', 'msg' ] as $k ) { if ( isset( $value[ $k ] ) && is_scalar( $value[ $k ] ) && '' !== (string) $value[ $k ] ) { return (string) $value[ $k ]; } } } return $fallback; } } /** Debug logger (active only when WP_DEBUG is true) */ if ( ! function_exists( 'aish_log' ) ) { function aish_log( $msg, array $ctx = [] ) : void { if ( defined('WP_DEBUG') && WP_DEBUG ) { $line = '[AISH] ' . ( is_string($msg) ? $msg : wp_json_encode($msg) ); if ( $ctx ) $line .= ' ' . wp_json_encode($ctx); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- debug output, gated by WP_DEBUG. error_log( $line ); } } } /** Sanitize API snippet response fields */ if ( ! function_exists( 'aish_sanitize_snippet_response' ) ) { function aish_sanitize_snippet_response( array $res ) : array { foreach ( ['keyword','title','description','slug'] as $k ) { if ( isset( $res[$k] ) ) { $res[$k] = wp_strip_all_tags( (string) $res[$k] ); } } return $res; } } /** Token-info cache helpers */ if ( ! function_exists( 'aish_token_cache_key' ) ) { function aish_token_cache_key( string $site_token ) : string { return 'aish_tok_' . wp_hash( home_url() . '|' . $site_token ); } } if ( ! function_exists( 'aish_flush_token_cache' ) ) { function aish_flush_token_cache( string $site_token ) : void { delete_transient( aish_token_cache_key( $site_token ) ); } } /** Normalize "global remaining" from token-info response */ if ( ! function_exists( 'aish_status_remaining' ) ) { function aish_status_remaining( array $st ) : int { // Prefer explicit global remaining fields if ( isset($st['remaining_total']) ) return (int)$st['remaining_total']; if ( isset($st['remaining_all']) ) return (int)$st['remaining_all']; if ( isset($st['global_remaining']) ) return (int)$st['global_remaining']; // Sum balances if present $fb = (int)($st['free_balance'] ?? 0); $pr = (int)($st['plan_remaining'] ?? 0); $pb = (int)($st['pack_balance'] ?? 0); $sum_bal = $fb + $pr + $pb; if ( $sum_bal > 0 ) return $sum_bal; // Legacy field names $free = (int)($st['free'] ?? 0); $monthly = (int)($st['monthly_remaining'] ?? 0); $pack = (int)($st['token_pack'] ?? 0); $sum_legacy = $free + $monthly + $pack; if ( $sum_legacy > 0 ) return $sum_legacy; // Generic "remaining" if ( isset($st['remaining']) ) { return max(0, (int)$st['remaining']); } // Fallback: limit - usage $limit = (int)($st['plan_limit_total'] ?? $st['plan_limit'] ?? $st['token_limit'] ?? 0); $usage = null; foreach ( ['usage_total','all_usage','total_usage','token_usage','usage'] as $k ) { if ( isset($st[$k]) ) { $usage = (int)$st[$k]; break; } } if ( $usage === null ) $usage = (int)($st['site_usage'] ?? 0); return max(0, $limit - (int)$usage); } } /* ========================================================= * 1) Snippet Score Calculation * Simple 3-rule check: keyword found in normalized Title, Description, and Slug. * ======================================================= */ if ( ! function_exists( 'aish_calc_score' ) ) { function aish_calc_score( $fw, $ti, $de, $slug ) { $norm_text = function( $s ){ $s = strtolower( (string) $s ); if ( function_exists('remove_accents') ) $s = remove_accents($s); return trim( preg_replace('/[^a-z0-9]+/',' ', $s ) ); }; $norm_slug = function( $s ){ $s = strtolower( (string) $s ); if ( function_exists('remove_accents') ) $s = remove_accents($s); return trim( preg_replace('/[^a-z0-9]+/','-', $s ), '-' ); }; $arr = array_map( 'trim', explode( ',', (string) $fw ) ); $kw = strtolower( $arr[0] ?? '' ); if ( $kw === '' ) { return [ 'score' => 0, 'color' => '#e74c3c' ]; } $has_title = strpos( ' ' . $norm_text($ti) . ' ', ' ' . $norm_text($kw) . ' ' ) !== false; $has_desc = strpos( ' ' . $norm_text($de) . ' ', ' ' . $norm_text($kw) . ' ' ) !== false; $has_slug = strpos( $norm_slug($slug), $norm_slug($kw) ) !== false; $hit = (int)$has_title + (int)$has_desc + (int)$has_slug; $score = ($hit === 0) ? 0 : (($hit === 1) ? 33 : (($hit === 2) ? 66 : 100)); $color = ($score >= 100) ? '#2980b9' : (($score >= 66) ? '#f39c12' : '#e74c3c'); return [ 'score' => $score, 'color' => $color ]; } } /** Minimal post payload for bulk operations */ function aish_prepare_snippet_item_for_post( int $post_id, array $opts = [] ) : array { $title = get_the_title( $post_id ); $excerpt = get_the_excerpt( $post_id ); $ptype = get_post_type( $post_id ); $fw = get_post_meta( $post_id, 'rank_math_focus_keyword', true ); return [ 'id' => (int) $post_id, 'context_type' => 'post', 'post_title' => $title, 'post_excerpt' => $excerpt, 'post_type' => $ptype, 'focus_keyword' => $fw, 'slug' => get_post_field( 'post_name', $post_id ), ]; } /* ========================================================= * 2) RankPilot AI — /snippet * ======================================================= */ if ( ! function_exists( 'aish_call_rankpilot_api' ) ) { function aish_call_rankpilot_api( string $site_token, array $payload ) : array { if ( empty( $site_token ) ) { throw new Exception( 'Site token missing.' ); } $payload = array_merge( $payload, [ 'site_token' => $site_token, 'plugin' => 'ai-snippet-seo-helper', 'site_url' => home_url(), ] ); // Allow plugins/themes to adjust payload $payload = apply_filters( 'aish/snippet_payload', $payload, $site_token ); $r = aish_http_json( AISH_RP_API . '/snippet', $payload, 'POST', ['timeout'=>30] ); if ( is_wp_error( $r ) ) { aish_log('API request error', ['err'=>$r->get_error_message()]); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- delivered as JSON to admin.js and shown with alert(); never echoed as HTML. throw new Exception( 'API request error: ' . $r->get_error_message() ); } $code = wp_remote_retrieve_response_code( $r ); $body = wp_remote_retrieve_body( $r ); if ( (int)$code !== 200 ) { aish_log('RankPilotAI API HTTP error', ['code'=>$code,'body'=>substr((string)$body,0,300)]); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- delivered as JSON to admin.js and shown with alert(); never echoed as HTML. throw new Exception( 'RankPilotAI API Error (HTTP ' . $code . '): ' . $body ); } $j = json_decode( (string)$body, true ); if ( ! is_array( $j ) ) { aish_log('Invalid JSON response', ['body'=>substr((string)$body,0,300)]); throw new Exception( 'Invalid JSON response.' ); } $j = aish_sanitize_snippet_response( $j ); // Usage changed → bust token-info cache immediately aish_flush_token_cache( $site_token ); // Allow plugins/themes to inspect/adjust response $j = apply_filters( 'aish/snippet_response', $j, $site_token ); return $j; } } /** * Try /snippet-bulk; if it fails, fall back to single /snippet calls. */ function aish_call_rankpilot_api_bulk( string $site_token, array $items, string $model_sel, array $opts = [] ) : array { if ( empty( $site_token ) ) { throw new Exception('Site token missing.'); } if ( empty( $items ) ) { return [ 'items' => [] ]; } $endpoint = AISH_RP_API . '/snippet-bulk'; $payload = [ 'site_token' => $site_token, 'plugin' => 'ai-snippet-seo-helper', 'site_url' => home_url(), 'domain' => wp_parse_url( home_url(), PHP_URL_HOST ), 'model' => strtolower( $model_sel ?: 'gpt-4-turbo' ), 'custom_prompt' => $opts['custom_prompt'] ?? '', 'auto_slug' => ( $opts['auto_slug'] ?? 'no' ) === 'yes', 'items' => array_values( $items ), ]; $res = aish_http_json( $endpoint, $payload, 'POST', [ 'timeout' => 120 ] ); if ( ! is_wp_error( $res ) ) { $code = (int) wp_remote_retrieve_response_code( $res ); $body = wp_remote_retrieve_body( $res ); if ( $code === 200 ) { $json = json_decode( (string)$body, true ); if ( is_array($json) ) { if ( empty($json['items']) ) $json['items'] = $json; return $json; } } } // Fallback: single /snippet loop $out = []; foreach ( $items as $it ) { $single = array_merge( $it, [ 'model' => strtolower( $model_sel ?: 'gpt-4-turbo' ), 'auto_slug' => ( $opts['auto_slug'] ?? 'no' ) === 'yes', 'custom_prompt' => $opts['custom_prompt'] ?? '', ] ); try { $resp = aish_call_rankpilot_api( $site_token, $single ); $resp['id'] = (int) ($it['id'] ?? 0); $out[ (int)$it['id'] ] = $resp; } catch ( \Exception $ex ) { $out[ (int)$it['id'] ] = [ 'id'=>(int)$it['id'], 'error'=>$ex->getMessage() ]; } usleep( 50000 ); // 50ms backoff } return [ 'items' => $out, 'fallback' => 'single' ]; } /** Apply a generated snippet to Rank Math fields for a given post */ function aish_apply_snippet_result_for_post( int $post_id, array $data, array $opts = [] ) : bool { if ( ! empty($data['error']) ) return false; $kw = $data['keyword'] ?? ''; $ti = $data['title'] ?? ''; $desc = $data['description'] ?? ''; $slug = $data['slug'] ?? ''; // Rank Math fields if ( $kw ) update_post_meta( $post_id, 'rank_math_focus_keyword', $kw ); if ( $ti ) update_post_meta( $post_id, 'rank_math_title', $ti ); if ( $desc ) update_post_meta( $post_id, 'rank_math_description', $desc ); // Slug (optional) if ( ( $opts['auto_slug'] ?? 'no' ) === 'yes' && $slug ) { $san = sanitize_title( $slug ); $cur = get_post_field( 'post_name', $post_id ); if ( $san && $san !== $cur ) { add_post_meta( $post_id, '_wp_old_slug', $cur ); wp_update_post( [ 'ID' => $post_id, 'post_name' => $san ] ); } } $final_slug = get_post_field( 'post_name', $post_id ); $sc = aish_calc_score( $kw, $ti, $desc, $final_slug ); update_post_meta( $post_id, '_aish_snippet_score', $sc['score'] ); update_post_meta( $post_id, '_aish_score', $sc['score'] ); // legacy return true; } /* ========================================================= * 3) /token-info * ======================================================= */ if ( ! function_exists( 'aish_check_token_status' ) ) { function aish_check_token_status( string $site_token, bool $force_refresh = false ) : array { $cache_key = aish_token_cache_key( $site_token ); // Soft cache with optional bypass if ( $force_refresh ) { delete_transient( $cache_key ); } else { $cached = get_transient( $cache_key ); if ( is_array( $cached ) ) { return $cached; } } if ( empty($site_token) ) { return [ 'error' => 'No token provided' ]; } $body = [ 'site_token' => $site_token, 'plugin' => 'ai-snippet-seo-helper', 'site_url' => home_url(), 'cache_bust' => time(), 'include' => 'balances,usage,limits', ]; $r = aish_http_json( AISH_RP_API . '/token-info', $body, 'POST', [ 'timeout' => 20, 'headers' => [ 'Cache-Control' => 'no-cache' ] ] ); if ( is_wp_error( $r ) ) { aish_log('Token-info connection error', ['err'=>$r->get_error_message()]); return [ 'error' => 'Connection Error' ]; } $resp = wp_remote_retrieve_body( $r ); $data = json_decode( (string)$resp, true ); if ( ! is_array($data) ) { aish_log('Invalid token-info response', ['body'=>substr((string)$resp,0,300)]); return [ 'error' => 'Invalid token-info response' ]; } // Ensure at least one allowed model is present if ( empty($data['allowed_models']) ) { $data['allowed_models'] = [ 'gpt-4-turbo' ]; } set_transient( $cache_key, $data, 30 ); return $data; } } /* ========================================================= * 4) /token-reduce * ======================================================= */ if ( ! function_exists( 'aish_reduce_rankpilot_token' ) ) { function aish_reduce_rankpilot_token( string $site_token, string $model = 'gpt-4-turbo' ) : array { $body = [ 'site_token' => $site_token, 'plugin' => 'ai-snippet-seo-helper', 'model' => strtolower( $model ), 'site_url' => home_url(), ]; $r = aish_http_json( AISH_RP_API . '/token-reduce', $body, 'POST', ['timeout'=>20] ); if ( is_wp_error( $r ) ) { aish_log('Token-reduce connection error', ['err'=>$r->get_error_message()]); return [ 'error' => 'Connection Error' ]; } $data = json_decode( (string) wp_remote_retrieve_body( $r ), true ); if ( is_array( $data ) && empty( $data['error'] ) ) { aish_flush_token_cache( $site_token ); } return is_array( $data ) ? $data : []; } }