100 lines
7.2 KiB
PHP
100 lines
7.2 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace think\facade {
|
|
class Log { public static function warning(string $message): void {} public static function write(string $message, string $level): void {} }
|
|
}
|
|
namespace app\common\service {
|
|
require __DIR__ . '/fixtures/CancellableCurlMultiStub.php';
|
|
function config(string $name): array { return $GLOBALS['missingFallbackConfig']; }
|
|
final class DifyRequiredAttachmentUpload {
|
|
public static function prepare(string $endpoint, string $key, string $user, array $files, int $timeout,
|
|
?callable $abort = null, array $options = []): array {
|
|
$skips = []; $wire = []; $indices = [];
|
|
foreach ($files as $position => $file) {
|
|
$missing = $GLOBALS['missingFallbackScenario'] === 'all-missing' || $position === 0;
|
|
if ($missing) {
|
|
$skips[] = ['index' => $position + 1, 'kind' => $file['type'], 'reason' => 'not_found'];
|
|
($options['on_upload'])(['index' => $position + 1, 'status' => 'skipped', 'reason' => 'not_found']);
|
|
} else {
|
|
$wire[] = ['type' => $file['type'], 'transfer_method' => 'local_file', 'upload_file_id' => 'synthetic-id'];
|
|
$indices[] = $position + 1;
|
|
($options['on_upload'])(['index' => $position + 1, 'status' => 'uploaded']);
|
|
}
|
|
}
|
|
return ['ok' => true, 'files' => $wire, 'uploaded_indices' => $indices, 'skipped' => $skips];
|
|
}
|
|
}
|
|
function curl_init(): \stdClass { return new \stdClass(); }
|
|
function curl_setopt_array(\stdClass $handle, array $options): bool {
|
|
if (!str_starts_with($options[CURLOPT_URL], 'https://fixture.invalid/v1/')) throw new \RuntimeException('Fixture destinations only');
|
|
$handle->options = $options; return true;
|
|
}
|
|
function curl_exec(\stdClass $handle): string|bool {
|
|
$payload = json_decode($handle->options[CURLOPT_POSTFIELDS], true);
|
|
$GLOBALS['missingFallbackRequests'][] = $payload;
|
|
$dify = str_ends_with($handle->options[CURLOPT_URL], '/chat-messages');
|
|
$handle->code = $dify ? 404 : 200;
|
|
($handle->options[CURLOPT_HEADERFUNCTION])($handle, 'HTTP/1.1 ' . $handle->code . " Synthetic\r\n");
|
|
if (isset($handle->options[CURLOPT_WRITEFUNCTION])) {
|
|
$write = $handle->options[CURLOPT_WRITEFUNCTION];
|
|
if ($dify) $write($handle, '{"code":"not_found"}');
|
|
else {
|
|
$write($handle, 'data: ' . json_encode(['choices' => [['delta' => ['content' => 'synthetic answer']]]]) . "\n\n");
|
|
$write($handle, "data: [DONE]\n\n");
|
|
}
|
|
return true;
|
|
}
|
|
return $dify ? '{"code":"not_found"}' : '{"choices":[{"message":{"content":"synthetic answer"}}]}';
|
|
}
|
|
function curl_errno(\stdClass $handle): int { return 0; }
|
|
function curl_getinfo(\stdClass $handle, int $option): int { return $handle->code; }
|
|
function curl_close(\stdClass $handle): void {}
|
|
}
|
|
namespace {
|
|
require dirname(__DIR__) . '/vendor/autoload.php';
|
|
use app\common\service\DifyChatService;
|
|
function missingFallbackExpect(bool $ok, string $message): void { if (!$ok) throw new RuntimeException($message); }
|
|
$missingFallbackConfig = ['enable' => true, 'base_url' => 'https://fixture.invalid/v1',
|
|
'timeout' => 5, 'required_attachment_transport' => 'upload',
|
|
'models' => ['qwen' => ['name' => 'synthetic', 'api_key' => 'fixture-only']]];
|
|
foreach ([false, true] as $stream) foreach (['all-missing', 'missing-image-available-document',
|
|
'missing-image-available-image', 'missing-document-available-image'] as $missingFallbackScenario) {
|
|
$missingFallbackRequests = []; $events = [];
|
|
$files = $missingFallbackScenario === 'all-missing'
|
|
? [['type' => 'document', 'url' => 'https://fixture.invalid/missing.pdf'], ['type' => 'audio', 'url' => 'https://fixture.invalid/missing.wav']]
|
|
: [['type' => $missingFallbackScenario === 'missing-document-available-image' ? 'document' : 'image', 'url' => 'https://fixture.invalid/missing'],
|
|
['type' => $missingFallbackScenario === 'missing-image-available-document' ? 'document' : 'image', 'url' => 'https://fixture.invalid/available']];
|
|
$options = ['required_files' => true, 'knowledge_mode' => 'evidence',
|
|
'on_progress' => static function (array $event) use (&$events): void { $events[] = $event; }];
|
|
$result = $stream ? DifyChatService::streamChat('qwen', [], 'complete synthetic clinical text', 'synthetic-user',
|
|
static fn (string $text): bool => true, null, $files, $options)
|
|
: DifyChatService::chat('qwen', [], 'complete synthetic clinical text', 'synthetic-user', $files, $options);
|
|
$expectedMissing = $missingFallbackScenario === 'all-missing' ? 2 : 1;
|
|
missingFallbackExpect($result['skipped_attachment_count'] === $expectedMissing && $result['attachment_count'] === 2,
|
|
'protocol fallback keeps original source and confirmed missing counts');
|
|
missingFallbackExpect($result['items'][0]['status'] === 'skipped' && $result['items'][0]['reason'] === 'not_found',
|
|
'known missing item survives every fallback outcome');
|
|
if ($missingFallbackScenario === 'missing-image-available-document') {
|
|
missingFallbackExpect(!empty($result['ok']) && $result['rejected_attachment_count'] === 1
|
|
&& count($missingFallbackRequests) === 6 && $result['submitted_attachment_count'] === 0
|
|
&& $result['attachment_transfer'] === 'uploaded_files'
|
|
&& $result['items'][1]['status'] === 'rejected' && $result['items'][1]['reason'] === 'file_type',
|
|
'unsupported document is isolated as a truthful unread gap while the confirmed missing source stays skipped');
|
|
$last = $missingFallbackRequests[count($missingFallbackRequests) - 1];
|
|
missingFallbackExpect(str_contains(json_encode($last), 'complete synthetic clinical text')
|
|
&& !str_contains(json_encode($last), 'https://fixture.invalid/missing'), 'final original body never resurrects known missing URLs');
|
|
continue;
|
|
}
|
|
missingFallbackExpect(!empty($result['ok']) && count($missingFallbackRequests) === 2
|
|
&& $result['submitted_attachment_count'] === 2 - $expectedMissing, 'text or available images complete through protocol fallback');
|
|
$wire = json_encode($missingFallbackRequests[1], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
missingFallbackExpect(!str_contains($wire, 'https://fixture.invalid/missing')
|
|
&& !str_contains($wire, 'ATTACHMENTS_NOT_INLINE') && str_contains($wire, 'complete synthetic clinical text')
|
|
&& substr_count($wire, '【附件可用性说明】') === 1, 'fallback builds from available sources without resurrecting dead references');
|
|
missingFallbackExpect(($missingFallbackScenario === 'all-missing') === !str_contains($wire, 'https://fixture.invalid/available'),
|
|
'available images are preserved while all-missing requests send only text');
|
|
}
|
|
echo "Missing attachment protocol fallback / truthful receipts / no revived references / blocking and stream: OK\n";
|
|
}
|