stream_socket_get_name($listener, false)]) . "\n"; flush(); $client = stream_socket_accept($listener, 5); if ($client === false) { fclose($listener); exit(3); } stream_set_timeout($client, 5); $wire = ''; while (!str_contains($wire, "\r\n\r\n")) { $chunk = fread($client, 8192); if ($chunk === '' || $chunk === false) break; $wire .= $chunk; } [$headers, $body] = array_pad(explode("\r\n\r\n", $wire, 2), 2, ''); preg_match('/\r\nContent-Length:\s*(\d+)/i', $headers, $length); $expected = (int) ($length[1] ?? 0); while (strlen($body) < $expected) { $chunk = fread($client, $expected - strlen($body)); if ($chunk === '' || $chunk === false) break; $body .= $chunk; } $payload = json_decode($body, true); $stream = ($payload['response_mode'] ?? '') === 'streaming' || !empty($payload['stream']); $status = ($argv[2] ?? '') === 'reject' ? 413 : 200; $response = $status === 413 ? '{"code":"invalid_param","message":"Number of files exceeds the limit."}' : ($stream ? "data: {\"event\":\"message\",\"answer\":\"synthetic-answer\"}\n\ndata: {\"event\":\"message_end\"}\n\n" : '{"answer":"synthetic-answer"}'); fwrite($client, 'HTTP/1.1 ' . $status . " Synthetic\r\nContent-Type: " . ($stream ? 'text/event-stream' : 'application/json') . "\r\nContent-Length: " . strlen($response) . "\r\nConnection: close\r\n\r\n" . $response); fclose($client); fclose($listener); echo json_encode(['payload' => $payload, 'body_bytes' => strlen($body), 'expected_body_bytes' => $expected], JSON_UNESCAPED_SLASHES) . "\n"; exit; } $files = []; for ($index = 1; $index <= 8; ++$index) $files[] = ['type' => $index <= 4 ? 'image' : 'document', 'url' => 'https://synthetic.invalid/required-' . $index . ($index <= 4 ? '.png' : '.pdf')]; $files[] = ['type' => 'video', 'url' => 'https://synthetic.invalid/not-transmitted.mp4']; $sections = [['key' => 'chat', 'label' => '聊天', 'count' => 0, 'availability' => 'empty', 'status' => 'prepared']]; foreach (['blocking', 'streaming', 'reject'] as $scenario) { $process = proc_open([PHP_BINARY, __FILE__, 'server', $scenario], [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes, null, null, ['bypass_shell' => true]); loopbackRequiredExpect(is_resource($process), 'loopback peer starts'); $ready = json_decode((string) fgets($pipes[1]), true); loopbackRequiredExpect(is_string($ready['address'] ?? null), 'loopback peer supplies ephemeral port'); $loopbackRequiredConfig = ['enable' => true, 'base_url' => 'http://' . $ready['address'] . '/v1/chat-messages', 'timeout' => 5, 'max_files' => 3, 'models' => ['qwen' => ['name' => 'synthetic', 'api_key' => 'loopback-fixture-only']]]; $options = ['required_files' => true, 'required_sections' => $sections, 'knowledge_mode' => 'evidence']; $result = $scenario === 'streaming' ? DifyChatService::streamChat('qwen', [], '合成资料;聊天:无', 'synthetic-user', static fn (string $delta): bool => true, null, $files, $options) : DifyChatService::chat('qwen', [], '合成资料;聊天:无', 'synthetic-user', $files, $options); $captured = json_decode((string) fgets($pipes[1]), true); fclose($pipes[0]); fclose($pipes[1]); $stderr = stream_get_contents($pipes[2]); fclose($pipes[2]); $exit = proc_close($process); loopbackRequiredExpect($exit === 0 && $stderr === '', 'loopback peer exits cleanly'); loopbackRequiredExpect($captured['body_bytes'] === $captured['expected_body_bytes'] && count($captured['payload']['files']) === 8, 'actual HTTP body carries all eight required attachments with complete Content-Length'); loopbackRequiredExpect(!str_contains(json_encode($captured['payload']), 'not-transmitted.mp4') && str_contains($captured['payload']['query'], '聊天:无'), 'actual HTTP wire contains explicit empty text and no video'); if ($scenario === 'reject') loopbackRequiredExpect(($result['error_code'] ?? '') === 'REQUIRED_FILES_REJECTED', 'real 413 terminates as required data failure'); else loopbackRequiredExpect(!empty($result['ok']) && $result['attachment_count'] === 8 && $result['required_sections'][0]['status'] === 'submitted', 'actual successful HTTP delivery has final complete manifest'); } echo "Dify required sources actual loopback HTTP blocking / streaming / hard-limit rejection: OK\n"; }