300 lines
10 KiB
PHP
300 lines
10 KiB
PHP
<?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;
|
|
}
|
|
}
|