false, 'provider' => self::PROVIDER_NONE, 'api_key' => '', 'api_base' => 'https://api.openai.com/v1', 'model' => 'gpt-3.5-turbo', 'max_tokens' => 1000, 'rate_limit' => 10, ]; $saved = get_option('aksrt_ai_settings', []); if (!is_array($saved)) { $saved = []; } return array_merge($defaults, $saved); } /** * 更新 AI 配置 */ public function update_config(array $config): bool { $current = $this->get_config(); $merged = array_merge($current, $config); // Preserve existing key when client sends the masked placeholder. if (isset($config['api_key']) && $config['api_key'] === '***') { $merged['api_key'] = $current['api_key']; } $merged['enabled'] = !empty($merged['enabled']); $merged['provider'] = sanitize_key((string) ($merged['provider'] ?? self::PROVIDER_NONE)); $merged['api_key'] = (string) ($merged['api_key'] ?? ''); $merged['api_base'] = esc_url_raw((string) ($merged['api_base'] ?? 'https://api.openai.com/v1')); $merged['model'] = sanitize_text_field((string) ($merged['model'] ?? 'gpt-3.5-turbo')); $merged['max_tokens'] = max(100, min(4000, (int) ($merged['max_tokens'] ?? 1000))); $merged['rate_limit'] = max(1, min(100, (int) ($merged['rate_limit'] ?? 10))); // Persist only the known keys so shape stays stable. $to_store = [ 'enabled' => $merged['enabled'], 'provider' => $merged['provider'], 'api_key' => $merged['api_key'], 'api_base' => $merged['api_base'], 'model' => $merged['model'], 'max_tokens' => $merged['max_tokens'], 'rate_limit' => $merged['rate_limit'], ]; $before = get_option('aksrt_ai_settings', []); $updated = update_option('aksrt_ai_settings', $to_store); if ($updated) { return true; } // update_option returns false when the value is unchanged — treat as success. if (is_array($before)) { $before_normalized = array_merge([ 'enabled' => false, 'provider' => self::PROVIDER_NONE, 'api_key' => '', 'api_base' => 'https://api.openai.com/v1', 'model' => 'gpt-3.5-turbo', 'max_tokens' => 1000, 'rate_limit' => 10, ], $before); $before_normalized['enabled'] = !empty($before_normalized['enabled']); $before_normalized['provider'] = sanitize_key((string) ($before_normalized['provider'] ?? self::PROVIDER_NONE)); $before_normalized['api_key'] = (string) ($before_normalized['api_key'] ?? ''); $before_normalized['api_base'] = esc_url_raw((string) ($before_normalized['api_base'] ?? 'https://api.openai.com/v1')); $before_normalized['model'] = sanitize_text_field((string) ($before_normalized['model'] ?? 'gpt-3.5-turbo')); $before_normalized['max_tokens'] = max(100, min(4000, (int) ($before_normalized['max_tokens'] ?? 1000))); $before_normalized['rate_limit'] = max(1, min(100, (int) ($before_normalized['rate_limit'] ?? 10))); return maybe_serialize($before_normalized) === maybe_serialize($to_store) || $before_normalized == $to_store; } return maybe_serialize($before) === maybe_serialize($to_store); } /** * WordPress 核心 AI Client 是否真正可用。 * * 仅有函数存根不够:没有 Connectors / wp_supports_ai=false 时不应抢占主题 provider。 */ public function is_wp_ai_available(): bool { if (!function_exists('wp_ai_client_prompt')) { return false; } // Prefer the official capability gate when present. if (function_exists('wp_supports_ai')) { return (bool) wp_supports_ai(); } // Without wp_supports_ai, do not claim availability (avoids false-positive 400s). return false; } /** * 主题 OpenAI 兼容 provider 是否可用。 */ public function is_theme_provider_available(): bool { $config = $this->get_config(); $provider = sanitize_key((string) ($config['provider'] ?? self::PROVIDER_NONE)); $api_key = trim((string) ($config['api_key'] ?? '')); $api_base = trim((string) ($config['api_base'] ?? '')); return !empty($config['enabled']) && $provider !== self::PROVIDER_NONE && $provider !== '' && $api_key !== '' && $api_base !== ''; } /** * 任一后端可用即可生成。 */ public function is_available(): bool { return $this->is_theme_provider_available() || $this->is_wp_ai_available(); } /** * 当前优先后端标识。 * * Theme provider wins when explicitly configured — WP AI stubs often exist * without a working connector and previously produced opaque 400 failures. */ public function resolve_backend(): string { if ($this->is_theme_provider_available()) { return 'theme'; } if ($this->is_wp_ai_available()) { return 'wp_ai'; } return 'none'; } /** * 获取 AI 状态(不回显完整密钥)。 */ public function get_status(): array { $config = $this->get_config(); $seo = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_seo_settings() : []; return [ 'enabled' => !empty($config['enabled']), 'provider' => (string) ($config['provider'] ?? self::PROVIDER_NONE), 'configured' => !empty($config['api_key']) || $this->is_wp_ai_available(), 'model' => (string) ($config['model'] ?? ''), 'api_base' => (string) ($config['api_base'] ?? ''), 'api_key' => !empty($config['api_key']) ? '***' : '', 'max_tokens' => (int) ($config['max_tokens'] ?? 1000), 'rate_limit' => (int) ($config['rate_limit'] ?? 10), 'available' => $this->is_available(), 'backend' => $this->resolve_backend(), 'wp_ai_available' => $this->is_wp_ai_available(), 'theme_available' => $this->is_theme_provider_available(), 'seo_enabled' => !empty($seo['seo_enabled']), 'seo_ai_enabled' => !empty($seo['seo_ai_enabled']), 'write_assist' => $this->is_available(), ]; } /** * 统一文本生成出口。 * * @param string $prompt 用户提示 / 上下文 * @param array $options system_instruction, max_tokens, temperature * @return array{success:bool,content?:string,message?:string,backend?:string} */ public function generate_text(string $prompt, array $options = []): array { if (!$this->is_available()) { return [ 'success' => false, 'message' => __('AI 服务未启用或未配置。请配置 WordPress AI Connectors,或在「扩展 → AI 辅助」填写 API Key。', 'aksrt'), ]; } if (!$this->check_rate_limit()) { return [ 'success' => false, 'message' => __('请求过于频繁,请稍后再试', 'aksrt'), ]; } $system = isset($options['system_instruction']) ? (string) $options['system_instruction'] : __('你是一个内容助手,提供建议性内容。输出仅作为参考建议,不会自动应用到线上内容。', 'aksrt'); $last_error = [ 'success' => false, 'message' => __('AI 服务未启用或未配置。请配置 WordPress AI Connectors,或在「扩展 → AI 辅助」填写 API Key。', 'aksrt'), ]; // 1) Prefer explicitly configured theme OpenAI-compatible provider. if ($this->is_theme_provider_available()) { $theme_result = $this->generate_via_theme($prompt, $system, $options); $this->record_call(); if (!empty($theme_result['success'])) { $theme_result['backend'] = 'theme'; return $theme_result; } $last_error = $theme_result; // Fall through to WP AI if available. } // 2) WordPress AI Client (Connectors). if ($this->is_wp_ai_available()) { $wp_result = $this->generate_via_wp_ai($prompt, $system, $options); $this->record_call(); if (!empty($wp_result['success'])) { $wp_result['backend'] = 'wp_ai'; return $wp_result; } $last_error = $wp_result; } return $last_error; } /** * 兼容旧调用。 */ public function generate(string $prompt, array $options = []): array { return $this->generate_text($prompt, $options); } private function generate_via_wp_ai(string $prompt, string $system, array $options): array { try { $builder = wp_ai_client_prompt($prompt); if (is_object($builder) && method_exists($builder, 'using_system_instruction') && $system !== '') { $builder = $builder->using_system_instruction($system); } if (!is_object($builder) || !method_exists($builder, 'generate_text')) { return [ 'success' => false, 'message' => __('WordPress AI Client 接口不可用', 'aksrt'), ]; } $text = $builder->generate_text(); if (is_wp_error($text)) { return [ 'success' => false, 'message' => $text->get_error_message(), ]; } $text = is_string($text) ? trim($text) : ''; if ($text === '') { return [ 'success' => false, 'message' => __('AI 未返回任何文本', 'aksrt'), ]; } return [ 'success' => true, 'content' => $text, ]; } catch (Throwable $t) { return [ 'success' => false, 'message' => $t->getMessage() ?: __('WordPress AI Client 调用失败', 'aksrt'), ]; } } private function generate_via_theme(string $prompt, string $system, array $options): array { $config = $this->get_config(); $max_tokens = (int) ($options['max_tokens'] ?? $config['max_tokens']); $temperature = (float) ($options['temperature'] ?? 0.7); $api_base = rtrim((string) $config['api_base'], '/'); $response = wp_remote_post($api_base . '/chat/completions', [ 'headers' => [ 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $config['api_key'], ], 'body' => wp_json_encode([ 'model' => $config['model'], 'messages' => [ ['role' => 'system', 'content' => $system], ['role' => 'user', 'content' => $prompt], ], 'max_tokens' => $max_tokens, 'temperature' => $temperature, ]), 'timeout' => 45, ]); if (is_wp_error($response)) { return [ 'success' => false, 'message' => sprintf( /* translators: %s: error message */ __('API 请求失败: %s', 'aksrt'), $response->get_error_message() ), ]; } $body = json_decode((string) wp_remote_retrieve_body($response), true); $status_code = (int) wp_remote_retrieve_response_code($response); if ($status_code !== 200) { $error_msg = is_array($body) ? ($body['error']['message'] ?? __('未知错误', 'aksrt')) : __('未知错误', 'aksrt'); return [ 'success' => false, 'message' => sprintf( /* translators: 1: HTTP status, 2: error message */ __('API 错误 (%1$d): %2$s', 'aksrt'), $status_code, $error_msg ), ]; } $message = is_array($body) ? ($body['choices'][0]['message'] ?? []) : []; $content = is_array($message) ? (string) ($message['content'] ?? '') : ''; if ($content === '' && is_array($message) && !empty($message['reasoning_content'])) { $content = (string) $message['reasoning_content']; } $content = trim($content); if ($content === '') { return [ 'success' => false, 'message' => __('AI 返回内容为空(可能 max_tokens 不足)', 'aksrt'), ]; } return [ 'success' => true, 'content' => $content, 'reasoning' => is_array($message) ? (string) ($message['reasoning_content'] ?? '') : '', 'usage' => is_array($body) ? ($body['usage'] ?? []) : [], 'finish_reason' => is_array($body) ? (string) ($body['choices'][0]['finish_reason'] ?? '') : '', ]; } private function check_rate_limit(): bool { $config = $this->get_config(); $limit = (int) $config['rate_limit']; if ($limit <= 0) { return true; } $key = 'aksrt_ai_rate_' . get_current_user_id(); $count = (int) get_transient($key); return $count < $limit; } private function record_call(): void { $key = 'aksrt_ai_rate_' . get_current_user_id(); $count = (int) get_transient($key); set_transient($key, $count + 1, MINUTE_IN_SECONDS); } }