This commit is contained in:
Your Name
2026-08-03 10:00:05 +08:00
parent 0fb03d0bca
commit 01729b1e0b
109 changed files with 7577 additions and 1153 deletions
+156 -47
View File
@@ -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)。'