112 lines
3.7 KiB
PHP
112 lines
3.7 KiB
PHP
<?php
|
|
|
|
namespace app\common\service\ai;
|
|
|
|
use app\common\model\ai\AiConfig;
|
|
|
|
class EmbeddingClient
|
|
{
|
|
private const DASHSCOPE_BASE_URL = 'https://dashscope.aliyuncs.com/compatible-mode';
|
|
private const DASHSCOPE_EMBEDDING_MODEL = 'text-embedding-v4';
|
|
|
|
protected string $apiKey;
|
|
protected string $baseUrl;
|
|
protected string $model;
|
|
protected int $defaultDim;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->apiKey = (string) (AiConfig::getVal('embedding_api_key', '') ?: env('DASHSCOPE_API_KEY', ''));
|
|
$this->baseUrl = (string) (AiConfig::getVal('embedding_base_url', '') ?: self::DASHSCOPE_BASE_URL);
|
|
$this->model = (string) (AiConfig::getVal('embedding_model', '') ?: self::DASHSCOPE_EMBEDDING_MODEL);
|
|
$this->defaultDim = (int) (AiConfig::getVal('embedding_dim', '') ?: '1024');
|
|
}
|
|
|
|
/**
|
|
* @param string|array<int, string> $input
|
|
*/
|
|
public function embed($input, array $options = []): array
|
|
{
|
|
$apiKey = $options['api_key'] ?? $this->apiKey;
|
|
$baseUrl = rtrim($options['base_url'] ?? $this->baseUrl, '/');
|
|
$model = $options['model'] ?? $this->model;
|
|
|
|
if ($apiKey === '' || $baseUrl === '' || $model === '') {
|
|
return [
|
|
'success' => false,
|
|
'error' => 'Embedding API配置未完成',
|
|
'embeddings' => [],
|
|
'cost_ms' => 0,
|
|
];
|
|
}
|
|
|
|
$payload = [
|
|
'model' => $model,
|
|
'input' => $input,
|
|
];
|
|
|
|
if (!empty($options['dimensions'])) {
|
|
$payload['dimensions'] = (int) $options['dimensions'];
|
|
} elseif ($this->defaultDim > 0) {
|
|
$payload['dimensions'] = $this->defaultDim;
|
|
}
|
|
|
|
$startTime = microtime(true);
|
|
$ch = curl_init($baseUrl . '/v1/embeddings');
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_POST => true,
|
|
CURLOPT_HTTPHEADER => [
|
|
'Content-Type: application/json',
|
|
'Authorization: Bearer ' . $apiKey,
|
|
],
|
|
CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE),
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_TIMEOUT => 60,
|
|
CURLOPT_SSL_VERIFYPEER => false,
|
|
]);
|
|
|
|
$response = curl_exec($ch);
|
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
$error = curl_error($ch);
|
|
curl_close($ch);
|
|
|
|
$costMs = (int) ((microtime(true) - $startTime) * 1000);
|
|
if ($error) {
|
|
return [
|
|
'success' => false,
|
|
'error' => 'Embedding cURL错误: ' . $error,
|
|
'embeddings' => [],
|
|
'cost_ms' => $costMs,
|
|
];
|
|
}
|
|
|
|
$data = json_decode((string) $response, true);
|
|
if ($httpCode !== 200 || empty($data['data']) || !is_array($data['data'])) {
|
|
return [
|
|
'success' => false,
|
|
'error' => $data['error']['message'] ?? ('HTTP ' . $httpCode),
|
|
'embeddings' => [],
|
|
'cost_ms' => $costMs,
|
|
];
|
|
}
|
|
|
|
$embeddings = [];
|
|
foreach ($data['data'] as $row) {
|
|
$embedding = $row['embedding'] ?? [];
|
|
if (is_array($embedding) && !empty($embedding)) {
|
|
$embeddings[] = array_map('floatval', $embedding);
|
|
}
|
|
}
|
|
|
|
return [
|
|
'success' => !empty($embeddings),
|
|
'error' => !empty($embeddings) ? '' : 'Embedding结果为空',
|
|
'embeddings' => $embeddings,
|
|
'model' => $model,
|
|
'dimension' => count($embeddings[0] ?? []),
|
|
'tokens' => (int) ($data['usage']['total_tokens'] ?? 0),
|
|
'cost_ms' => $costMs,
|
|
];
|
|
}
|
|
}
|