'GET', 'callback' => [$this, 'get_status'], 'permission_callback' => [$this, 'check_logged_in'], ]); register_rest_route(self::API_NAMESPACE, '/ai/settings', [ 'methods' => 'POST', 'callback' => [$this, 'update_settings'], 'permission_callback' => [$this, 'check_permission'], ]); register_rest_route(self::API_NAMESPACE, '/ai/seo-generate', [ 'methods' => 'POST', 'callback' => [$this, 'seo_generate'], 'permission_callback' => [$this, 'check_logged_in'], ]); register_rest_route(self::API_NAMESPACE, '/ai/seo-suggest', [ 'methods' => 'POST', 'callback' => [$this, 'seo_suggest'], 'permission_callback' => [$this, 'check_write_assist'], ]); register_rest_route(self::API_NAMESPACE, '/ai/summary-suggest', [ 'methods' => 'POST', 'callback' => [$this, 'summary_suggest'], 'permission_callback' => [$this, 'check_write_assist'], ]); register_rest_route(self::API_NAMESPACE, '/ai/tags-suggest', [ 'methods' => 'POST', 'callback' => [$this, 'tags_suggest'], 'permission_callback' => [$this, 'check_write_assist'], ]); } public function check_logged_in(): bool { return is_user_logged_in(); } /** * 投稿/编辑写作辅助:已登录且可编辑文章或允许前台投稿。 */ public function check_write_assist(): bool { if (!is_user_logged_in()) { return false; } if (current_user_can('edit_posts')) { return true; } if (class_exists('\AKSRT\Core\FrontSubmit')) { return (bool) \AKSRT\Core\FrontSubmit::get_instance()->can_submit(); } return false; } public function get_status(): \WP_REST_Response { if (!class_exists('\AKSRT\Ai\AI')) { return $this->success(['data' => [ 'enabled' => false, 'provider' => 'none', 'configured' => false, 'available' => false, 'backend' => 'none', ]]); } return $this->success(['data' => \AKSRT\Ai\AI::get_instance()->get_status()]); } public function update_settings(\WP_REST_Request $request): \WP_REST_Response { if (!class_exists('\AKSRT\Ai\AI')) { return $this->error(__('AI 模块未加载', 'aksrt'), 500); } $params = $this->get_json_params($request); $allowed = [ 'enabled' => 'bool', 'provider' => 'string', 'api_key' => 'string', 'api_base' => 'url', 'model' => 'string', 'max_tokens' => 'int', 'rate_limit' => 'int', ]; $config = []; foreach ($allowed as $key => $type) { if (!isset($params[$key])) { continue; } switch ($type) { case 'bool': $config[$key] = !empty($params[$key]); break; case 'int': $config[$key] = (int) $params[$key]; break; case 'url': $config[$key] = esc_url_raw((string) $params[$key]); break; default: $config[$key] = sanitize_text_field((string) $params[$key]); } } $success = \AKSRT\Ai\AI::get_instance()->update_config($config); if ($success) { return $this->success([ 'message' => __('AI 设置已保存', 'aksrt'), 'data' => \AKSRT\Ai\AI::get_instance()->get_status(), ]); } return $this->error(__('保存失败', 'aksrt')); } /** * POST /ai/seo-generate — 从 post/term 实体生成 SEO 三字段。 */ public function seo_generate(\WP_REST_Request $request): \WP_REST_Response { if (!class_exists('\AKSRT\Ai\AISEO')) { return $this->error(__('AI SEO 模块未加载', 'aksrt'), 500); } $params = $this->get_json_params($request); $type = sanitize_key((string) ($params['type'] ?? 'post')); $id = absint($params['id'] ?? 0); if (!in_array($type, ['post', 'term'], true) || $id <= 0) { return $this->error(__('参数错误', 'aksrt'), 422); } if ($type === 'post') { if (!current_user_can('edit_post', $id)) { return $this->error(__('权限不足,无法编辑此文章', 'aksrt'), 403); } } else { if (!current_user_can('edit_term', $id)) { return $this->error(__('权限不足,无法编辑此分类/标签', 'aksrt'), 403); } } $result = \AKSRT\Ai\AISEO::get_instance()->generate_fields($type, $id); if (empty($result['success'])) { return $this->error((string) ($result['message'] ?? __('生成失败', 'aksrt'))); } return $this->success(['data' => $result['data']]); } public function seo_suggest(\WP_REST_Request $request): \WP_REST_Response { if (!class_exists('\AKSRT\Ai\AISEO')) { return $this->error(__('AI SEO 模块未加载', 'aksrt'), 500); } $params = $this->get_json_params($request); $title = sanitize_text_field((string) ($params['title'] ?? '')); $content = wp_kses_post((string) ($params['content'] ?? '')); $post_id = absint($params['post_id'] ?? 0); if ($post_id > 0 && !current_user_can('edit_post', $post_id)) { return $this->error(__('权限不足', 'aksrt'), 403); } if ($title === '') { return $this->error(__('标题不能为空', 'aksrt')); } $result = \AKSRT\Ai\AISEO::get_instance()->suggest($title, $content); if (!empty($result['success'])) { return $this->success(['data' => $result['data']]); } return $this->error((string) ($result['message'] ?? __('生成失败', 'aksrt'))); } public function summary_suggest(\WP_REST_Request $request): \WP_REST_Response { if (!class_exists('\AKSRT\Ai\AI') || !class_exists('\AKSRT\Ai\AIPrompt')) { return $this->error(__('AI 模块未加载', 'aksrt'), 500); } $params = $this->get_json_params($request); // Accept JSON body or form body (some admin contexts strip JSON). if ($params === []) { $params = $request->get_params(); if (!is_array($params)) { $params = []; } } $title = sanitize_text_field((string) ($params['title'] ?? '')); $raw = (string) ($params['content'] ?? ''); // Keep readable text; strip tags for emptiness / prompt length. $plain = trim(wp_strip_all_tags(html_entity_decode($raw, ENT_QUOTES | ENT_HTML5, 'UTF-8'))); $content = $plain !== '' ? $plain : trim(wp_strip_all_tags(wp_kses_post($raw))); $max_len = max(50, min(500, (int) ($params['max_length'] ?? 200))); if ($content === '') { return $this->error(__('内容不能为空,请先在编辑器填写正文后再生成摘要', 'aksrt'), 422); } $ai = \AKSRT\Ai\AI::get_instance(); if (!$ai->is_available()) { return $this->error( __('AI 服务未启用或未配置。请到「扩展 → AI 辅助」启用并填写 API Key(或配置 WordPress AI Connectors)。', 'aksrt'), 503 ); } // Prefer excerpt-oriented prompt when title is present. $prompt_builder = \AKSRT\Ai\AIPrompt::get_instance(); if ($title !== '' && method_exists($prompt_builder, 'build_excerpt_prompt')) { $prompt = $prompt_builder->build_excerpt_prompt($title, $content); } else { $prompt = $prompt_builder->build_summary_prompt($content, $max_len); } $result = $ai->generate_text($prompt, [ 'max_tokens' => min(600, max(120, $max_len * 2)), 'temperature' => 0.5, 'system_instruction' => __('你是中文内容编辑,只输出摘要正文,不要标题、前缀或 Markdown。', 'aksrt'), ]); if (!empty($result['success'])) { $summary = trim((string) ($result['content'] ?? '')); if ($summary === '') { return $this->error(__('AI 返回内容为空,请稍后重试或检查模型配置', 'aksrt'), 502); } // Soft trim to requested length (characters). if (function_exists('mb_substr') && mb_strlen($summary) > $max_len + 20) { $summary = mb_substr($summary, 0, $max_len); } return $this->success([ 'data' => [ 'summary' => $summary, 'backend' => (string) ($result['backend'] ?? ''), ], ]); } $message = trim((string) ($result['message'] ?? '')); if ($message === '') { $message = __('生成失败', 'aksrt'); } // Upstream/provider failures are not client validation errors. $status = 502; if (str_contains($message, '未启用') || str_contains($message, '未配置')) { $status = 503; } elseif (str_contains($message, '频繁')) { $status = 429; } return $this->error($message, $status); } public function tags_suggest(\WP_REST_Request $request): \WP_REST_Response { if (!class_exists('\AKSRT\Ai\AI') || !class_exists('\AKSRT\Ai\AIPrompt')) { return $this->error(__('AI 模块未加载', 'aksrt'), 500); } $params = $this->get_json_params($request); $title = sanitize_text_field((string) ($params['title'] ?? '')); $content = wp_kses_post((string) ($params['content'] ?? '')); if ($title === '' && trim(wp_strip_all_tags($content)) === '') { return $this->error(__('标题或内容不能为空', 'aksrt')); } $prompt = \AKSRT\Ai\AIPrompt::get_instance()->build_tags_prompt($title, $content); $result = \AKSRT\Ai\AI::get_instance()->generate_text($prompt, [ 'max_tokens' => 300, 'temperature' => 0.5, ]); if (!empty($result['success'])) { $tags = json_decode((string) $result['content'], true); if (!is_array($tags)) { $tags = array_filter(array_map('trim', explode(',', (string) $result['content']))); } $tags = array_values(array_map('sanitize_text_field', $tags)); return $this->success(['data' => ['tags' => $tags]]); } return $this->error((string) ($result['message'] ?? __('生成失败', 'aksrt'))); } }