更新
This commit is contained in:
@@ -21,7 +21,7 @@ namespace app\common\service {
|
||||
$stream = ($payload['response_mode'] ?? '') === 'streaming' || !empty($payload['stream']);
|
||||
$GLOBALS['requiredRequests'][] = ['url' => $handle->url, 'payload' => $payload];
|
||||
$scenario = $GLOBALS['requiredScenario'];
|
||||
$handle->code = $scenario === 'reject' ? 413 : ($scenario === 'input_rejected' && !empty($payload['inputs']) ? 400
|
||||
$handle->code = $scenario === 'reject' && !empty($payload['files']) ? 413 : ($scenario === 'input_rejected' && !empty($payload['inputs']) ? 400
|
||||
: ($scenario === 'fallback' && str_ends_with($handle->url, '/chat-messages') ? 404 : 200));
|
||||
if (isset($handle->options[CURLOPT_HEADERFUNCTION])) {
|
||||
($handle->options[CURLOPT_HEADERFUNCTION])($handle, 'HTTP/1.1 ' . $handle->code . " Synthetic\r\n");
|
||||
@@ -39,13 +39,16 @@ namespace app\common\service {
|
||||
}
|
||||
return $errorBody;
|
||||
}
|
||||
if (!$stream) return json_encode(['answer' => '合成回答']);
|
||||
$answer = ($payload['inputs']['synthetic_task'] ?? '') === 'prescription_generate'
|
||||
? json_encode(['clinical_diagnosis' => '合成测试证候', 'herbs' => [['name' => '合成药名', 'dosage' => 1]],
|
||||
'rationale' => '合成草稿,需人工复核'], JSON_UNESCAPED_UNICODE) : '合成回答';
|
||||
if (!$stream) return json_encode(['answer' => $answer]);
|
||||
$write = $handle->options[CURLOPT_WRITEFUNCTION];
|
||||
if (str_ends_with($handle->url, '/chat-messages')) {
|
||||
$write($handle, 'data: ' . json_encode(['event' => 'message', 'answer' => '合成回答']) . "\n\n");
|
||||
$write($handle, 'data: ' . json_encode(['event' => 'message', 'answer' => $answer]) . "\n\n");
|
||||
$write($handle, "data: {\"event\":\"message_end\"}\n\n");
|
||||
} else {
|
||||
$write($handle, 'data: ' . json_encode(['choices' => [['delta' => ['content' => '合成回答']]]]) . "\n\n");
|
||||
$write($handle, 'data: ' . json_encode(['choices' => [['delta' => ['content' => $answer]]]]) . "\n\n");
|
||||
$write($handle, "data: [DONE]\n\n");
|
||||
}
|
||||
return true;
|
||||
@@ -87,23 +90,25 @@ namespace {
|
||||
$result = $streaming
|
||||
? DifyChatService::streamChat('qwen', [], '合成资料;聊天:无', 'synthetic-user', static fn (string $text): bool => true, null, $withVideos, $withProgress)
|
||||
: DifyChatService::chat('qwen', [], '合成资料;聊天:无', 'synthetic-user', $withVideos, $withProgress);
|
||||
requiredExpect(!empty($result['ok']) && count($requiredRequests) === 1, 'required request succeeds exactly once');
|
||||
requiredExpect(count($requiredRequests[0]['payload']['files']) === 8 && $result['transmitted_file_count'] === 8,
|
||||
'all eight images/documents reach serialized wire payload despite default max_files=3');
|
||||
requiredExpect(array_column($requiredRequests[0]['payload']['files'], 'url') === array_column($files, 'url'), 'video references never enter payload');
|
||||
requiredExpect(!empty($result['ok']) && count($requiredRequests) === 4, 'three evidence batches finish with one original clinical task');
|
||||
$wireFiles = array_merge(...array_map(static fn (array $request): array => $request['payload']['files'] ?? [], $requiredRequests));
|
||||
requiredExpect(count($wireFiles) === 8 && $result['transmitted_file_count'] === 8,
|
||||
'all eight images/documents reach bounded batches with max_files=3');
|
||||
requiredExpect(array_column($wireFiles, 'url') === array_column($files, 'url'), 'video references never enter payload and original positions remain complete');
|
||||
requiredExpect($result['transfer_manifest_final'] === true && $result['attachment_count'] === 8
|
||||
&& array_unique(array_column($result['items'], 'status')) === ['submitted'], 'final attachment manifest reflects actual complete payload');
|
||||
requiredExpect(count($result['required_sections']) === 13
|
||||
&& array_unique(array_column($result['required_sections'], 'status')) === ['submitted']
|
||||
&& $result['required_sections'][12]['availability'] === 'empty', 'empty required text section is explicitly submitted, not omitted');
|
||||
requiredExpect($events[0]['required_sections'][0]['status'] === 'submitting'
|
||||
&& $events[1]['source_summary']['required_sections'][0]['status'] === 'submitted', 'progress moves actual source manifest from submitting to submitted');
|
||||
requiredExpect($events[0]['required_sections'][0]['status'] === 'prepared'
|
||||
&& $events[count($events) - 1]['source_summary']['required_sections'][0]['status'] === 'submitted', 'full clinical body stays prepared until final original task submission');
|
||||
$reset('reject');
|
||||
$result = $streaming
|
||||
? DifyChatService::streamChat('qwen', [], '合成资料', 'synthetic-user', static fn (string $text): bool => true, null, $files, $options)
|
||||
: DifyChatService::chat('qwen', [], '合成资料', 'synthetic-user', $files, $options);
|
||||
requiredExpect(($result['error_code'] ?? '') === 'REQUIRED_FILES_REJECTED' && count($requiredRequests) === 1
|
||||
&& count($requiredRequests[0]['payload']['files']) === 8, 'hard provider limit fails explicitly without stripping files');
|
||||
requiredExpect(!empty($result['ok']) && $result['rejected_attachment_count'] === 8
|
||||
&& $result['submitted_attachment_count'] === 0 && count($requiredRequests) <= 17
|
||||
&& empty($requiredRequests[count($requiredRequests) - 1]['payload']['files']), 'confirmed per-file rejection finishes original task with eight explicit unread gaps');
|
||||
}
|
||||
foreach ([['type' => 'unknown', 'url' => 'https://synthetic.invalid/file'], ['type' => 'image', 'url' => 'invalid'],
|
||||
['type' => 'custom', 'url' => 'https://synthetic.invalid/file'], ['type' => 'image', 'url' => 'https://synthetic.invalid/a.png', 'transfer_method' => 'local_file']] as $invalid) {
|
||||
@@ -115,9 +120,10 @@ namespace {
|
||||
$result = $streaming
|
||||
? DifyChatService::streamChat('qwen', ['synthetic_undeclared' => 'value'], '合成正文', 'synthetic-user', static fn (string $text): bool => true, null, $files, $options)
|
||||
: DifyChatService::chat('qwen', ['synthetic_undeclared' => 'value'], '合成正文', 'synthetic-user', $files, $options);
|
||||
requiredExpect(!empty($result['ok']) && count($requiredRequests) === 2
|
||||
&& count($requiredRequests[0]['payload']['files']) === 8 && count($requiredRequests[1]['payload']['files']) === 8,
|
||||
'undeclared Dify inputs retry preserves complete required files and query');
|
||||
requiredExpect(!empty($result['ok']) && count($requiredRequests) === 5
|
||||
&& $result['submitted_attachment_count'] === 8
|
||||
&& $requiredRequests[3]['payload']['query'] === $requiredRequests[4]['payload']['query'],
|
||||
'undeclared Dify inputs retry preserves final complete original body and all batch evidence');
|
||||
}
|
||||
$reset();
|
||||
$fallbackOptions = array_replace($options, ['required_sections' => []]);
|
||||
@@ -126,17 +132,22 @@ namespace {
|
||||
'source_summary manifest fallback keeps all text sections when attachment list is empty');
|
||||
$requiredConfig['base_url'] = 'https://synthetic.invalid/v1/chat/completions';
|
||||
$reset(); $result = DifyChatService::chat('qwen', [], '合成资料', 'synthetic-user', $files, $options);
|
||||
requiredExpect(($result['error_code'] ?? '') === 'REQUIRED_FILES_UNSUPPORTED' && $requiredRequests === [], 'OpenAI documents fail before unsupported transmission');
|
||||
requiredExpect(!empty($result['ok']) && $result['submitted_attachment_count'] === 4
|
||||
&& $result['rejected_attachment_count'] === 4, 'OpenAI documents become explicit unread gaps while every supported image and original body continue');
|
||||
$reset(); $result = DifyChatService::chat('qwen', [], '合成资料', 'synthetic-user', array_slice($files, 0, 4), $options);
|
||||
requiredExpect(!empty($result['ok']) && count($requiredRequests[0]['payload']['messages'][0]['content']) === 5, 'all four OpenAI image_url parts are serialized');
|
||||
requiredExpect(!empty($result['ok']) && $result['submitted_attachment_count'] === 4
|
||||
&& count($requiredRequests) === 3 && count($requiredRequests[0]['payload']['messages'][0]['content']) === 4
|
||||
&& count($requiredRequests[1]['payload']['messages'][0]['content']) === 2, 'all four OpenAI image_url parts are serialized within the configured cap');
|
||||
$requiredConfig['base_url'] = 'https://synthetic.invalid/v1';
|
||||
$reset('fallback'); $result = DifyChatService::chat('qwen', [], '合成资料', 'synthetic-user', $files, $options);
|
||||
requiredExpect(($result['error_code'] ?? '') === 'REQUIRED_FILES_UNSUPPORTED' && count($requiredRequests) === 1, 'protocol fallback cannot fake document submission');
|
||||
requiredExpect(!empty($result['ok']) && $result['submitted_attachment_count'] === 4
|
||||
&& $result['rejected_attachment_count'] === 4, 'protocol fallback cannot fake document submission and preserves explicit gaps');
|
||||
|
||||
$requiredConfig['base_url'] = 'https://synthetic.invalid/v1/chat-messages';
|
||||
$prepared = ['profile' => 'qwen', 'task' => 'summary', 'query' => '合成资料;聊天:无', 'user' => 'synthetic-user',
|
||||
'files' => $files, 'required_sections' => $sections, 'source_summary' => ['required_sections' => $sections], 'upstream_timeout' => 5];
|
||||
$run = static function () use ($prepared): array {
|
||||
$run = static function (array $overrides = []) use ($prepared): array {
|
||||
$prepared = array_replace($prepared, $overrides);
|
||||
$deltas = []; $events = [];
|
||||
$result = DiagnosisAiLogic::streamPreparedAssistant($prepared,
|
||||
static function (string $delta) use (&$deltas): bool { $deltas[] = $delta; return true; },
|
||||
@@ -148,9 +159,25 @@ namespace {
|
||||
requiredExpect($test['result']['transfer_manifest_final'] === true && $test['result']['attachment_count'] === 8
|
||||
&& $test['result']['required_sections'][0]['status'] === 'submitted'
|
||||
&& $test['result']['source_summary']['required_sections'][0]['status'] === 'submitted', 'assistant done preserves actual final manifest instead of prepared status');
|
||||
foreach (['summary', 'tcm_pattern', 'prescription_review', 'prescription_generate', 'medication_review',
|
||||
'exam_review', 'complication_risk', 'guideline_review', 'custom'] as $task) {
|
||||
$reset();
|
||||
$taskQuery = '完整合成病历;TASK=' . $task . ';保留原始任务输出结构';
|
||||
$taskInputs = ['synthetic_task' => $task, 'clinical_context' => $taskQuery];
|
||||
$test = $run(['task' => $task, 'query' => $taskQuery, 'inputs' => $taskInputs,
|
||||
'files' => array_slice($files, 0, 7)]);
|
||||
requiredExpect(is_array($test['result']) && $test['result']['task'] === $task
|
||||
&& $test['result']['attachment_count'] === 7 && $test['result']['submitted_attachment_count'] === 7
|
||||
&& count($requiredRequests) === 4 && count($test['deltas']) === 1, 'all nine real assistant task routes retain all seven attachments and emit only final answer: ' . $task);
|
||||
$final = $requiredRequests[3]['payload'];
|
||||
requiredExpect(str_starts_with($final['query'], $taskQuery) && $final['inputs'] === $taskInputs,
|
||||
'each original assistant task and all original inputs survive evidence synthesis');
|
||||
if ($task === 'prescription_generate') requiredExpect(is_array($test['result']['prescription_draft'] ?? null),
|
||||
'JSON prescription schema stays parseable without a prose notice prefix');
|
||||
}
|
||||
$reset('reject'); $test = $run();
|
||||
requiredExpect($test['result'] === null && DiagnosisAiLogic::getAssistantErrorCode() === 'REQUIRED_FILES_REJECTED'
|
||||
&& count($requiredRequests) === 1 && $test['deltas'] === [], 'assistant mandatory rejection does not trigger blocking compatibility retry');
|
||||
requiredExpect(is_array($test['result']) && $test['result']['submitted_attachment_count'] === 0
|
||||
&& count($requiredRequests) <= 17 && $test['deltas'] === ['合成回答'], 'assistant receives only final clinical answer after bounded affirmative file isolation');
|
||||
$reset('cancel'); $test = $run();
|
||||
requiredExpect($test['result'] === null && DiagnosisAiLogic::getAssistantErrorCode() === 'AI_ASSISTANT_CANCELLED'
|
||||
&& count($requiredRequests) === 1 && $requiredClosed === 1 && $test['deltas'] === [], 'cancellation still closes transport without late answer or retry');
|
||||
|
||||
Reference in New Issue
Block a user