WP_REST_Server::CREATABLE, 'callback' => array( $this, 'analyze_intent' ), 'permission_callback' => array( $this, 'check_permission' ), ) ); register_rest_route( $namespace, '/article/generate', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'generate_article_v2' ), 'permission_callback' => array( $this, 'check_permission' ), ) ); register_rest_route( $namespace, '/page/generate', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'generate_page' ), 'permission_callback' => array( $this, 'check_permission' ), ) ); register_rest_route( $namespace, '/podcast/generate', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'generate_podcast' ), 'permission_callback' => array( $this, 'check_permission' ), ) ); register_rest_route( $namespace, '/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 ), ); } /** * 统一处理主站 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 ); } }