Files
sbnews/server/app/common/service/ai/DeepSeekClient.php
T
2026-06-11 12:15:29 +08:00

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;
}
}