$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, ]; } }