wpmind/includes/API/ErrorHandler.php
LinuxJoy 6c36d563e4 security: Codex 审计问题修复 + WordPress 插件开发指导文档
安全修复(Codex 审计 11 项):
- transcribe() SSRF 防护: wp_http_validate_url + 协议白名单
- transcribe() 本地文件读取限制: realpath + uploads 目录校验
- transcribe() 文件大小上限 25MB + 扩展名白名单
- ErrorHandler 响应体截断到 500 字符,防止信息泄露
- SDKAdapter getTokenUsage() 空值保护
- SDKAdapter 异常消息脱敏,仅 WP_DEBUG 记录详情
- stream() 添加 allow_url_fopen 检测
- embed() JSON 解码显式校验
- speech() file_put_contents 返回值检查

新增文档:
- WordPress 插件开发指导手册 v1.1.0 (1225 行)
- 覆盖项目准备、编码规范、架构设计、开发流程等 10 章
- 基于 Codex 审计反馈补充安全基线和 WP 生态兼容性

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-07 12:41:21 +08:00

311 lines
9.7 KiB
PHP

<?php
/**
* WPMind 错误处理类
*
* @package WPMind
* @subpackage API
* @since 2.5.0
*/
declare(strict_types=1);
namespace WPMind\API;
use WP_Error;
/**
* 统一错误处理类
*
* 提供标准化的错误创建和处理机制
*
* @since 2.5.0
*/
class ErrorHandler {
/**
* 错误代码常量
*/
// 通用错误
const ERROR_NOT_AVAILABLE = 'wpmind_not_available';
const ERROR_INVALID_PARAMS = 'wpmind_invalid_params';
const ERROR_EMPTY_INPUT = 'wpmind_empty_input';
// 调用限制错误
const ERROR_RECURSIVE_CALL = 'wpmind_recursive_call';
const ERROR_CALL_DEPTH_EXCEEDED = 'wpmind_call_depth_exceeded';
const ERROR_RATE_LIMITED = 'wpmind_rate_limited';
const ERROR_BUDGET_EXCEEDED = 'wpmind_budget_exceeded';
// API 错误
const ERROR_API_ERROR = 'wpmind_api_error';
const ERROR_API_TIMEOUT = 'wpmind_api_timeout';
const ERROR_API_AUTH = 'wpmind_api_auth';
const ERROR_API_QUOTA = 'wpmind_api_quota';
// 服务商错误
const ERROR_PROVIDER_NOT_FOUND = 'wpmind_provider_not_found';
const ERROR_PROVIDER_NOT_CONFIGURED = 'wpmind_provider_not_configured';
const ERROR_MODEL_NOT_SUPPORTED = 'wpmind_model_not_supported';
/**
* 错误消息映射
*
* @var array
*/
private static $error_messages = [
self::ERROR_NOT_AVAILABLE => 'WPMind 插件未激活或未配置',
self::ERROR_INVALID_PARAMS => '无效的参数',
self::ERROR_EMPTY_INPUT => '输入内容为空',
self::ERROR_RECURSIVE_CALL => '检测到循环调用',
self::ERROR_CALL_DEPTH_EXCEEDED => '调用深度超过限制',
self::ERROR_RATE_LIMITED => '请求频率过高,请稍后再试',
self::ERROR_BUDGET_EXCEEDED => '已超出预算限制',
self::ERROR_API_ERROR => 'API 调用失败',
self::ERROR_API_TIMEOUT => 'API 请求超时',
self::ERROR_API_AUTH => 'API 认证失败,请检查 API Key',
self::ERROR_API_QUOTA => 'API 配额已用尽',
self::ERROR_PROVIDER_NOT_FOUND => '找不到指定的服务商',
self::ERROR_PROVIDER_NOT_CONFIGURED => '服务商未配置',
self::ERROR_MODEL_NOT_SUPPORTED => '不支持的模型',
];
/**
* 创建标准错误对象
*
* @param string $code 错误代码(使用类常量)
* @param string $message 可选的自定义消息
* @param array $data 额外的错误数据
* @return WP_Error
*/
public static function create(string $code, string $message = '', array $data = []): WP_Error {
// 如果没有提供消息,使用默认消息
if (empty($message)) {
$message = self::$error_messages[$code] ?? __('未知错误', 'wpmind');
}
// 添加时间戳和请求 ID
$data['timestamp'] = time();
$data['request_id'] = self::generate_request_id();
// 记录错误日志
self::log_error($code, $message, $data);
return new WP_Error($code, __($message, 'wpmind'), $data);
}
/**
* 快捷方法:创建"不可用"错误
*
* @return WP_Error
*/
public static function not_available(): WP_Error {
return self::create(self::ERROR_NOT_AVAILABLE);
}
/**
* 快捷方法:创建"无效参数"错误
*
* @param string $param_name 参数名
* @param mixed $value 参数值
* @return WP_Error
*/
public static function invalid_param(string $param_name, $value = null): WP_Error {
return self::create(
self::ERROR_INVALID_PARAMS,
sprintf(__('无效的参数: %s', 'wpmind'), $param_name),
['param' => $param_name, 'value' => $value]
);
}
/**
* 快捷方法:创建"API 错误"
*
* @param string $provider API 服务商
* @param string $message 错误消息
* @param int $code HTTP 状态码
* @return WP_Error
*/
public static function api_error(string $provider, string $message, int $code = 0): WP_Error {
return self::create(
self::ERROR_API_ERROR,
$message,
['provider' => $provider, 'http_code' => $code]
);
}
/**
* 快捷方法:创建"循环调用"错误
*
* @param string $method 方法名
* @param string $call_id 调用 ID
* @return WP_Error
*/
public static function recursive_call(string $method, string $call_id): WP_Error {
return self::create(
self::ERROR_RECURSIVE_CALL,
sprintf(__('检测到循环调用: %s', 'wpmind'), $method),
['method' => $method, 'call_id' => $call_id]
);
}
/**
* 快捷方法:创建"调用深度超限"错误
*
* @param string $method 方法名
* @param int $depth 当前深度
* @param int $max_depth 最大深度
* @return WP_Error
*/
public static function call_depth_exceeded(string $method, int $depth, int $max_depth): WP_Error {
return self::create(
self::ERROR_CALL_DEPTH_EXCEEDED,
sprintf(__('调用深度超限: %s (当前 %d, 最大 %d)', 'wpmind'), $method, $depth, $max_depth),
['method' => $method, 'depth' => $depth, 'max_depth' => $max_depth]
);
}
/**
* 从 API 响应创建错误
*
* @param array $response API 响应
* @param string $provider 服务商
* @return WP_Error
*/
public static function from_api_response(array $response, string $provider): WP_Error {
$http_code = $response['response']['code'] ?? 0;
$body = $response['body'] ?? '';
// 尝试解析 JSON 错误
$error_data = json_decode($body, true);
$error_message = $error_data['error']['message']
?? $error_data['message']
?? $body
?? __('未知 API 错误', 'wpmind');
// 根据 HTTP 状态码分类错误
$code = self::ERROR_API_ERROR;
if ($http_code === 401 || $http_code === 403) {
$code = self::ERROR_API_AUTH;
} elseif ($http_code === 429) {
$code = self::ERROR_RATE_LIMITED;
} elseif ($http_code === 408 || $http_code === 504) {
$code = self::ERROR_API_TIMEOUT;
}
return self::create($code, $error_message, [
'provider' => $provider,
'http_code' => $http_code,
'response' => substr((string) $body, 0, 500),
]);
}
/**
* 检查错误是否为特定类型
*
* @param WP_Error $error 错误对象
* @param string $code 错误代码
* @return bool
*/
public static function is_error_type(WP_Error $error, string $code): bool {
return $error->get_error_code() === $code;
}
/**
* 检查错误是否可重试
*
* @param WP_Error $error 错误对象
* @return bool
*/
public static function is_retryable(WP_Error $error): bool {
$retryable_codes = [
self::ERROR_API_TIMEOUT,
self::ERROR_RATE_LIMITED,
];
return in_array($error->get_error_code(), $retryable_codes, true);
}
/**
* 生成请求 ID
*
* @return string
*/
private static function generate_request_id(): string {
return 'wpmind_' . substr(md5(uniqid(mt_rand(), true)), 0, 12);
}
/**
* 记录错误日志
*
* @param string $code 错误代码
* @param string $message 错误消息
* @param array $data 错误数据
*/
private static function log_error(string $code, string $message, array $data): void {
if (!defined('WP_DEBUG') || !WP_DEBUG) {
return;
}
$log_message = sprintf(
'[WPMind Error] %s: %s | Request ID: %s',
$code,
$message,
$data['request_id'] ?? 'unknown'
);
if (!empty($data['provider'])) {
$log_message .= ' | Provider: ' . $data['provider'];
}
if (!empty($data['http_code'])) {
$log_message .= ' | HTTP Code: ' . $data['http_code'];
}
error_log($log_message);
}
/**
* 获取所有错误代码
*
* @return array
*/
public static function get_all_error_codes(): array {
return [
self::ERROR_NOT_AVAILABLE,
self::ERROR_INVALID_PARAMS,
self::ERROR_EMPTY_INPUT,
self::ERROR_RECURSIVE_CALL,
self::ERROR_CALL_DEPTH_EXCEEDED,
self::ERROR_RATE_LIMITED,
self::ERROR_BUDGET_EXCEEDED,
self::ERROR_API_ERROR,
self::ERROR_API_TIMEOUT,
self::ERROR_API_AUTH,
self::ERROR_API_QUOTA,
self::ERROR_PROVIDER_NOT_FOUND,
self::ERROR_PROVIDER_NOT_CONFIGURED,
self::ERROR_MODEL_NOT_SUPPORTED,
];
}
/**
* 获取用户友好的错误消息
*
* @param WP_Error $error 错误对象
* @return string
*/
public static function get_user_friendly_message(WP_Error $error): string {
$code = $error->get_error_code();
// 用户友好消息映射
$user_messages = [
self::ERROR_NOT_AVAILABLE => __('AI 服务暂不可用,请稍后再试或检查插件设置。', 'wpmind'),
self::ERROR_RATE_LIMITED => __('请求太频繁,请稍后再试。', 'wpmind'),
self::ERROR_BUDGET_EXCEEDED => __('本月 AI 使用额度已用完,请联系管理员。', 'wpmind'),
self::ERROR_API_TIMEOUT => __('AI 服务响应超时,请稍后再试。', 'wpmind'),
self::ERROR_API_AUTH => __('AI 服务认证失败,请联系管理员检查配置。', 'wpmind'),
];
return $user_messages[$code] ?? __('操作失败,请稍后再试。', 'wpmind');
}
}