WP_REST_Server::CREATABLE, 'callback' => array( $this, 'extract_featured_image_content' ), 'permission_callback' => array( $this, 'check_permission' ), ) ); register_rest_route( 'wpthemoon/v1', '/featured-image/generate', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'generate_featured_image' ), 'permission_callback' => array( $this, 'check_permission' ), ) ); register_rest_route( 'wpthemoon/v1', '/featured-image/status', array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_featured_image_status' ), 'permission_callback' => array( $this, 'check_permission' ), ) ); register_rest_route( 'wpthemoon/v1', '/featured-image/apply', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'apply_featured_image' ), 'permission_callback' => array( $this, 'check_permission' ), ) ); register_rest_route( 'wpthemoon/v1', '/intent/analyze', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'analyze_intent' ), 'permission_callback' => array( $this, 'check_permission' ), ) ); register_rest_route( 'wpthemoon/v1', '/article/generate', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'generate_article_v2' ), 'permission_callback' => array( $this, 'check_permission' ), ) ); register_rest_route( 'wpthemoon/v1', '/page/generate', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'generate_page' ), 'permission_callback' => array( $this, 'check_permission' ), ) ); register_rest_route( 'wpthemoon/v1', '/podcast/generate', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'generate_podcast' ), 'permission_callback' => array( $this, 'check_permission' ), ) ); register_rest_route( 'wpthemoon/v1', '/user/credits', array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_user_credits' ), 'permission_callback' => array( $this, 'check_permission' ), ) ); } public function check_permission() { return current_user_can( 'edit_posts' ) || current_user_can( 'edit_pages' ); } /** * 代理播客生成接口,并将音频保存到媒体库。 */ public function generate_podcast( WP_REST_Request $request ) { $content = $request->get_param( 'content' ); $voice = sanitize_text_field( $request->get_param( 'voice' ) ?: 'Rachel' ); if ( empty( $content ) ) { return new WP_Error( 'invalid_request', __( 'Content is required.', 'wpthemoon' ), array( 'status' => 400 ) ); } $client = new WPTheMoon_API_Client(); $result = $client->post( '/podcast', array( 'content' => $content, 'voice' => $voice, ) ); if ( is_wp_error( $result ) ) { return $this->ensure_api_result( $result ); } // 删除积分缓存,确保前端刷新时拿到最新积分 delete_transient( 'wpthemoon_credits_cache' ); if ( ! empty( $result['audio_url'] ) ) { $sideload_result = $this->sideload_audio_to_media_library( $result['audio_url'] ); if ( ! is_wp_error( $sideload_result ) ) { $result['audio_url'] = $sideload_result['url']; $result['attachment_id'] = $sideload_result['id']; } } return rest_ensure_response( $result ); } /** * 将远程音频文件下载并导入 WordPress 媒体库。 * * @param string $audio_url 远程音频 URL。 * @return array|WP_Error 包含 id 和 url 的数组,或错误。 */ private function sideload_audio_to_media_library( $audio_url ) { require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/media.php'; require_once ABSPATH . 'wp-admin/includes/image.php'; $tmp_file = download_url( $audio_url, 60 ); if ( is_wp_error( $tmp_file ) ) { return $tmp_file; } $path = wp_parse_url( $audio_url, PHP_URL_PATH ); $filename = $path ? wp_basename( $path ) : 'podcast.mp3'; if ( false === strpos( $filename, '.' ) ) { $filename .= '.mp3'; } $file_array = array( 'name' => sanitize_file_name( $filename ), 'tmp_name' => $tmp_file, ); $attachment_id = media_handle_sideload( $file_array, 0 ); if ( is_wp_error( $attachment_id ) ) { @unlink( $tmp_file ); return $attachment_id; } return array( 'id' => $attachment_id, 'url' => wp_get_attachment_url( $attachment_id ), ); } /** * 代理封面内容提取。 */ public function extract_featured_image_content( WP_REST_Request $request ) { $title = sanitize_text_field( $request->get_param( 'title' ) ); $excerpt = sanitize_textarea_field( $request->get_param( 'excerpt' ) ); $content = wp_kses_post( $request->get_param( 'content' ) ); $document_parts = array(); if ( ! empty( $title ) ) { $document_parts[] = 'Title: ' . $title; } if ( ! empty( $excerpt ) ) { $document_parts[] = 'Excerpt: ' . $excerpt; } if ( ! empty( $content ) ) { $document_parts[] = "Content:\n" . wp_strip_all_tags( $content ); } $full_document = implode( "\n\n", $document_parts ); if ( empty( $full_document ) ) { return new WP_Error( 'invalid_request', __( 'Post content is empty.', 'wpthemoon' ), array( 'status' => 400 ) ); } $client = new WPTheMoon_API_Client(); $result = $client->post( '/cover/cover-extract', array( 'fullDocument' => $full_document, ) ); return $this->ensure_api_result( $result ); } /** * 代理通用图片生成接口。 */ public function generate_featured_image( WP_REST_Request $request ) { $tool_id = sanitize_text_field( $request->get_param( 'toolId' ) ?: 'wordpress-featured-image' ); $prompt = sanitize_textarea_field( $request->get_param( 'prompt' ) ); $image_size = sanitize_text_field( $request->get_param( 'imageSize' ) ); $aspect_ratio = sanitize_text_field( $request->get_param( 'aspectRatio' ) ); $n = absint( $request->get_param( 'n' ) ?: 1 ); $seed = $request->get_param( 'seed' ); if ( empty( $prompt ) ) { return new WP_Error( 'invalid_request', __( 'Prompt is required.', 'wpthemoon' ), array( 'status' => 400 ) ); } $payload = array( 'toolId' => $tool_id, 'prompt' => $prompt, 'imageSize' => $image_size, 'aspectRatio' => $aspect_ratio, 'n' => max( 1, $n ), ); if ( is_numeric( $seed ) ) { $payload['seed'] = intval( $seed ); } $client = new WPTheMoon_API_Client(); $result = $client->post( '/image/generate', $payload ); return $this->ensure_api_result( $result ); } /** * 代理通用图片状态查询接口。 */ public function get_featured_image_status( WP_REST_Request $request ) { $task_id = sanitize_text_field( $request->get_param( 'taskId' ) ); $model_id = sanitize_text_field( $request->get_param( 'modelId' ) ); if ( empty( $task_id ) ) { return new WP_Error( 'invalid_request', __( 'taskId is required.', 'wpthemoon' ), array( 'status' => 400 ) ); } $client = new WPTheMoon_API_Client(); $result = $client->get( '/image/status', array_filter( array( 'taskId' => $task_id, 'modelId' => $model_id, ) ) ); return $this->ensure_api_result( $result ); } /** * 下载图片到媒体库并设置 featured image。 */ public function apply_featured_image( WP_REST_Request $request ) { $post_id = absint( $request->get_param( 'postId' ) ); $image_url = esc_url_raw( $request->get_param( 'imageUrl' ) ); $title = sanitize_text_field( $request->get_param( 'title' ) ); if ( ! $post_id || ! get_post( $post_id ) ) { return new WP_Error( 'invalid_post', __( 'A valid postId is required.', 'wpthemoon' ), array( 'status' => 400 ) ); } if ( ! current_user_can( 'edit_post', $post_id ) ) { return new WP_Error( 'forbidden', __( 'You do not have permission to edit this post.', 'wpthemoon' ), array( 'status' => 403 ) ); } if ( empty( $image_url ) ) { return new WP_Error( 'invalid_image', __( 'imageUrl is required.', 'wpthemoon' ), array( 'status' => 400 ) ); } require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/media.php'; require_once ABSPATH . 'wp-admin/includes/image.php'; $tmp_file = download_url( $image_url, 30 ); if ( is_wp_error( $tmp_file ) ) { return new WP_Error( 'download_failed', $tmp_file->get_error_message(), array( 'status' => 500 ) ); } $path = wp_parse_url( $image_url, PHP_URL_PATH ); $filename = $path ? wp_basename( $path ) : 'wpthemoon-featured-image.jpg'; if ( false === strpos( $filename, '.' ) ) { $filename .= '.jpg'; } $file_array = array( 'name' => sanitize_file_name( $filename ), 'tmp_name' => $tmp_file, ); $attachment_id = media_handle_sideload( $file_array, $post_id, $title ?: get_the_title( $post_id ) ); if ( is_wp_error( $attachment_id ) ) { @unlink( $tmp_file ); return new WP_Error( 'media_sideload_failed', $attachment_id->get_error_message(), array( 'status' => 500 ) ); } set_post_thumbnail( $post_id, $attachment_id ); return rest_ensure_response( array( 'attachmentId' => $attachment_id, 'imageUrl' => wp_get_attachment_url( $attachment_id ), 'postId' => $post_id, ) ); } /** * 统一处理主站 API 响应。 */ private function ensure_api_result( $result ) { if ( is_wp_error( $result ) ) { $code = $result->get_error_code(); $data = $result->get_error_data( $code ); $status = ( is_array( $data ) && isset( $data['status'] ) ) ? $data['status'] : 500; return new WP_Error( $code, $result->get_error_message(), array( 'status' => $status ) ); } return rest_ensure_response( $result ); } /** * 代理封装图片生成接口。 */ public function analyze_intent( WP_REST_Request $request ) { $keyword = sanitize_text_field( $request->get_param( 'keyword' ) ); $topic_idea = sanitize_textarea_field( $request->get_param( 'topicIdea' ) ); $reference_url = esc_url_raw( $request->get_param( 'referenceUrl' ) ); if ( empty( $keyword ) ) { return new WP_Error( 'invalid_request', __( 'Keyword is required.', 'wpthemoon' ), array( 'status' => 400 ) ); } $api_key = get_option( 'wpthemoon_api_key' ); $base_url = defined('WPTHEMOON_API_BASE_URL') ? WPTHEMOON_API_BASE_URL : 'https://www.wpthemoon.com/api/open/v1'; if ( empty( $api_key ) ) { return new WP_Error( 'no_api_key', __( 'API Key is not configured', 'wpthemoon' ), array( 'status' => 400 ) ); } $endpoint = $base_url . '/intent'; $body = array( 'keyword' => $keyword, 'topicIdea' => $topic_idea, 'referenceUrl' => $reference_url, ); $body_json = wp_json_encode( $body ); // 1. 设置流式响应头(application/json 用于意图分析) if (!headers_sent()) { header('Content-Type: application/json; charset=utf-8'); header('Cache-Control: no-cache, no-transform'); header('X-Accel-Buffering: no'); header('Connection: keep-alive'); } // 2. 关闭 PHP 输出缓冲 while (ob_get_level()) { ob_end_clean(); } // 3. 使用 cURL 流式透传 $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $endpoint); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, $body_json); curl_setopt($ch, CURLOPT_HTTPHEADER, array( 'x-api-key: ' . $api_key, 'Content-Type: application/json', 'Accept: application/json', )); curl_setopt($ch, CURLOPT_TIMEOUT, 120); // 2分钟超时 curl_setopt($ch, CURLOPT_ENCODING, ''); curl_setopt($ch, CURLOPT_HEADER, 0); // 实时输出 curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) { echo $data; if (ob_get_level() > 0) ob_flush(); flush(); return strlen($data); }); curl_exec($ch); if (curl_errno($ch)) { $error_msg = curl_error($ch); if (strpos($error_msg, 'bad decrypt') === false && strpos($error_msg, 'LibreSSL') === false) { echo "\n" . wp_json_encode(array('error' => $error_msg)); } } curl_close($ch); exit; } /** * 生成文章 (流式响应) */ public function generate_article_v2( WP_REST_Request $request ) { $keyword = sanitize_text_field( $request->get_param( 'keyword' ) ); $analysis = $request->get_param( 'analysis' ); $article_type = sanitize_text_field( $request->get_param( 'articleType' ) ?: 'general' ); $topic_idea = sanitize_textarea_field( $request->get_param( 'topicIdea' ) ); $reference_url = esc_url_raw( $request->get_param( 'referenceUrl' ) ); $transcript = $request->get_param( 'transcript' ); $target_audience = sanitize_text_field( $request->get_param( 'targetAudience' ) ) ?: get_option( 'wpthemoon_target_audience' ); if ( empty( $keyword ) ) { return new WP_Error( 'invalid_request', __( 'Keyword is required.', 'wpthemoon' ), array( 'status' => 400 ) ); } if ( empty( $analysis ) ) { return new WP_Error( 'invalid_request', __( 'Analysis result is required.', 'wpthemoon' ), array( 'status' => 400 ) ); } $api_key = get_option( 'wpthemoon_api_key' ); $base_url = defined('WPTHEMOON_API_BASE_URL') ? WPTHEMOON_API_BASE_URL : 'https://www.wpthemoon.com/api/open/v1'; if ( empty( $api_key ) ) { return new WP_Error( 'no_api_key', __( 'API Key is not configured', 'wpthemoon' ), array( 'status' => 400 ) ); } $endpoint = $base_url . '/article'; $body = array( 'keyword' => $keyword, 'analysis' => $analysis, 'articleType' => $article_type, 'topicIdea' => $topic_idea, 'referenceUrl' => $reference_url, 'transcript' => $transcript, 'targetAudience' => $target_audience, ); $body_json = wp_json_encode( $body ); // 1. 设置流式响应头(text/plain 以匹配 Next.js API) if (!headers_sent()) { header('Content-Type: text/plain; charset=utf-8'); header('Cache-Control: no-cache, no-transform'); header('X-Accel-Buffering: no'); // 禁用 Nginx 缓冲 header('Connection: keep-alive'); } // 2. 彻底关闭 PHP 输出缓冲 while (ob_get_level()) { ob_end_clean(); } // 3. 使用 cURL 流式透传 $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $endpoint); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, $body_json); curl_setopt($ch, CURLOPT_HTTPHEADER, array( 'x-api-key: ' . $api_key, 'Content-Type: application/json', 'Accept: text/plain', // 接受 text/plain 响应 )); curl_setopt($ch, CURLOPT_TIMEOUT, 300); curl_setopt($ch, CURLOPT_ENCODING, ''); // 自动处理 gzip 解压 curl_setopt($ch, CURLOPT_HEADER, 0); // 不包含响应头 // 关键:实时将接收到的数据吐给客户端 curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) { echo $data; if (ob_get_level() > 0) ob_flush(); flush(); return strlen($data); }); curl_exec($ch); if (curl_errno($ch)) { $error_msg = curl_error($ch); // macOS LibreSSL 在处理 HTTP/2 连接结束时常报 bad decrypt, errno 0。 // 此时流数据通常已完整接收,我们忽略这个特定的底层报错。 if (strpos($error_msg, 'bad decrypt') === false && strpos($error_msg, 'LibreSSL') === false) { echo "\n__ERROR__\n" . wp_json_encode(array('message' => $error_msg)); } } curl_close($ch); // 必须终止,防止 WordPress 追加多余的 JSON exit; } /** * 生成页面 (流式响应) */ public function generate_page( WP_REST_Request $request ) { $prompt = sanitize_textarea_field( $request->get_param( 'prompt' ) ); $page_type = sanitize_text_field( $request->get_param( 'pageType' ) ?: 'general' ); $page_title = sanitize_text_field( $request->get_param( 'pageTitle' ) ); if ( empty( $prompt ) ) { return new WP_Error( 'invalid_request', __( 'Prompt is required.', 'wpthemoon' ), array( 'status' => 400 ) ); } $api_key = get_option( 'wpthemoon_api_key' ); $base_url = defined('WPTHEMOON_API_BASE_URL') ? WPTHEMOON_API_BASE_URL : 'https://www.wpthemoon.com/api/open/v1'; if ( empty( $api_key ) ) { return new WP_Error( 'no_api_key', __( 'API Key is not configured', 'wpthemoon' ), array( 'status' => 400 ) ); } $endpoint = $base_url . '/page'; $body = array( 'prompt' => $prompt, 'pageType' => 'landing' === $page_type ? 'landing' : 'general', 'pageTitle' => $page_title, ); $body_json = wp_json_encode( $body ); if (!headers_sent()) { header('Content-Type: application/json; charset=utf-8'); header('Cache-Control: no-cache, no-transform'); header('X-Accel-Buffering: no'); header('Connection: keep-alive'); } while (ob_get_level()) { ob_end_clean(); } $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $endpoint); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, $body_json); curl_setopt($ch, CURLOPT_HTTPHEADER, array( 'x-api-key: ' . $api_key, 'Content-Type: application/json', 'Accept: application/json', )); curl_setopt($ch, CURLOPT_TIMEOUT, 300); curl_setopt($ch, CURLOPT_ENCODING, ''); curl_setopt($ch, CURLOPT_HEADER, 0); curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) { echo $data; if (ob_get_level() > 0) ob_flush(); flush(); return strlen($data); }); curl_exec($ch); if (curl_errno($ch)) { $error_msg = curl_error($ch); if (strpos($error_msg, 'bad decrypt') === false && strpos($error_msg, 'LibreSSL') === false) { echo "\n" . wp_json_encode(array('error' => $error_msg)); } } curl_close($ch); exit; } /** * 获取用户积分(带缓存) */ public function get_user_credits( WP_REST_Request $request ) { // 尝试从缓存获取(缓存 2 分钟,编辑器中刷新更频繁) $credits_info = get_transient( 'wpthemoon_credits_cache' ); if ( false === $credits_info ) { $client = new WPTheMoon_API_Client(); $credits_info = $client->get_credits(); if ( is_wp_error( $credits_info ) ) { return $credits_info; } // 缓存成功的响应 set_transient( 'wpthemoon_credits_cache', $credits_info, 2 * MINUTE_IN_SECONDS ); } return rest_ensure_response( $credits_info ); } }