no message
This commit is contained in:
@@ -0,0 +1,299 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service\ai;
|
||||
|
||||
use app\common\model\ai\AiConfig;
|
||||
|
||||
class DeepSeekClient
|
||||
{
|
||||
private const GPT_PROXY_API_KEY = 'sk-fbbad0b884aa6bd16da234723c1f1a4a9e7cc97c23d1ab8137a00dc4d9fa0ec2';
|
||||
private const GPT_PROXY_BASE_URL = 'https://sub2.congmingai.com';
|
||||
private const GPT_PROXY_MODEL = 'gpt-5.5';
|
||||
private const GPT_PROXY_WIRE_API = 'responses';
|
||||
|
||||
protected string $apiKey;
|
||||
protected string $baseUrl;
|
||||
protected string $model;
|
||||
protected string $providerLabel;
|
||||
protected string $wireApi;
|
||||
protected int $maxTokens;
|
||||
protected float $temperature;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->apiKey = self::GPT_PROXY_API_KEY;
|
||||
$this->baseUrl = self::GPT_PROXY_BASE_URL;
|
||||
$this->model = self::GPT_PROXY_MODEL;
|
||||
$this->wireApi = self::GPT_PROXY_WIRE_API;
|
||||
$this->providerLabel = 'OpenAI(GPT中转)';
|
||||
$this->maxTokens = (int) AiConfig::getVal('max_tokens', '2000');
|
||||
$this->temperature = (float) AiConfig::getVal('temperature', '0.7');
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送聊天请求
|
||||
*/
|
||||
public function chat(string $systemPrompt, string $userMessage, array $options = []): array
|
||||
{
|
||||
$messages = [
|
||||
['role' => 'system', 'content' => $systemPrompt],
|
||||
['role' => 'user', 'content' => $userMessage],
|
||||
];
|
||||
|
||||
return $this->request($messages, $options);
|
||||
}
|
||||
|
||||
public function chatMessages(array $messages, array $options = []): array
|
||||
{
|
||||
if (empty($messages)) {
|
||||
return ['success' => false, 'error' => 'messages不能为空', 'tokens' => 0];
|
||||
}
|
||||
|
||||
return $this->request($messages, $options);
|
||||
}
|
||||
|
||||
public function chatWithImages(string $systemPrompt, string $userMessage, array $imageUrls, array $options = []): array
|
||||
{
|
||||
$content = [
|
||||
['type' => 'text', 'text' => $userMessage],
|
||||
];
|
||||
|
||||
foreach ($imageUrls as $imageUrl) {
|
||||
if ($imageUrl === '') {
|
||||
continue;
|
||||
}
|
||||
$content[] = [
|
||||
'type' => 'image_url',
|
||||
'image_url' => ['url' => $imageUrl],
|
||||
];
|
||||
}
|
||||
|
||||
$messages = [
|
||||
['role' => 'system', 'content' => $systemPrompt],
|
||||
['role' => 'user', 'content' => $content],
|
||||
];
|
||||
|
||||
return $this->request($messages, $options);
|
||||
}
|
||||
|
||||
protected function request(array $messages, array $options = []): array
|
||||
{
|
||||
$apiKey = $options['api_key'] ?? $this->apiKey;
|
||||
$providerLabel = $options['provider_label'] ?? $this->providerLabel;
|
||||
if (empty($apiKey)) {
|
||||
return ['success' => false, 'error' => $providerLabel . ' API Key未配置', 'tokens' => 0, 'model' => $options['model'] ?? $this->model, 'provider_label' => $providerLabel];
|
||||
}
|
||||
|
||||
$startTime = microtime(true);
|
||||
$baseUrl = rtrim($options['base_url'] ?? $this->baseUrl, '/');
|
||||
$wireApi = strtolower((string) ($options['wire_api'] ?? $this->wireApi));
|
||||
$payload = $wireApi === 'responses'
|
||||
? $this->buildResponsesPayload($messages, $options)
|
||||
: $this->buildChatPayload($messages, $options);
|
||||
$endpoint = $wireApi === 'responses'
|
||||
? $this->buildResponsesEndpoint($baseUrl)
|
||||
: $this->buildChatEndpoint($baseUrl);
|
||||
$timeout = $this->resolveTimeout($options['timeout'] ?? null);
|
||||
|
||||
$ch = curl_init($endpoint);
|
||||
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_CONNECTTIMEOUT => min(15, $timeout),
|
||||
CURLOPT_TIMEOUT => $timeout,
|
||||
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' => 'cURL错误: ' . $error, 'tokens' => 0, 'cost_ms' => $costMs, 'model' => $payload['model'], 'provider_label' => $providerLabel];
|
||||
}
|
||||
|
||||
$data = json_decode($response, true);
|
||||
|
||||
$content = $wireApi === 'responses' ? $this->extractResponsesContent($data) : ($data['choices'][0]['message']['content'] ?? '');
|
||||
if ($httpCode !== 200 || $content === '') {
|
||||
$errMsg = $data['error']['message'] ?? ('HTTP ' . $httpCode);
|
||||
return ['success' => false, 'error' => $providerLabel . ' 返回异常: ' . $errMsg, 'tokens' => 0, 'cost_ms' => $costMs, 'model' => $payload['model'], 'provider_label' => $providerLabel];
|
||||
}
|
||||
|
||||
$tokens = $data['usage']['total_tokens'] ?? (($data['usage']['input_tokens'] ?? 0) + ($data['usage']['output_tokens'] ?? 0));
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'content' => $content,
|
||||
'tokens' => $tokens,
|
||||
'cost_ms' => $costMs,
|
||||
'model' => $payload['model'],
|
||||
'provider_label' => $providerLabel,
|
||||
];
|
||||
}
|
||||
|
||||
protected function buildChatPayload(array $messages, array $options): array
|
||||
{
|
||||
return [
|
||||
'model' => $options['model'] ?? $this->model,
|
||||
'messages' => $messages,
|
||||
'max_tokens' => $options['max_tokens'] ?? $this->maxTokens,
|
||||
'temperature' => $options['temperature'] ?? $this->temperature,
|
||||
];
|
||||
}
|
||||
|
||||
protected function buildResponsesPayload(array $messages, array $options): array
|
||||
{
|
||||
$instructions = [];
|
||||
$input = [];
|
||||
foreach ($messages as $message) {
|
||||
$role = (string) ($message['role'] ?? 'user');
|
||||
$content = $message['content'] ?? '';
|
||||
if ($role === 'system') {
|
||||
$text = $this->contentToText($content);
|
||||
if ($text !== '') {
|
||||
$instructions[] = $text;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
$input[] = [
|
||||
'role' => $role === 'assistant' ? 'assistant' : 'user',
|
||||
'content' => $this->convertResponsesContent($content),
|
||||
];
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'model' => $options['model'] ?? $this->model,
|
||||
'input' => $input,
|
||||
'max_output_tokens' => $options['max_tokens'] ?? $this->maxTokens,
|
||||
'temperature' => $options['temperature'] ?? $this->temperature,
|
||||
];
|
||||
if (!empty($instructions)) {
|
||||
$payload['instructions'] = implode("\n\n", $instructions);
|
||||
}
|
||||
return $payload;
|
||||
}
|
||||
|
||||
protected function convertResponsesContent($content): array
|
||||
{
|
||||
if (is_string($content)) {
|
||||
return [['type' => 'input_text', 'text' => $content]];
|
||||
}
|
||||
|
||||
$rows = [];
|
||||
foreach ((array) $content as $item) {
|
||||
$type = (string) ($item['type'] ?? '');
|
||||
if ($type === 'text') {
|
||||
$rows[] = ['type' => 'input_text', 'text' => (string) ($item['text'] ?? '')];
|
||||
} elseif ($type === 'image_url') {
|
||||
$imageUrl = $item['image_url']['url'] ?? '';
|
||||
if ($imageUrl !== '') {
|
||||
$rows[] = ['type' => 'input_image', 'image_url' => (string) $imageUrl];
|
||||
}
|
||||
}
|
||||
}
|
||||
return $rows ?: [['type' => 'input_text', 'text' => '']];
|
||||
}
|
||||
|
||||
protected function contentToText($content): string
|
||||
{
|
||||
if (is_string($content)) {
|
||||
return trim($content);
|
||||
}
|
||||
|
||||
$texts = [];
|
||||
foreach ((array) $content as $item) {
|
||||
if (($item['type'] ?? '') === 'text') {
|
||||
$texts[] = (string) ($item['text'] ?? '');
|
||||
}
|
||||
}
|
||||
return trim(implode("\n", $texts));
|
||||
}
|
||||
|
||||
protected function extractResponsesContent(array $data): string
|
||||
{
|
||||
if (!empty($data['output_text'])) {
|
||||
return (string) $data['output_text'];
|
||||
}
|
||||
|
||||
$texts = [];
|
||||
foreach (($data['output'] ?? []) as $output) {
|
||||
foreach (($output['content'] ?? []) as $content) {
|
||||
if (isset($content['text'])) {
|
||||
$texts[] = (string) $content['text'];
|
||||
}
|
||||
}
|
||||
}
|
||||
return trim(implode("\n", $texts));
|
||||
}
|
||||
|
||||
protected function buildChatEndpoint(string $baseUrl): string
|
||||
{
|
||||
if (str_ends_with($baseUrl, '/chat/completions')) {
|
||||
return $baseUrl;
|
||||
}
|
||||
|
||||
if (str_ends_with($baseUrl, '/v1')) {
|
||||
return $baseUrl . '/chat/completions';
|
||||
}
|
||||
|
||||
return $baseUrl . '/v1/chat/completions';
|
||||
}
|
||||
|
||||
protected function buildResponsesEndpoint(string $baseUrl): string
|
||||
{
|
||||
if (str_ends_with($baseUrl, '/responses')) {
|
||||
return $baseUrl;
|
||||
}
|
||||
|
||||
if (str_ends_with($baseUrl, '/v1')) {
|
||||
return $baseUrl . '/responses';
|
||||
}
|
||||
|
||||
return $baseUrl . '/v1/responses';
|
||||
}
|
||||
|
||||
protected function resolveTimeout($timeout): int
|
||||
{
|
||||
$value = (int) ($timeout ?: AiConfig::getVal('ai_request_timeout', '180'));
|
||||
return max(30, min($value, 300));
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送请求并解析JSON结果
|
||||
*/
|
||||
public function chatJson(string $systemPrompt, string $userMessage, array $options = []): array
|
||||
{
|
||||
$result = $this->chat($systemPrompt, $userMessage, $options);
|
||||
if (!$result['success']) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$content = $result['content'];
|
||||
// 尝试提取JSON块
|
||||
if (preg_match('/```json\s*(.*?)\s*```/s', $content, $m)) {
|
||||
$content = $m[1];
|
||||
} elseif (preg_match('/\{.*\}/s', $content, $m)) {
|
||||
$content = $m[0];
|
||||
}
|
||||
|
||||
$parsed = json_decode($content, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
$result['parsed'] = null;
|
||||
$result['raw_content'] = $result['content'];
|
||||
} else {
|
||||
$result['parsed'] = $parsed;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user