137 lines
4.9 KiB
PHP
137 lines
4.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace app\common\service;
|
|
|
|
/**
|
|
* Dify Chat App blocking 客户端。
|
|
*
|
|
* 只接受服务端配置中的模型 profile,避免把上游地址和密钥暴露给前端。
|
|
*/
|
|
class DifyChatService
|
|
{
|
|
/**
|
|
* @param array<string,mixed> $inputs
|
|
* @return array{ok:bool,content?:string,message_id?:string,latency_ms?:int,error_code?:string,error?:string}
|
|
*/
|
|
public static function chat(string $profile, array $inputs, string $query, string $user): array
|
|
{
|
|
$config = config('prescription_ai') ?: [];
|
|
if (empty($config['enable'])) {
|
|
return self::error('CONFIG_DISABLED', '处方 AI 解释未启用');
|
|
}
|
|
|
|
$modelConfig = $config['models'][$profile] ?? null;
|
|
if (!is_array($modelConfig)) {
|
|
return self::error('INVALID_PROFILE', '不支持的 AI 模型');
|
|
}
|
|
|
|
$baseUrl = trim((string) ($config['base_url'] ?? ''));
|
|
$apiKey = trim((string) ($modelConfig['api_key'] ?? ''));
|
|
if ($baseUrl === '' || $apiKey === '') {
|
|
return self::error('CONFIG_MISSING', '该模型尚未配置 Dify 地址或 App Key');
|
|
}
|
|
if (!function_exists('curl_init')) {
|
|
return self::error('CURL_UNAVAILABLE', '服务器尚未启用 cURL 扩展');
|
|
}
|
|
|
|
$payload = [
|
|
'inputs' => $inputs,
|
|
'query' => $query,
|
|
'response_mode' => 'blocking',
|
|
'user' => $user,
|
|
];
|
|
$body = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
|
|
if ($body === false) {
|
|
return self::error('REQUEST_BUILD_FAILED', '处方数据编码失败');
|
|
}
|
|
|
|
$timeout = max(10, min(120, (int) ($config['timeout'] ?? 90)));
|
|
$ch = curl_init();
|
|
if ($ch === false) {
|
|
return self::error('CURL_INIT_FAILED', '无法初始化 AI 请求');
|
|
}
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_URL => self::buildEndpoint($baseUrl),
|
|
CURLOPT_POST => true,
|
|
CURLOPT_POSTFIELDS => $body,
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_CONNECTTIMEOUT => min(8, max(3, (int) ceil($timeout / 4))),
|
|
CURLOPT_TIMEOUT => $timeout,
|
|
CURLOPT_SSL_VERIFYPEER => true,
|
|
CURLOPT_SSL_VERIFYHOST => 2,
|
|
CURLOPT_HTTPHEADER => [
|
|
'Content-Type: application/json',
|
|
'Accept: application/json',
|
|
'Authorization: Bearer ' . $apiKey,
|
|
],
|
|
]);
|
|
|
|
$startedAt = microtime(true);
|
|
$responseBody = curl_exec($ch);
|
|
$errno = curl_errno($ch);
|
|
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
$latencyMs = (int) round((microtime(true) - $startedAt) * 1000);
|
|
|
|
if ($errno !== 0) {
|
|
if ($errno === CURLE_OPERATION_TIMEDOUT) {
|
|
return self::error('UPSTREAM_TIMEOUT', '模型响应超时,请稍后重试', $latencyMs);
|
|
}
|
|
return self::error('UPSTREAM_UNAVAILABLE', '暂时无法连接 AI 服务,请稍后重试', $latencyMs);
|
|
}
|
|
|
|
$decoded = json_decode((string) $responseBody, true);
|
|
if ($httpCode === 401 || $httpCode === 403) {
|
|
return self::error('CONFIG_INVALID', '模型 App Key 无效或无权限', $latencyMs);
|
|
}
|
|
if ($httpCode === 429 || $httpCode >= 500) {
|
|
return self::error('UPSTREAM_BUSY', '模型服务繁忙,请稍后重试', $latencyMs);
|
|
}
|
|
if ($httpCode >= 400) {
|
|
return self::error('UPSTREAM_REJECTED', '模型未能处理本次请求', $latencyMs);
|
|
}
|
|
if (!is_array($decoded)) {
|
|
return self::error('INVALID_RESPONSE', '模型返回格式异常,请重试', $latencyMs);
|
|
}
|
|
|
|
$answer = trim((string) ($decoded['answer'] ?? ''));
|
|
if ($answer === '') {
|
|
return self::error('EMPTY_RESPONSE', '模型未返回报告内容,请重试', $latencyMs);
|
|
}
|
|
|
|
return [
|
|
'ok' => true,
|
|
'content' => $answer,
|
|
'message_id' => (string) ($decoded['message_id'] ?? ''),
|
|
'latency_ms' => $latencyMs,
|
|
];
|
|
}
|
|
|
|
private static function buildEndpoint(string $baseUrl): string
|
|
{
|
|
$baseUrl = rtrim($baseUrl, '/');
|
|
if (str_ends_with($baseUrl, '/chat-messages')) {
|
|
return $baseUrl;
|
|
}
|
|
if (str_ends_with($baseUrl, '/v1')) {
|
|
return $baseUrl . '/chat-messages';
|
|
}
|
|
return $baseUrl . '/v1/chat-messages';
|
|
}
|
|
|
|
/**
|
|
* @return array{ok:false,error_code:string,error:string,latency_ms:int}
|
|
*/
|
|
private static function error(string $code, string $message, int $latencyMs = 0): array
|
|
{
|
|
return [
|
|
'ok' => false,
|
|
'error_code' => $code,
|
|
'error' => $message,
|
|
'latency_ms' => $latencyMs,
|
|
];
|
|
}
|
|
}
|