96 lines
3.3 KiB
PHP
96 lines
3.3 KiB
PHP
|
|
<?php
|
|||
|
|
|
|||
|
|
namespace App\Service\common\ai;
|
|||
|
|
|
|||
|
|
use App\BaseApp\BaseService;
|
|||
|
|
use GuzzleHttp\Client;
|
|||
|
|
use GuzzleHttp\Exception\GuzzleException;
|
|||
|
|
use Throwable;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* DeepSeek Agent(OpenAI 兼容协议)
|
|||
|
|
* 每次调用从 AiRuntimeConfigService 取最新平台/密钥/模型
|
|||
|
|
*/
|
|||
|
|
class DeepSeekAiAgent extends BaseService implements AiAgentInterface
|
|||
|
|
{
|
|||
|
|
public function __construct()
|
|||
|
|
{
|
|||
|
|
// Agent 本身不读用户态,避免工厂链路强制鉴权
|
|||
|
|
$this->isAuth = false;
|
|||
|
|
parent::__construct();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public function provider(): string
|
|||
|
|
{
|
|||
|
|
return 'deepseek';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 调用 DeepSeek chat completions
|
|||
|
|
*/
|
|||
|
|
public function chatCompletions(array $messages, array $options = []): AiChatResult
|
|||
|
|
{
|
|||
|
|
$cfg = AiRuntimeConfigService::getInstance()->resolve($this->provider());
|
|||
|
|
$apiUrl = (string) ($cfg['api_url'] ?? '');
|
|||
|
|
$apiKey = (string) ($cfg['api_key'] ?? '');
|
|||
|
|
$defaultModel = (string) ($cfg['model'] ?? 'deepseek-chat');
|
|||
|
|
$timeout = (float) ($cfg['timeout'] ?? 60);
|
|||
|
|
if ($apiKey === '') {
|
|||
|
|
$this->utils->errorThrow('DeepSeek 暂未开通(请在「AI密钥管理」或 .env DEEPSEEK_API_KEY 配置)');
|
|||
|
|
}
|
|||
|
|
if ($apiUrl === '') {
|
|||
|
|
$this->utils->errorThrow('未配置 DeepSeek 接口地址');
|
|||
|
|
}
|
|||
|
|
if (empty($messages)) {
|
|||
|
|
$this->utils->errorThrow('AI 对话消息不能为空');
|
|||
|
|
}
|
|||
|
|
$model = (string) ($options['model'] ?? $defaultModel);
|
|||
|
|
$body = [
|
|||
|
|
'model' => $model,
|
|||
|
|
'messages' => array_values($messages),
|
|||
|
|
'stream' => false,
|
|||
|
|
];
|
|||
|
|
foreach (['temperature', 'max_tokens', 'top_p'] as $key) {
|
|||
|
|
if (array_key_exists($key, $options)) {
|
|||
|
|
$body[$key] = $options[$key];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
try {
|
|||
|
|
$client = new Client([
|
|||
|
|
'timeout' => $timeout,
|
|||
|
|
'connect_timeout' => min(10.0, $timeout),
|
|||
|
|
]);
|
|||
|
|
$response = $client->request('POST', $apiUrl, [
|
|||
|
|
'headers' => [
|
|||
|
|
'Authorization' => 'Bearer ' . $apiKey,
|
|||
|
|
'Content-Type' => 'application/json',
|
|||
|
|
],
|
|||
|
|
'json' => $body,
|
|||
|
|
]);
|
|||
|
|
$raw = json_decode((string) $response->getBody(), true);
|
|||
|
|
if (!is_array($raw)) {
|
|||
|
|
$this->utils->errorThrow('DeepSeek 返回非 JSON');
|
|||
|
|
}
|
|||
|
|
$content = (string) data_get($raw, 'choices.0.message.content', '');
|
|||
|
|
if ($content === '') {
|
|||
|
|
$errMsg = (string) ($raw['error']['message'] ?? '');
|
|||
|
|
$this->utils->errorThrow($errMsg !== '' ? ('DeepSeek 调用失败:' . $errMsg) : 'DeepSeek 未返回有效内容');
|
|||
|
|
}
|
|||
|
|
$usage = is_array($raw['usage'] ?? null) ? $raw['usage'] : [];
|
|||
|
|
return new AiChatResult(
|
|||
|
|
$content,
|
|||
|
|
$raw,
|
|||
|
|
$this->provider(),
|
|||
|
|
$model,
|
|||
|
|
$usage,
|
|||
|
|
(int) ($cfg['api_key_id'] ?? 0)
|
|||
|
|
);
|
|||
|
|
} catch (Throwable $e) {
|
|||
|
|
if ($e instanceof GuzzleException) {
|
|||
|
|
$this->utils->errorThrow('DeepSeek 请求失败:' . $e->getMessage());
|
|||
|
|
}
|
|||
|
|
throw $e;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|