This commit is contained in:
Your Name
2026-08-27 14:23:23 +08:00
parent b5b14516a1
commit 2fa8492c56
27 changed files with 3832 additions and 1435 deletions
+55 -7
View File
@@ -91,6 +91,9 @@ class DifyChatService
);
$lastResponse = null;
$lastSpec = [];
// 协议回退会把最初的“附件被拒”换成另一协议的状态码,因此降级判断
// 必须记住本轮出现过的附件拒绝信号,而不能只看最后一次响应。
$fileRejected = false;
foreach ($requestSpecs as $index => $requestSpec) {
$elapsedSeconds = (int) floor(microtime(true) - $startedAt);
@@ -111,11 +114,12 @@ class DifyChatService
);
$lastResponse = $response;
$lastSpec = $requestSpec;
$fileRejected = $fileRejected || self::isFileRejection($response, $attempt['files']);
// /v1 在两种协议中都是合法基址。仅在明确表示路径不存在时尝试另一协议,
// 避免因业务参数错误而重复提交同一份临床数据。
$hasFallback = isset($requestSpecs[$index + 1]);
if ($hasFallback && in_array($response['http_code'], [404, 405, 501], true)) {
if ($hasFallback && self::shouldTryNextProtocol($response, false)) {
continue;
}
break;
@@ -128,7 +132,7 @@ class DifyChatService
return $formatted;
}
// 附件整体被拒时退回纯文本重试,附件清单已在下一轮尝试中补齐。
if (!self::shouldRetryWithoutFiles((int) $lastResponse['http_code'], $attempt['files'])) {
if (!$fileRejected) {
return $formatted;
}
}
@@ -203,6 +207,9 @@ class DifyChatService
);
$lastResponse = null;
$lastSpec = [];
// 协议回退会把最初的“附件被拒”换成另一协议的状态码,因此降级判断
// 必须记住本轮出现过的附件拒绝信号,而不能只看最后一次响应。
$fileRejected = false;
foreach ($requestSpecs as $index => $requestSpec) {
$elapsedSeconds = (int) floor(microtime(true) - $startedAt);
@@ -226,13 +233,13 @@ class DifyChatService
);
$lastResponse = $response;
$lastSpec = $requestSpec;
$fileRejected = $fileRejected || self::isFileRejection($response, $attempt['files']);
// 只在尚未向下游发送任何文本、且明确为路径不支持时尝试另一协议。
$hasFallback = isset($requestSpecs[$index + 1]);
if (
$hasFallback
&& empty($response['emitted'])
&& in_array($response['http_code'], [404, 405, 501], true)
&& self::shouldTryNextProtocol($response, true)
) {
continue;
}
@@ -246,9 +253,6 @@ class DifyChatService
return $formatted;
}
// 已经推给医生的文本不能重复输出,因此只在一个字都没发出去时才降级重试。
// 附件不可达时 Dify 会在 200 流里发 event:error,同样按附件问题降级。
$fileRejected = self::shouldRetryWithoutFiles((int) $lastResponse['http_code'], $attempt['files'])
|| (!empty($lastResponse['upstream_error']) && $attempt['files'] !== []);
if (!empty($lastResponse['emitted']) || !$fileRejected) {
return $formatted;
}
@@ -458,6 +462,26 @@ class DifyChatService
return $files !== [] && in_array($httpCode, self::FILE_REJECTION_CODES, true);
}
/**
* 判断一次上游响应是否属于“这批附件我处理不了”。
*
* 除了 4xx 状态码,Dify 拉不到附件时会在 200 的 SSE 流里发 event:error
* 这两种形态都必须触发去掉附件的降级重试。
*
* @param array<string,mixed> $response
* @param array<int,array<string,string>> $files
*/
private static function isFileRejection(array $response, array $files): bool
{
if ($files === [] || (int) ($response['errno'] ?? 0) !== 0) {
return false;
}
if (self::shouldRetryWithoutFiles((int) ($response['http_code'] ?? 0), $files)) {
return true;
}
return !empty($response['upstream_error']);
}
/**
* 把无法随请求送达的附件写成显式清单。模型必须知道这些资料存在但读不到,
* 才不会把“没看到”当成“没有”。
@@ -509,6 +533,30 @@ class DifyChatService
return $baseUrl . '/v1/' . $endpoint;
}
/**
* Decide whether an ambiguous base URL should be tried with the other wire
* protocol. A 400/415/422 response cannot have started generation, and a
* 2xx stream with no delivered delta but no valid terminal frame is also
* safe to retry. Authentication, rate-limit and server failures retain
* their original diagnosis instead of being hidden by a second request.
*
* @param array<string,mixed> $response
*/
private static function shouldTryNextProtocol(array $response, bool $streaming): bool
{
if ((int) ($response['errno'] ?? 0) !== 0) {
return false;
}
$httpCode = (int) ($response['http_code'] ?? 0);
if (in_array($httpCode, [400, 404, 405, 415, 422, 501], true)) {
return true;
}
if (!$streaming || $httpCode < 200 || $httpCode >= 300 || !empty($response['emitted'])) {
return false;
}
return !empty($response['upstream_error']) || empty($response['finished']);
}
private static function isValidBaseUrl(string $baseUrl): bool
{
if (preg_match('/[\x00-\x20\x7f]/', $baseUrl)) {