This commit is contained in:
Your Name
2026-08-11 17:39:41 +08:00
parent cfe4c82c90
commit 25467b9d91
350 changed files with 201115 additions and 132208 deletions
@@ -0,0 +1,136 @@
<?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,
];
}
}