WordPress插件:快速解决OpenAI API返回null的5大坑
2025-03-30 02:55:16
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.php
和 ai-blog-generator-rest-api.php
(看起来后者是前者的一个面向对象重构或补充,存在功能重叠),我们可以揪出几个最可疑的地方:
- OpenAI API Endpoint 地址不对: 代码里定义了
OPENAI_API_URL
为https://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/completions
或https://api.openai.com/v1/chat/completions
,而不是/engines/...
。/engines
路径通常是用来列出可用模型的。 - API 请求结构可能不适配: 即使用了正确的
davinci-003
模型,它也属于旧的 Completions API。现在 OpenAI 主推的是 Chat Completions API(例如使用gpt-3.5-turbo
或gpt-4
模型),这个 API 的请求体结构要求使用messages
数组,而不是简单的prompt
字符串。如果目标 API endpoint 指向的是 Chat Completions,但发送的还是旧格式的prompt
数据,API 肯定会一脸懵,给不出正确的文本结果。 - 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。 - JavaScript 本地化变量名不一致:
wp_localize_script
函数的第三个参数是 PHP 数组,它会被转换成 JavaScript 对象。代码里用的是aiBlogGeneratorSettings
作为这个 JS 对象的名字:wp_localize_script('ai-blog-generator', 'aiBlogGeneratorSettings', ...)
。但是在ai-blog-generator.js
文件里,AJAX 调用时却用了ai_blog_generator_params.endpoint
和ai_blog_generator_params.nonce
。这个aiBlogGeneratorSettings
和ai_blog_generator_params
不匹配,导致 JavaScript 根本拿不到正确的 API 地址和 Nonce 值,AJAX 请求自然会失败。 - 代码冗余和潜在冲突:
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_p
、frequency_penalty
和presence_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'),
));
}
操作步骤:
- 修改
openai-blog-generator.php
中wp_localize_script
的第三个参数为'ai_blog_generator_params'
。 - 确保
js/ai-blog-generator.js
文件中使用的是ai_blog_generator_params.apiUrl
和ai_blog_generator_params.nonce
。检查一下原 JS 文件,它确实用的就是ai_blog_generator_params
,所以只需要改 PHP 这边就行。
方案四:精简代码,避免重复
如果 openai-blog-generator-rest-api.php
文件只是 openai-blog-generator.php
中 REST API 相关代码的一个面向对象版本,你需要二选一。
原理与作用:
- 保持代码库整洁,避免潜在冲突,方便维护。
操作步骤:
- 决定保留哪个实现:
- 如果喜欢过程式,就在
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
文件被正确地require
或include
一次。同时,可能需要调整AI_Blog_Generator_REST_API
类,使其能访问到 API Key(可以通过构造函数注入,或者直接在类的方法里get_option
)。
- 如果喜欢过程式,就在
- 移除或注释掉多余的代码: 彻底删除不再使用的文件或代码块。
示例 (假设保留 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
中的类):
- 在
openai-blog-generator.php
文件的开头或合适位置加入:require_once plugin_dir_path(__FILE__) . 'ai-blog-generator-rest-api.php'; new AI_Blog_Generator_REST_API(); // 实例化类来注册路由
- 在
openai-blog-generator.php
文件中,删除add_action('rest_api_init', 'ai_blog_generator_register_endpoint');
和function ai_blog_generator_generate_blog($request) { ... }
这两块代码。 - 确保
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
和浏览器的开发者工具,它们是排查问题的好帮手。