更新
This commit is contained in:
@@ -38,7 +38,8 @@ class DifyChatService
|
||||
array $inputs,
|
||||
string $query,
|
||||
string $user,
|
||||
array $files = []
|
||||
array $files = [],
|
||||
array $options = []
|
||||
): array
|
||||
{
|
||||
$config = config('prescription_ai') ?: [];
|
||||
@@ -61,7 +62,9 @@ class DifyChatService
|
||||
return self::error('CONFIG_INVALID', 'AI 服务配置无效');
|
||||
}
|
||||
|
||||
$timeout = (int) ($config['timeout'] ?? 0);
|
||||
// A caller may raise the single-request budget for staged background analysis. It stays
|
||||
// bounded by MAX_TIMEOUT; interactive callers keep the configured default.
|
||||
$timeout = (int) ($options['timeout'] ?? $config['timeout'] ?? 0);
|
||||
if (!self::isValidTimeout($timeout)) {
|
||||
return self::error('CONFIG_INVALID', 'AI 服务超时配置无效');
|
||||
}
|
||||
@@ -74,11 +77,18 @@ class DifyChatService
|
||||
return self::error('CONFIG_INVALID', 'AI 模型配置无效');
|
||||
}
|
||||
|
||||
$normalized = self::normalizeFiles($files, self::maxFiles($config));
|
||||
$strictFiles = !empty($options['strict_files']);
|
||||
$normalized = self::normalizeFiles($files, self::maxFiles($config, $modelConfig), !$strictFiles);
|
||||
if ($strictFiles && !self::strictFilesComplete($files, $normalized)) {
|
||||
return self::error('STRICT_FILES_INVALID_OR_LIMIT', '附件无效或超出单批上限,须分批处理');
|
||||
}
|
||||
$startedAt = microtime(true);
|
||||
$formatted = null;
|
||||
|
||||
foreach (self::buildAttemptPlan($normalized['kept'], $normalized['dropped']) as $attempt) {
|
||||
$attemptPlan = $strictFiles
|
||||
? [['files' => $normalized['kept'], 'omitted' => []]]
|
||||
: self::buildAttemptPlan($normalized['kept'], $normalized['dropped']);
|
||||
foreach ($attemptPlan as $attempt) {
|
||||
$fileRejected = false;
|
||||
$inputRejected = false;
|
||||
|
||||
@@ -100,6 +110,11 @@ class DifyChatService
|
||||
$lastSpec = [];
|
||||
|
||||
foreach ($requestSpecs as $index => $requestSpec) {
|
||||
// The legacy compatible path cannot transmit documents. Strict callers must
|
||||
// receive an explicit gap, never a successful text-only fallback.
|
||||
if ($strictFiles && !self::strictProtocolSupportsFiles($requestSpec['protocol'], $attempt['files'])) {
|
||||
return self::error('FILE_TYPE_UNSUPPORTED', '当前模型接口不支持此类原始附件');
|
||||
}
|
||||
$elapsedSeconds = (int) floor(microtime(true) - $startedAt);
|
||||
$remainingTimeout = $timeout - $elapsedSeconds;
|
||||
if ($remainingTimeout < self::MIN_TIMEOUT) {
|
||||
@@ -142,6 +157,10 @@ class DifyChatService
|
||||
$formatted = self::formatResponse($lastResponse, $startedAt);
|
||||
self::logUpstreamFailure($lastSpec, $lastResponse, $query, $attempt['files'], $formatted);
|
||||
if (!empty($formatted['ok'])) {
|
||||
if ($strictFiles) {
|
||||
$formatted['transmitted_file_count'] = count($attempt['files']);
|
||||
$formatted['attachment_transport'] = (string) ($lastSpec['protocol'] ?? '');
|
||||
}
|
||||
return $formatted;
|
||||
}
|
||||
// Dify 只接受应用中已声明且满足长度约束的 inputs。病例正文已经完整
|
||||
@@ -210,7 +229,7 @@ class DifyChatService
|
||||
return self::error('CONFIG_INVALID', 'AI 模型配置无效');
|
||||
}
|
||||
|
||||
$normalized = self::normalizeFiles($files, self::maxFiles($config));
|
||||
$normalized = self::normalizeFiles($files, self::maxFiles($config, $modelConfig));
|
||||
$startedAt = microtime(true);
|
||||
$formatted = null;
|
||||
|
||||
@@ -422,6 +441,16 @@ class DifyChatService
|
||||
));
|
||||
}
|
||||
|
||||
private static function strictFilesComplete(array $requested, array $normalized): bool
|
||||
{
|
||||
return count($normalized['kept']) === count($requested) && $normalized['dropped'] === [];
|
||||
}
|
||||
|
||||
private static function strictProtocolSupportsFiles(string $protocol, array $files): bool
|
||||
{
|
||||
return $protocol === 'dify' || ($protocol === 'openai' && self::nonImageFiles($files) === []);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清洗附件,并按上游应用允许的数量截断。
|
||||
*
|
||||
@@ -430,6 +459,7 @@ class DifyChatService
|
||||
* 录像可能几十份),因此这里必须主动截断;被截断的附件不会被悄悄丢弃,
|
||||
* 而是以清单形式随提示词送达,让模型知道存在哪些它读不到的资料。
|
||||
* 保持调用方给定的顺序,由调用方决定哪些附件最值得送上去。
|
||||
* 严格批次关闭 URL 去重,保留同一地址对应的每个逻辑附件及其清单位置。
|
||||
*
|
||||
* @param array<int,mixed> $files
|
||||
* @return array{
|
||||
@@ -437,7 +467,7 @@ class DifyChatService
|
||||
* dropped:array<int,array{type:string,transfer_method:string,url:string}>
|
||||
* }
|
||||
*/
|
||||
private static function normalizeFiles(array $files, int $maxFiles): array
|
||||
private static function normalizeFiles(array $files, int $maxFiles, bool $deduplicate = true): array
|
||||
{
|
||||
$maxFiles = max(0, $maxFiles);
|
||||
$kept = [];
|
||||
@@ -451,7 +481,7 @@ class DifyChatService
|
||||
$url = trim((string) ($file['url'] ?? ''));
|
||||
if (!in_array($type, ['image', 'document', 'audio', 'video', 'custom'], true)
|
||||
|| !self::isValidRemoteFileUrl($url)
|
||||
|| isset($seen[$url])) {
|
||||
|| ($deduplicate && isset($seen[$url]))) {
|
||||
continue;
|
||||
}
|
||||
$seen[$url] = true;
|
||||
@@ -470,9 +500,11 @@ class DifyChatService
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $config */
|
||||
private static function maxFiles(array $config): int
|
||||
/** Each application declares its own file_upload.number_limits; the global value is the fallback. */
|
||||
private static function maxFiles(array $config, array $modelConfig = []): int
|
||||
{
|
||||
$configured = (int) ($config['max_files'] ?? self::DEFAULT_MAX_FILES);
|
||||
$configured = array_key_exists('max_files', $modelConfig)
|
||||
? (int) $modelConfig['max_files'] : (int) ($config['max_files'] ?? self::DEFAULT_MAX_FILES);
|
||||
return $configured >= 0 ? $configured : self::DEFAULT_MAX_FILES;
|
||||
}
|
||||
|
||||
@@ -1162,9 +1194,22 @@ class DifyChatService
|
||||
'content' => $answer,
|
||||
'message_id' => (string) ($decoded['message_id'] ?? $decoded['id'] ?? ''),
|
||||
'latency_ms' => $latencyMs,
|
||||
'model_name' => is_string($decoded['model'] ?? null) ? $decoded['model'] : null,
|
||||
'usage' => self::normalizedUsage($decoded['usage'] ?? $decoded['metadata']['usage'] ?? null),
|
||||
];
|
||||
}
|
||||
|
||||
/** Missing provider usage remains unknown, never a fabricated zero-token charge. */
|
||||
private static function normalizedUsage($usage): array
|
||||
{
|
||||
$result = [];
|
||||
foreach (['prompt_tokens', 'completion_tokens', 'total_tokens'] as $key) {
|
||||
$value = is_array($usage) ? ($usage[$key] ?? null) : null;
|
||||
$result[$key] = is_numeric($value) && (float) $value >= 0 ? (int) $value : null;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $decoded */
|
||||
private static function extractContent(array $decoded): string
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user