更新
This commit is contained in:
@@ -21,61 +21,147 @@ class DifyService
|
||||
public static function chat(AiModel $model, string $query, array $files, ?string $conversationId, string $userId): array
|
||||
{
|
||||
$url = rtrim($model->api_base_url, '/') . '/chat-messages';
|
||||
$payload = self::buildPayload($query, $files, $conversationId, $userId, false);
|
||||
$conversationReset = false;
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => json_encode($payload),
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Content-Type: application/json',
|
||||
'Authorization: Bearer ' . $model->api_key,
|
||||
],
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 120,
|
||||
CURLOPT_CONNECTTIMEOUT => 15,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
]);
|
||||
for ($attempt = 0; $attempt < 2; $attempt++) {
|
||||
$requestConversationId = $attempt === 0 ? $conversationId : null;
|
||||
$payload = self::buildPayload($query, $files, $requestConversationId, $userId, false);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$curlError = curl_error($ch);
|
||||
curl_close($ch);
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => json_encode($payload),
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Content-Type: application/json',
|
||||
'Authorization: Bearer ' . $model->api_key,
|
||||
],
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 120,
|
||||
CURLOPT_CONNECTTIMEOUT => 15,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
]);
|
||||
|
||||
if ($response === false) {
|
||||
throw new HttpResponseException(json([
|
||||
'code' => 1,
|
||||
'message' => 'Dify 请求失败: ' . ($curlError ?: '网络错误'),
|
||||
'data' => null,
|
||||
], 502));
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$curlError = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($response === false) {
|
||||
throw new HttpResponseException(json([
|
||||
'code' => 1,
|
||||
'message' => 'Dify 请求失败: ' . ($curlError ?: '网络错误'),
|
||||
'data' => null,
|
||||
], 502));
|
||||
}
|
||||
|
||||
if ($httpCode !== 200) {
|
||||
$detail = self::parseErrorBody($response) ?: ('HTTP ' . $httpCode);
|
||||
if (
|
||||
$attempt === 0 &&
|
||||
$conversationId &&
|
||||
self::isConversationNotFoundError($response . ' ' . $detail)
|
||||
) {
|
||||
$conversationReset = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new HttpResponseException(json([
|
||||
'code' => 1,
|
||||
'message' => self::humanizeError('Dify 请求失败: ' . $detail . self::urlHint($httpCode)),
|
||||
'data' => null,
|
||||
], 502));
|
||||
}
|
||||
|
||||
$data = json_decode($response, true);
|
||||
if (!$data) {
|
||||
throw new HttpResponseException(json([
|
||||
'code' => 1,
|
||||
'message' => 'Dify 响应解析失败',
|
||||
'data' => null,
|
||||
], 502));
|
||||
}
|
||||
|
||||
return [
|
||||
'answer' => $data['answer'] ?? '',
|
||||
'conversation_id' => $data['conversation_id'] ?? null,
|
||||
'conversation_reset' => $conversationReset,
|
||||
'tokens' => $data['metadata']['usage']['total_tokens'] ?? 0,
|
||||
];
|
||||
}
|
||||
|
||||
if ($httpCode !== 200) {
|
||||
$detail = self::parseErrorBody($response) ?: ('HTTP ' . $httpCode);
|
||||
throw new HttpResponseException(json([
|
||||
'code' => 1,
|
||||
'message' => self::humanizeError('Dify 请求失败: ' . $detail . self::urlHint($httpCode)),
|
||||
'data' => null,
|
||||
], 502));
|
||||
}
|
||||
|
||||
$data = json_decode($response, true);
|
||||
if (!$data) {
|
||||
throw new HttpResponseException(json([
|
||||
'code' => 1,
|
||||
'message' => 'Dify 响应解析失败',
|
||||
'data' => null,
|
||||
], 502));
|
||||
}
|
||||
|
||||
return [
|
||||
'answer' => $data['answer'] ?? '',
|
||||
'conversation_id' => $data['conversation_id'] ?? null,
|
||||
'tokens' => $data['metadata']['usage']['total_tokens'] ?? 0,
|
||||
];
|
||||
throw new \RuntimeException('Dify 会话恢复失败');
|
||||
}
|
||||
|
||||
public static function streamChat(
|
||||
AiModel $model,
|
||||
string $query,
|
||||
array $files,
|
||||
?string $conversationId,
|
||||
string $userId,
|
||||
callable $onChunk,
|
||||
callable $onDone,
|
||||
?callable $onError = null,
|
||||
?callable $onConversationReset = null
|
||||
): void {
|
||||
$emittedContent = false;
|
||||
$attemptError = null;
|
||||
$attemptDone = null;
|
||||
|
||||
$chunkProxy = function (string $delta) use (&$emittedContent, $onChunk) {
|
||||
$emittedContent = true;
|
||||
$onChunk($delta);
|
||||
};
|
||||
$doneProxy = function (?string $newConversationId, int $tokens) use (&$attemptDone) {
|
||||
$attemptDone = [$newConversationId, $tokens];
|
||||
};
|
||||
$errorProxy = function (string $message) use (&$attemptError) {
|
||||
$attemptError = $message;
|
||||
};
|
||||
|
||||
self::streamChatAttempt(
|
||||
$model,
|
||||
$query,
|
||||
$files,
|
||||
$conversationId,
|
||||
$userId,
|
||||
$chunkProxy,
|
||||
$doneProxy,
|
||||
$errorProxy
|
||||
);
|
||||
|
||||
if (
|
||||
$attemptDone === null &&
|
||||
!$emittedContent &&
|
||||
$conversationId &&
|
||||
self::isConversationNotFoundError((string) $attemptError)
|
||||
) {
|
||||
if ($onConversationReset) {
|
||||
$onConversationReset();
|
||||
}
|
||||
$attemptError = null;
|
||||
self::streamChatAttempt(
|
||||
$model,
|
||||
$query,
|
||||
$files,
|
||||
null,
|
||||
$userId,
|
||||
$chunkProxy,
|
||||
$doneProxy,
|
||||
$errorProxy
|
||||
);
|
||||
}
|
||||
|
||||
if ($attemptDone !== null) {
|
||||
$onDone($attemptDone[0], $attemptDone[1]);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($onError && $attemptError !== null) {
|
||||
$onError($attemptError);
|
||||
}
|
||||
}
|
||||
|
||||
private static function streamChatAttempt(
|
||||
AiModel $model,
|
||||
string $query,
|
||||
array $files,
|
||||
@@ -373,6 +459,25 @@ class DifyService
|
||||
return '';
|
||||
}
|
||||
|
||||
public static function isConversationNotFoundError(string $message): bool
|
||||
{
|
||||
$message = mb_strtolower($message);
|
||||
foreach ([
|
||||
'conversation not exists',
|
||||
'conversation does not exist',
|
||||
'conversation not found',
|
||||
'conversation_not_exists',
|
||||
'conversation_not_found',
|
||||
'dify 会话已失效',
|
||||
] as $needle) {
|
||||
if (str_contains($message, $needle)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static function parseErrorBody(?string $body): ?string
|
||||
{
|
||||
if (!$body) {
|
||||
@@ -390,6 +495,10 @@ class DifyService
|
||||
*/
|
||||
public static function humanizeError(string $message): string
|
||||
{
|
||||
if (self::isConversationNotFoundError($message)) {
|
||||
return 'Dify 会话已失效,系统创建新会话后仍未恢复,请稍后重新发送。';
|
||||
}
|
||||
|
||||
if (str_contains($message, "Unsupported chat content part type: 'file'")
|
||||
|| str_contains($message, 'Unsupported chat content part type')) {
|
||||
return 'Dify 模型层仍不接受 file 类型。请确认 Dify 应用已开启文档上传,且 files.type 使用 document(不是 file)。'
|
||||
|
||||
Reference in New Issue
Block a user