feat(ai): 打通 SEO 闭环与投稿写作辅助

对齐 Zibll 的 AI 生成 SEO 流程:统一 WP AI Client/主题 Provider 双后端、文章/分类 SEO 框与生成回填、投稿摘要/标签建议,并收敛扩展页为连接配置。
This commit is contained in:
kate522 committed 2026-07-12 15:36:04 +08:00
1 parent a4cd5276e2
commit b36186da65
14 files changed
+1243 -330

No files matched your search

+199 -64
View File
@@ -2,8 +2,9 @@
/**
* AKSRT AI — AI 子系统核心
*
* 统一管理 AI provider 调用、速率限制和输出处理。
* 默认关闭,需管理员显式启用。
* 统一管理 AI 调用:优先 WordPress AI Client(Connectors),
* 回退主题 OpenAI 兼容配置。默认关闭,需管理员显式启用主题 provider
* 或配置核心 Connectors。
*
* @package AKSRT
*/
@@ -25,7 +26,7 @@ class AKSRT_AI {
}
/**
* 获取 AI 配置
* 获取 AI 配置(主题侧 provider)
*/
public function get_config(): array {
$defaults = [
@@ -35,11 +36,13 @@ class AKSRT_AI {
'api_base' => 'https://api.openai.com/v1',
'model' => 'gpt-3.5-turbo',
'max_tokens' => 1000,
'rate_limit' => 10, // 每分钟最大调用次数
'rate_limit' => 10,
];
$saved = get_option('aksrt_ai_settings', []);
if (!is_array($saved)) $saved = [];
if (!is_array($saved)) {
$saved = [];
}
return array_merge($defaults, $saved);
}
@@ -49,9 +52,8 @@ class AKSRT_AI {
*/
public function update_config(array $config): bool {
$current = $this->get_config();
$merged = array_merge($current, $config);
$merged = array_merge($current, $config);
// 如果 api_key 为掩码,保留原值
if (isset($config['api_key']) && $config['api_key'] === '***') {
$merged['api_key'] = $current['api_key'];
}
@@ -60,125 +62,258 @@ class AKSRT_AI {
}
/**
* 检查 AI 是否可用
* WordPress 核心 AI Client 是否可用。
*/
public function is_available(): bool {
public function is_wp_ai_available(): bool {
if (!function_exists('wp_ai_client_prompt')) {
return false;
}
if (function_exists('wp_supports_ai') && !wp_supports_ai()) {
return false;
}
return true;
}
/**
* 主题 OpenAI 兼容 provider 是否可用。
*/
public function is_theme_provider_available(): bool {
$config = $this->get_config();
return !empty($config['enabled'])
&& $config['provider'] !== self::PROVIDER_NONE
&& ($config['provider'] ?? self::PROVIDER_NONE) !== self::PROVIDER_NONE
&& !empty($config['api_key']);
}
/**
* 获取 AI 状态
* 任一后端可用即可生成。
*/
public function is_available(): bool {
return $this->is_wp_ai_available() || $this->is_theme_provider_available();
}
/**
* 当前优先后端标识。
*/
public function resolve_backend(): string {
if ($this->is_wp_ai_available()) {
return 'wp_ai';
}
if ($this->is_theme_provider_available()) {
return 'theme';
}
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' => $config['provider'],
'configured' => !empty($config['api_key']),
'model' => $config['model'],
'api_key' => !empty($config['api_key']) ? '***' : '',
'rate_limit' => $config['rate_limit'],
'available' => $this->is_available(),
'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(),
];
}
/**
* 调用 AI 生成
* 统一文本生成出口。
*
* @param string $prompt 提示词
* @param array $options 选项
* @return array{success: bool, content?: string, message?: string}
* @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');
// 1) Prefer WP AI Client
if ($this->is_wp_ai_available()) {
$wp_result = $this->generate_via_wp_ai($prompt, $system, $options);
if (!empty($wp_result['success'])) {
$this->record_call();
$wp_result['backend'] = 'wp_ai';
return $wp_result;
}
// Fall through to theme if configured
if (!$this->is_theme_provider_available()) {
$this->record_call();
return $wp_result;
}
}
// 2) Theme OpenAI-compatible
$theme_result = $this->generate_via_theme($prompt, $system, $options);
$this->record_call();
if (!empty($theme_result['success'])) {
$theme_result['backend'] = 'theme';
}
return $theme_result;
}
/**
* 兼容旧调用。
*/
public function generate(string $prompt, array $options = []): array {
if (!$this->is_available()) {
return ['success' => false, 'message' => 'AI 服务未启用或未配置'];
}
return $this->generate_text($prompt, $options);
}
// 速率限制检查
if (!$this->check_rate_limit()) {
return ['success' => false, 'message' => '请求过于频繁,请稍后再试'];
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'),
];
}
}
$config = $this->get_config();
$max_tokens = (int) ($options['max_tokens'] ?? $config['max_tokens']);
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($config['api_base'] . '/chat/completions', [
$response = wp_remote_post($api_base . '/chat/completions', [
'headers' => [
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . $config['api_key'],
],
'body' => wp_json_encode([
'body' => wp_json_encode([
'model' => $config['model'],
'messages' => [
['role' => 'system', 'content' => '你是一个内容助手,提供建议性内容。输出仅作为参考建议,不会自动应用到线上内容。'],
['role' => 'system', 'content' => $system],
['role' => 'user', 'content' => $prompt],
],
'max_tokens' => $max_tokens,
'temperature' => $temperature,
]),
'timeout' => 30,
'timeout' => 45,
]);
// 记录调用
$this->record_call();
if (is_wp_error($response)) {
return ['success' => false, 'message' => 'API 请求失败: ' . $response->get_error_message()];
return [
'success' => false,
'message' => sprintf(
/* translators: %s: error message */
__('API 请求失败: %s', 'aksrt'),
$response->get_error_message()
),
];
}
$body = json_decode(wp_remote_retrieve_body($response), true);
$status_code = wp_remote_retrieve_response_code($response);
$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 = $body['error']['message'] ?? '未知错误';
return ['success' => false, 'message' => 'API 错误 (' . $status_code . '): ' . $error_msg];
$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 = $body['choices'][0]['message'] ?? [];
$content = $message['content'] ?? '';
$message = is_array($body) ? ($body['choices'][0]['message'] ?? []) : [];
$content = is_array($message) ? (string) ($message['content'] ?? '') : '';
// 兼容推理模型(如 mimo-v2.5-pro):content 为空时取 reasoning_content
if (empty($content) && !empty($message['reasoning_content'])) {
$content = $message['reasoning_content'];
if ($content === '' && is_array($message) && !empty($message['reasoning_content'])) {
$content = (string) $message['reasoning_content'];
}
if (empty($content)) {
return ['success' => false, 'message' => 'AI 返回内容为空(可能 max_tokens 不足)'];
$content = trim($content);
if ($content === '') {
return [
'success' => false,
'message' => __('AI 返回内容为空(可能 max_tokens 不足)', 'aksrt'),
];
}
return [
'success' => true,
'content' => trim($content),
'reasoning' => $message['reasoning_content'] ?? '',
'usage' => $body['usage'] ?? [],
'finish_reason' => $body['choices'][0]['finish_reason'] ?? '',
'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;
$limit = (int) $config['rate_limit'];
if ($limit <= 0) {
return true;
}
$key = 'aksrt_ai_rate_' . get_current_user_id();
$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();
$key = 'aksrt_ai_rate_' . get_current_user_id();
$count = (int) get_transient($key);
set_transient($key, $count + 1, MINUTE_IN_SECONDS);
}