安全修复(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>
131 lines
3.6 KiB
PHP
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,
|
|
];
|
|
});
|
|
}
|
|
}
|