返回

WordPress插件:快速解决OpenAI API返回null的5大坑

php

WordPress 插件开发:搞定 OpenAI API 调用返回 null 的那些坑

碰到了个有点意思的问题:想写个 WordPress 插件,它能抓取 Contact Form 7 表单里的信息,扔给 OpenAI API 去生成一篇博客文章,然后把结果显示给用户看。想法挺好,但在实际操作中,尤其是对于没写过 WordPress 插件的朋友来说,确实容易踩坑。

这位朋友遇到的具体情况是:插件的基础架子搭起来了,短代码(shortcode)也用了,REST API 端点也注册了,JavaScript 也写了,看着都挺像回事儿。但一点“生成”按钮,啥也没发生。打开浏览器开发者工具一看,网络请求倒是发出去了,可返回的数据里 text 字段愣是 null。这就有点头疼了,到底哪儿出问题了呢?

问题出在哪儿?剖析“text: null”背后的原因

拿到 text: null 的结果,直觉告诉我们,问题很大概率出在 WordPress 后端和 OpenAI API 服务器之间的“对话”环节。前端 JavaScript 似乎成功把请求发到了我们自己写的 WordPress REST API 端点,但这个端点在请求 OpenAI 时可能没成功,或者成功了但没拿到预期的文本内容。

仔细看看提供的代码,尤其是 openai-blog-generator.phpai-blog-generator-rest-api.php(看起来后者是前者的一个面向对象重构或补充,存在功能重叠),我们可以揪出几个最可疑的地方:

  1. OpenAI API Endpoint 地址不对: 代码里定义了 OPENAI_API_URLhttps://api.openai.com/v1/engines,然后在实际调用 wp_remote_post 时,又拼上了 /davinci-003/completions。这个组合看起来像是把新旧 API 的用法搞混了。OpenAI 的 Completions API,特别是对于像 davinci-003 这样的旧版模型(或者现在更推荐的 Chat Completions API),其基础 URL 通常是 https://api.openai.com/v1/completionshttps://api.openai.com/v1/chat/completions,而不是 /engines/.../engines 路径通常是用来列出可用模型的。
  2. API 请求结构可能不适配: 即使用了正确的 davinci-003 模型,它也属于旧的 Completions API。现在 OpenAI 主推的是 Chat Completions API(例如使用 gpt-3.5-turbogpt-4 模型),这个 API 的请求体结构要求使用 messages 数组,而不是简单的 prompt 字符串。如果目标 API endpoint 指向的是 Chat Completions,但发送的还是旧格式的 prompt 数据,API 肯定会一脸懵,给不出正确的文本结果。
  3. API Key 管理混乱: 代码里既用了 define('OPENAI_API_KEY', 'your_openai_api_key'); 硬编码(虽然值是占位符),又写了一套完整的设置页面逻辑来通过 get_option('ai_blog_generator_api_key') 获取密钥,甚至还加了个 http_request_args 钩子 ai_blog_generator_add_api_key 来尝试注入 API Key。这几种方式掺和在一起,很容易导致实际使用的 API Key 不对或者根本没用上。ai_blog_generator_generate_blog 函数和 AI_Blog_Generator_REST_API::generate_blog 方法直接用了 OPENAI_API_KEY 这个常量,可能就没用上设置里保存的 Key。
  4. JavaScript 本地化变量名不一致: wp_localize_script 函数的第三个参数是 PHP 数组,它会被转换成 JavaScript 对象。代码里用的是 aiBlogGeneratorSettings 作为这个 JS 对象的名字:wp_localize_script('ai-blog-generator', 'aiBlogGeneratorSettings', ...)。但是在 ai-blog-generator.js 文件里,AJAX 调用时却用了 ai_blog_generator_params.endpointai_blog_generator_params.nonce。这个 aiBlogGeneratorSettingsai_blog_generator_params 不匹配,导致 JavaScript 根本拿不到正确的 API 地址和 Nonce 值,AJAX 请求自然会失败。
  5. 代码冗余和潜在冲突: openai-blog-generator.php 文件里已经包含了注册 REST API 端点和其回调函数的代码。ai-blog-generator-rest-api.php 文件似乎又用类的方式实现了一遍相同的功能。如果两个文件都被加载了,可能会导致重复注册路由或者其他不可预料的冲突。需要确定到底用哪一套逻辑,并移除多余的。

搞清楚了这些潜在的问题点,我们就可以对症下药了。

解决方案:一步步修复你的 AI 博客生成器插件

下面我们来逐个解决上面分析到的问题。建议集中在一个核心插件文件(比如 openai-blog-generator.php)里进行修改,并移除或整合 ai-blog-generator-rest-api.php 中的重复逻辑。

方案一:修正 OpenAI API Endpoint 和请求结构

这是最核心的问题。OpenAI 已经迭代了 API,推荐使用 Chat Completions。我们来更新一下:

原理与作用:

  • 将 API endpoint 地址改为 https://api.openai.com/v1/chat/completions,这是当前推荐的文本生成接口。
  • 选用一个合适的 Chat 模型,比如 gpt-3.5-turbo,它性价比高,效果也不错。
  • 修改发送给 OpenAI API 的数据结构,使用 messages 数组来构造对话历史或指令,而不是单一的 prompt 字符串。

代码示例 (修改 ai_blog_generator_generate_blog 函数):

// Callback function for generating blog post
function ai_blog_generator_generate_blog($request) {
  error_log("REST API Endpoint: generate_blog callback started.");
  // 优先从设置中获取 API Key
  $api_key = get_option('ai_blog_generator_api_key');
  if (empty($api_key)) {
      // 如果设置里没有,再尝试用常量(但不推荐长期这样用)
      if (defined('OPENAI_API_KEY') && OPENAI_API_KEY != 'your_openai_api_key') {
          $api_key = OPENAI_API_KEY;
          error_log("Warning: Using API Key from constant. Recommend setting it in plugin settings.");
      } else {
          error_log("API Error: OpenAI API Key is not configured.");
          return new WP_Error('api_key_missing', 'OpenAI API Key is not configured.', array('status' => 500));
      }
  }

  $params = $request->get_params();

  // 获取并清理表单输入
  $topic = sanitize_text_field($params['blog_topic']);
  $style = sanitize_text_field($params['writing_style']);
  $outline = sanitize_textarea_field($params['outline']);

  error_log("Input Data: Topic: " . $topic . ", Writing Style: " . $style . ", Outline: " . $outline);

  // 构建符合 Chat Completions API 要求的 messages 数组
  $messages = array(
      array(
          "role" => "system",
          "content" => "You are a helpful assistant that generates blog posts based on user requirements." // 系统指令,可以优化
      ),
      array(
          "role" => "user",
          "content" => "Generate a blog post with the following details:\nBlog topic: $topic\nWriting style: $style\nOutline:\n$outline" // 用户具体要求
      )
  );
  error_log("Messages payload: " . print_r($messages, true));

  // 设置 API 请求数据 (注意:不再使用 prompt,而是 model 和 messages)
  $data = array(
    'model' => 'gpt-3.5-turbo', // 或者 gpt-4, gpt-4o 等你选用的模型
    'messages' => $messages,
    'temperature' => 0.7, // Temperature 可以按需调整
    'max_tokens' => 1024, // Max tokens 按需调整,确保足够长但别浪费
    // 'n' => 1, // Chat Completions 通常默认 n=1
  );

  // 配置 wp_remote_post 请求参数
  $args = array(
    'headers' => array(
      'Content-Type' => 'application/json',
      'Authorization' => 'Bearer ' . $api_key, // 直接使用从设置获取的 API Key
    ),
    'body' => json_encode($data),
    'timeout' => 60, // OpenAI 请求可能耗时较长,适当增加超时时间
    'method' => 'POST', // 明确指定 POST 方法
  );

  // 发送请求到 OpenAI Chat Completions API Endpoint
  $openai_api_url = 'https://api.openai.com/v1/chat/completions'; // 正确的 Endpoint
  error_log("Sending request to OpenAI API: " . $openai_api_url);
  $response = wp_remote_post($openai_api_url, $args);
  error_log("Received response from OpenAI API");

  // 处理响应
  if (is_wp_error($response)) {
    $error_message = $response->get_error_message();
    error_log("API Communication Error: " . $error_message);
    // 可以返回更具体的错误给前端
    return new WP_Error('api_comm_error', 'Error communicating with OpenAI: ' . $error_message, array('status' => 502)); // 502 Bad Gateway 可能更合适
  } else {
    $response_code = wp_remote_retrieve_response_code($response);
    $body = wp_remote_retrieve_body($response);
    error_log("OpenAI API Response Code: " . $response_code);
    error_log("OpenAI API Response Body: " . $body);

    if ($response_code >= 200 && $response_code < 300) {
        $json = json_decode($body, true);
        // 检查 OpenAI 是否返回了错误信息
        if (isset($json['error'])) {
            $openai_error_message = $json['error']['message'];
            error_log("OpenAI API Error returned: " . $openai_error_message);
            return new WP_Error('openai_api_error', 'OpenAI Error: ' . $openai_error_message, array('status' => 400)); // 可能是 Bad Request
        }
        // 从 Chat Completions 的响应结构中提取文本
        // 注意: 路径是 ['choices'][0]['message']['content']
        if (isset($json['choices'][0]['message']['content'])) {
            $text = $json['choices'][0]['message']['content'];
            error_log("Generated text: " . substr($text, 0, 200) . "..."); // 只记录部分内容防止日志过长
            // 这里可以做一些基本的清理,比如 trim whitespace
            $text = trim($text);
            return array('text' => $text);
        } else {
            error_log("API Response Error: Could not find text in expected structure.");
            return new WP_Error('api_response_parse_error', 'Could not parse text from OpenAI response.', array('status' => 500));
        }
    } else {
        // 处理非 2xx 的 HTTP 状态码
        error_log("OpenAI API returned non-2xx status: " . $response_code);
        // 尝试解析 body 中的错误信息
        $json = json_decode($body, true);
        $error_message = isset($json['error']['message']) ? $json['error']['message'] : 'Unknown error from OpenAI.';
        return new WP_Error('openai_http_error', 'OpenAI API request failed: (' . $response_code . ') ' . $error_message, array('status' => $response_code));
    }
  }
}

// 重要:如果使用了 AI_Blog_Generator_REST_API 类,请对那个类里的 generate_blog 方法做同样的修改。

安全建议:

  • 确保你的 OpenAI API Key 是通过设置页面输入的,而不是硬编码在代码里的。.gitignore 文件里应该包含插件目录,防止意外提交密钥。
  • Chat Completions API 的 system message 可以用来设定 AI 的行为和安全边界,比如指示它不要生成不适宜的内容。

进阶使用技巧:

  • 调整 temperature 参数控制生成文本的创造性(值越高越随机,越低越确定)。
  • 使用 max_tokens 参数精确控制生成内容的长度,避免超出预期或被截断。
  • 可以研究 top_pfrequency_penaltypresence_penalty 等参数进行更细致的调优。
  • 考虑加入错误重试机制,或者在前端给用户更友好的等待提示(比如 loading 动画)。

方案二:统一和优化 API Key 管理

代码里多种方式获取 API Key 很混乱,容易出错。我们应该统一使用 WordPress 的 options 系统。

原理与作用:

  • 移除硬编码的 define('OPENAI_API_KEY', ...)
  • 移除 add_filter('http_request_args', 'ai_blog_generator_add_api_key', ...) 这个钩子,因为它现在是多余的。
  • 在实际调用 wp_remote_post 时,明确从 get_option('ai_blog_generator_api_key') 获取密钥并添加到请求头中。

代码示例 (修改 openai-blog-generator.php):

<?php
/*
Plugin Name: AI Blog Generator
Description: Generates blog posts using OpenAI's API models
Version: 1.1
Author: trobanko
*/

// 移除这里的 define('OPENAI_API_KEY', 'your_openai_api_key');
// 移除这里的 define('OPENAI_API_URL', ...); // 不再需要旧的 URL 常量

// ... (保留 add_shortcode, rest_api_init 等)

// 修改 generate_blog 函数内部获取 API Key 的方式 (已在方案一的代码示例中体现)
function ai_blog_generator_generate_blog($request) {
    // 获取 API Key (应首先从 get_option 获取)
    $api_key = get_option('ai_blog_generator_api_key');
    if (empty($api_key)) {
        // 返回错误,提示用户在设置页面配置 Key
        error_log("API Error: OpenAI API Key is not configured in settings.");
        return new WP_Error('api_key_missing', 'Please configure your OpenAI API Key in the plugin settings.', array('status' => 400)); // Bad Request 比较合适,因为是配置问题
    }
    // ... 后续代码使用 $api_key ...
    // 参考方案一中的完整函数实现
}


// ... (保留 enqueue_scripts, enqueue_styles 等)

// 移除这个过滤器,不再需要它自动添加 Key
// remove_filter('http_request_args', 'ai_blog_generator_add_api_key', 10);
// function ai_blog_generator_add_api_key($args, $url) { ... } // 整段函数可以删掉

// ... (保留 settings 相关的代码:add_action admin_menu, admin_init 等)
// 确保 settings 页面正常工作,保存的 option key 是 'ai_blog_generator_api_key'
?>

安全建议:

  • 设置页面的 API Key 字段应该是 <input type="password"> 类型,这样输入时不会明文显示。虽然保存到数据库还是明文,但至少输入时安全些。更好的做法是考虑加密存储,但这会增加复杂性。
  • 确保只有管理员(或其他有权限的角色)能访问插件设置页面(manage_options 权限检查是正确的)。

方案三:修复 JavaScript 本地化变量名错误

JS 拿不到后端传过来的 API 地址和 Nonce,AJAX 请求自然失败。

原理与作用:

  • 确保 wp_localize_script 的第三个参数(JS 对象名)和 JS 文件里实际使用的对象名一致。

代码示例 (修改 openai-blog-generator.php 中的 ai_blog_generator_enqueue_scripts 函数):

function ai_blog_generator_enqueue_scripts() {
  error_log("Enqueueing scripts for AI Blog Generator.");
  wp_enqueue_script('ai-blog-generator', plugins_url('/js/ai-blog-generator.js', __FILE__), array('jquery'), '1.1', true); // 建议更新版本号

  // 使用 'ai_blog_generator_params' 作为 JS 对象名,与 JS 代码保持一致
  wp_localize_script('ai-blog-generator', 'ai_blog_generator_params', array(
    'apiUrl' => esc_url_raw(rest_url('ai-blog-generator/v1/generate-blog')),
    'nonce' => wp_create_nonce('wp_rest'),
    // 可以添加一些其他的设置或翻译文本到这里,方便 JS 使用
    // 'loading_message' => __('Generating content, please wait...', 'ai-blog-generator'),
    // 'error_message_prefix' => __('Error: ', 'ai-blog-generator'),
  ));
}

操作步骤:

  1. 修改 openai-blog-generator.phpwp_localize_script 的第三个参数为 'ai_blog_generator_params'
  2. 确保 js/ai-blog-generator.js 文件中使用的是 ai_blog_generator_params.apiUrlai_blog_generator_params.nonce。检查一下原 JS 文件,它确实用的就是 ai_blog_generator_params,所以只需要改 PHP 这边就行。

方案四:精简代码,避免重复

如果 openai-blog-generator-rest-api.php 文件只是 openai-blog-generator.php 中 REST API 相关代码的一个面向对象版本,你需要二选一。

原理与作用:

  • 保持代码库整洁,避免潜在冲突,方便维护。

操作步骤:

  1. 决定保留哪个实现:
    • 如果喜欢过程式,就在 openai-blog-generator.php 中保留 add_action('rest_api_init', ...)ai_blog_generator_generate_blog 函数。
    • 如果觉得面向对象更清晰,就在 openai-blog-generator.php 中移除 REST API 相关代码,然后确保 ai-blog-generator-rest-api.php 文件被正确地 requireinclude 一次。同时,可能需要调整 AI_Blog_Generator_REST_API 类,使其能访问到 API Key(可以通过构造函数注入,或者直接在类的方法里 get_option)。
  2. 移除或注释掉多余的代码: 彻底删除不再使用的文件或代码块。

示例 (假设保留 openai-blog-generator.php 中的实现):

  • 确保 openai-blog-generator.php 文件里没有 require 'ai-blog-generator-rest-api.php'; 这样的语句。
  • 可以安全地删除 ai-blog-generator-rest-api.php 这个文件(或者保留作为参考但不要让 WordPress 加载它)。

示例 (假设使用 ai-blog-generator-rest-api.php 中的类):

  1. openai-blog-generator.php 文件的开头或合适位置加入:
    require_once plugin_dir_path(__FILE__) . 'ai-blog-generator-rest-api.php';
    new AI_Blog_Generator_REST_API(); // 实例化类来注册路由
    
  2. openai-blog-generator.php 文件中,删除 add_action('rest_api_init', 'ai_blog_generator_register_endpoint');function ai_blog_generator_generate_blog($request) { ... } 这两块代码。
  3. 确保 AI_Blog_Generator_REST_API 类里的 generate_blog 方法应用了方案一和方案二的修复(正确的 endpoint, messages 结构, 从 get_option 获取 API key)。

(额外) 方案五:改进前端交互逻辑

虽然当前问题主要是后端 API 调用,但前端的交互方式也值得看看。ai-blog-generator.js 监听了 #generate-blog 元素的 submit 事件。这通常意味着这个 ID 应该属于一个 <form> 元素。然而,你正在用 Contact Form 7 (CF7),它本身就有自己的表单和提交机制(通常也是 AJAX)。直接劫持 CF7 表单的 submit 事件可能不是最好的做法,也可能拿不到正确的表单数据。

更好的方法可能是:

  • 在 CF7 表单 外部(或者在表单内部,但类型是 type="button" 而不是 submit)放置一个独立的“生成博客”按钮,给它 ID,比如 id="trigger-ai-generate"
  • 让你的 JavaScript 监听这个按钮的 click 事件。
  • 当按钮被点击时,用 jQuery 从 CF7 表单的各个字段里读取当前的输入值。CF7 表单字段通常有 name 属性,你可以用类似 $('input[name="your-cf7-field-name"]').val() 的方式获取。你需要知道你的 CF7 表单里,主题、风格、大纲对应的字段名是什么。
  • 拿到这些值后,构造一个数据对象,然后发起你的 AJAX 请求到 ai-blog-generator/v1/generate-blog 端点。

代码示例 (概念性 JS):

jQuery(document).ready(function($) {
  // 假设你的 CF7 表单字段 name 分别是 blog-topic, writing-style, outline
  // 并且你有一个按钮 <button id="trigger-ai-generate">生成博客</button>

  $('#trigger-ai-generate').on('click', function(event) {
    event.preventDefault(); // 阻止按钮默认行为(如果有的话)
    console.log('Generate button clicked');

    // 从 CF7 表单字段获取数据 (注意: 选择器需要根据你的 CF7 表单结构调整)
    // 假设 CF7 表单在一个 ID 为 'wpcf7-f10-pXXX-o1' 之类的容器里
    var cf7FormContainer = $('.wpcf7 form').first(); // 或者用更精确的 ID 选择器
    var topic = cf7FormContainer.find('input[name="blog_topic"]').val(); // 使用你在CF7里设置的字段名称
    var style = cf7FormContainer.find('select[name="writing_style"]').val(); // 如果是下拉选择
    var outline = cf7FormContainer.find('textarea[name="outline"]').val(); // 如果是大纲是文本域

    // 校验数据是否为空等
    if (!topic || !style || !outline) {
        alert('请填写所有必需的字段!');
        return;
    }

    console.log('Form data collected:', { blog_topic: topic, writing_style: style, outline: outline });

    // 准备发送给 REST API 的数据
    var requestData = {
        blog_topic: topic,
        writing_style: style,
        outline: outline
        // 不需要 $(this).serialize() 了,因为我们是手动收集数据
    };

    // 显示加载状态
    $('#blog-post').html('正在生成内容,请稍候...'); // 假设 #blog-post 是显示结果的区域

    // 发送 AJAX 请求
    $.ajax({
      url: ai_blog_generator_params.apiUrl, // 使用修正后的本地化变量
      type: 'POST',
      data: requestData, // 发送我们构造的数据对象
      beforeSend: function(xhr) {
        xhr.setRequestHeader('X-WP-Nonce', ai_blog_generator_params.nonce); // Nonce 验证
      },
      success: function(response) {
        console.log('API response:', response);
        if (response.text) {
          console.log('Generated text received.');
          // 把返回的文本显示出来,可能需要处理换行符,比如替换 \n 为 <br>
          var formattedText = response.text.replace(/\n/g, '<br>');
          $('#blog-post').html(formattedText);
        } else {
          // API 可能成功返回了 200 OK 但业务逻辑出错了,比如 parse error
          var errorMessage = response.message || '返回的数据格式不正确。'; // 尝试获取后端传来的错误消息
          console.log('Error: Response structure incorrect or empty text.', response);
          $('#blog-post').html('错误:' + errorMessage);
        }
      },
      error: function(jqXHR, textStatus, errorThrown) {
        console.log('AJAX error:', jqXHR.responseText);
        var errorMessage = '请求失败。';
        // 尝试解析 WP_Error 返回的 JSON
        try {
            var errorResponse = JSON.parse(jqXHR.responseText);
            if (errorResponse.message) {
                errorMessage = errorResponse.message;
            } else if (jqXHR.responseText) { // 如果不是标准 WP_Error JSON
                 errorMessage = jqXHR.responseText.substring(0, 100); // 显示部分原始错误
            }
        } catch(e) {
             // 解析失败,显示通用错误
        }
        $('#blog-post').html('错误:' + errorMessage);
      }
    });
  });
});

把上面这些点都检查并修正一遍,特别是前四个,应该就能解决掉那个恼人的 text: null 问题了。编码过程中碰到问题是很正常的,关键在于定位问题、分析原因、然后找到正确的解决办法。多利用 error_log 和浏览器的开发者工具,它们是排查问题的好帮手。