wpmind/includes/API/Services/EmbeddingService.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

131 lines
3.6 KiB
PHP

<?php
/**
* Embedding Service
*
* 处理文本嵌入向量
*
* @package WPMind
* @subpackage API\Services
* @since 3.7.0
*/
declare(strict_types=1);
namespace WPMind\API\Services;
use WP_Error;
/**
* Embedding Service
*
* @since 3.7.0
*/
class EmbeddingService extends AbstractService {
/**
* 嵌入模型映射表
*/
private const EMBED_MODELS = [
'openai' => 'text-embedding-3-small',
'deepseek' => 'text-embedding-3-small',
'zhipu' => 'embedding-2',
'qwen' => 'text-embedding-v2',
];
/**
* 文本嵌入向量
*
* @since 2.6.0
* @param string|array $texts 要嵌入的文本
* @param array $options 选项
* @return array|WP_Error
*/
public function embed($texts, array $options = []) {
$defaults = [
'context' => 'embedding',
'model' => 'auto',
'provider' => 'auto',
];
$options = wp_parse_args($options, $defaults);
$context = $options['context'];
$input_texts = is_array($texts) ? $texts : [$texts];
$original_model = $options['model'];
$model_is_auto = ($original_model === 'auto');
$provider = $this->resolve_provider($options['provider'], $context);
do_action('wpmind_before_request', 'embed', compact('texts', 'options'), $context);
return $this->execute_with_failover('embed', $provider, $context, function (string $try_provider, array $endpoint) use ($input_texts, $model_is_auto, $original_model, $texts, $options) {
$embed_model = $model_is_auto
? (self::EMBED_MODELS[$try_provider] ?? 'text-embedding-3-small')
: $original_model;
$base_url = $endpoint['custom_base_url'] ?? $endpoint['base_url'] ?? '';
$api_url = trailingslashit($base_url) . 'embeddings';
$api_key = $endpoint['api_key'];
$start_time = microtime(true);
$response = wp_remote_post($api_url, [
'headers' => [
'Authorization' => 'Bearer ' . $api_key,
'Content-Type' => 'application/json',
],
'body' => wp_json_encode([
'model' => $embed_model,
'input' => $input_texts,
]),
'timeout' => 60,
]);
$latency_ms = (int)((microtime(true) - $start_time) * 1000);
if (is_wp_error($response)) {
$this->record_result($try_provider, false, $latency_ms);
return new WP_Error('wpmind_embed_failed',
sprintf(__('嵌入请求失败: %s', 'wpmind'), $response->get_error_message()));
}
$status_code = wp_remote_retrieve_response_code($response);
$body = wp_remote_retrieve_body($response);
$data = json_decode($body, true);
if ($status_code !== 200) {
$this->record_result($try_provider, false, $latency_ms);
$error_message = is_array($data) ? ($data['error']['message'] ?? __('未知错误', 'wpmind')) : __('未知错误', 'wpmind');
return new WP_Error('wpmind_embed_error',
sprintf(__('嵌入 API 错误 (%d): %s', 'wpmind'), $status_code, $error_message));
}
if (!is_array($data)) {
$this->record_result($try_provider, false, $latency_ms);
return new WP_Error('wpmind_invalid_response', __('嵌入 API 返回了无效的响应格式', 'wpmind'));
}
$this->record_result($try_provider, true, $latency_ms);
$embeddings = [];
foreach ($data['data'] ?? [] as $item) {
$embeddings[] = $item['embedding'];
}
$usage = [
'prompt_tokens' => $data['usage']['prompt_tokens'] ?? 0,
'total_tokens' => $data['usage']['total_tokens'] ?? 0,
];
do_action('wpmind_after_request', 'embed', $embeddings, compact('texts', 'options'), $usage);
return [
'embeddings' => $embeddings,
'model' => $embed_model,
'provider' => $try_provider,
'usage' => $usage,
'dimensions' => !empty($embeddings[0]) ? count($embeddings[0]) : 0,
];
});
}
}