126 lines
8.3 KiB
PHP
126 lines
8.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace think\facade {
|
|
class Log {
|
|
public static function warning(string $message): void {}
|
|
}
|
|
}
|
|
|
|
namespace app\common\service {
|
|
require __DIR__ . '/fixtures/CancellableCurlMultiStub.php';
|
|
function config(string $name): array { return $GLOBALS['integrationConfig']; }
|
|
function curl_init(): \stdClass { return new \stdClass(); }
|
|
function curl_setopt_array(\stdClass $handle, array $options): bool {
|
|
$handle->options = $options;
|
|
$handle->url = $options[CURLOPT_URL];
|
|
$handle->payload = json_decode($options[CURLOPT_POSTFIELDS], true);
|
|
return true;
|
|
}
|
|
function curl_exec(\stdClass $handle) {
|
|
$payload = $handle->payload;
|
|
$streaming = ($payload['response_mode'] ?? '') === 'streaming' || !empty($payload['stream']);
|
|
$GLOBALS['integrationRequests'][] = ['url' => $handle->url, 'payload' => $payload, 'streaming' => $streaming];
|
|
$scenario = $GLOBALS['integrationScenario'];
|
|
if (($scenario === 'blocking_only' || $scenario === 'cancel_blocking') && $streaming) $handle->code = 400;
|
|
elseif ($scenario === 'file_rejected' && !empty($payload['files'])) $handle->code = 400;
|
|
elseif ($scenario === 'protocol_fallback' && str_ends_with($handle->url, '/chat-messages')) $handle->code = 404;
|
|
else $handle->code = 200;
|
|
if (isset($handle->options[CURLOPT_HEADERFUNCTION])) {
|
|
($handle->options[CURLOPT_HEADERFUNCTION])($handle, 'HTTP/1.1 ' . $handle->code . " Synthetic\r\n");
|
|
}
|
|
if ($handle->code !== 200) return $streaming ? true : '{}';
|
|
$progressOption = defined('CURLOPT_XFERINFOFUNCTION') ? CURLOPT_XFERINFOFUNCTION : CURLOPT_PROGRESSFUNCTION;
|
|
$progress = $handle->options[$progressOption] ?? null;
|
|
if (!$streaming) {
|
|
if ($scenario === 'cancel_blocking') $GLOBALS['integrationCancelled'] = true;
|
|
if (is_callable($progress) && $progress($handle, 0, 0, 100, 100)) return false;
|
|
return json_encode(['answer' => '合成完整回答']);
|
|
}
|
|
if (is_callable($progress) && $progress($handle, 0, 0, 100, 100)) return false;
|
|
$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: {\"event\":\"message_end\"}\n\n");
|
|
} else {
|
|
$write($handle, 'data: ' . json_encode(['choices' => [['delta' => ['content' => '合成完整回答']]]]) . "\n\n");
|
|
$write($handle, "data: [DONE]\n\n");
|
|
}
|
|
return true;
|
|
}
|
|
function curl_errno(\stdClass $handle): int { return 0; }
|
|
function curl_getinfo(\stdClass $handle, int $option): int { return $handle->code ?? 200; }
|
|
function curl_close(\stdClass $handle): void { ++$GLOBALS['integrationClosed']; }
|
|
}
|
|
|
|
namespace {
|
|
require dirname(__DIR__) . '/vendor/autoload.php';
|
|
use app\common\service\DifyChatService;
|
|
use app\adminapi\logic\tcm\DiagnosisAiLogic;
|
|
use app\adminapi\service\AssistantSseTiming;
|
|
function integrationExpect(bool $ok, string $message): void { if (!$ok) throw new RuntimeException($message); }
|
|
$integrationConfig = ['enable' => true, 'base_url' => 'https://synthetic.invalid/v1/chat-messages', 'timeout' => 5,
|
|
'max_files' => 2, 'models' => ['qwen' => ['name' => 'synthetic', 'api_key' => 'fixture-only']]];
|
|
$reset = static function (string $scenario = 'success'): void {
|
|
$GLOBALS['integrationScenario'] = $scenario;
|
|
$GLOBALS['integrationRequests'] = [];
|
|
$GLOBALS['integrationCancelled'] = false;
|
|
$GLOBALS['integrationClosed'] = 0;
|
|
};
|
|
$abort = static fn (): bool => $GLOBALS['integrationCancelled'];
|
|
$strict = ['strict_files' => true, 'knowledge_mode' => 'evidence', 'should_abort' => $abort];
|
|
$images = array_map(static fn (int $id): array => ['type' => 'image', 'url' => 'https://synthetic.invalid/' . $id . '.png'], [1, 2, 3]);
|
|
$document = [['type' => 'document', 'url' => 'https://synthetic.invalid/report.pdf']];
|
|
$call = static fn (array $files): array => DifyChatService::chat('qwen', [], '合成资料', 'synthetic-user', $files, $strict);
|
|
|
|
$reset();
|
|
integrationExpect($call($images)['error_code'] === 'STRICT_FILES_INVALID_OR_LIMIT' && $integrationRequests === [], 'strict + abort rejects over-limit files before transport');
|
|
integrationExpect($call([['type' => 'image', 'url' => 'invalid']])['error_code'] === 'STRICT_FILES_INVALID_OR_LIMIT'
|
|
&& $integrationRequests === [], 'strict + abort rejects invalid files before transport');
|
|
$result = $call([$images[0], $images[0]]);
|
|
integrationExpect(!empty($result['ok']) && $result['transmitted_file_count'] === 2 && $result['attachment_transport'] === 'dify'
|
|
&& count($integrationRequests[0]['payload']['files']) === 2, 'strict stream retains logical duplicates and exact success metadata');
|
|
|
|
$reset('file_rejected');
|
|
$result = $call([$images[0]]);
|
|
integrationExpect(empty($result['ok']) && count($integrationRequests) === 1 && count($integrationRequests[0]['payload']['files']) === 1,
|
|
'strict rejected files never trigger a text-only success fallback');
|
|
$integrationConfig['base_url'] = 'https://synthetic.invalid/v1/chat/completions';
|
|
$reset();
|
|
integrationExpect($call($document)['error_code'] === 'FILE_TYPE_UNSUPPORTED' && $integrationRequests === [], 'strict documents cannot be omitted by OpenAI transport');
|
|
$result = $call([$images[0]]);
|
|
integrationExpect(!empty($result['ok']) && $result['transmitted_file_count'] === 1 && $result['attachment_transport'] === 'openai', 'strict supported OpenAI images retain success metadata');
|
|
$integrationConfig['base_url'] = 'https://synthetic.invalid/v1';
|
|
$reset('protocol_fallback');
|
|
integrationExpect($call($document)['error_code'] === 'FILE_TYPE_UNSUPPORTED' && count($integrationRequests) === 1,
|
|
'strict protocol fallback fails explicitly before an unsupported document request');
|
|
|
|
$integrationConfig['base_url'] = 'https://synthetic.invalid/v1/chat-messages';
|
|
$prepared = ['profile' => 'qwen', 'task' => 'summary', 'query' => '合成资料', 'inputs' => [], 'user' => 'synthetic-user', 'upstream_timeout' => 5];
|
|
$runAssistant = static function () use ($prepared, $abort): array {
|
|
$stops = []; $ends = 0; $deltas = []; $progress = [];
|
|
$result = DiagnosisAiLogic::streamPreparedAssistant($prepared,
|
|
static function (string $text) use (&$deltas): bool { $deltas[] = $text; return true; }, $abort, new AssistantSseTiming(), [
|
|
'knowledge_mode' => 'evidence',
|
|
'on_stop' => static function (bool $accepted) use (&$stops): void { $stops[] = $accepted; },
|
|
'on_transfer_end' => static function () use (&$ends): void { ++$ends; },
|
|
'on_progress' => static function (array $event) use (&$progress): void { $progress[] = $event; },
|
|
]);
|
|
return compact('result', 'stops', 'ends', 'deltas', 'progress');
|
|
};
|
|
$reset('blocking_only');
|
|
$test = $runAssistant();
|
|
integrationExpect(is_array($test['result']) && $test['result']['answer'] === '合成完整回答', 'actual blocking-only gateway compatibility succeeds');
|
|
integrationExpect(array_column($integrationRequests, 'streaming') === [true, false]
|
|
&& array_column(array_column($integrationRequests, 'payload'), 'response_mode') === ['streaming', 'blocking'], 'compatibility sends streaming once then genuine blocking once');
|
|
integrationExpect($test['ends'] === 2 && $test['stops'] === [], 'completed compatibility transfer does not invent stop acceptance');
|
|
|
|
$reset('cancel_blocking');
|
|
$test = $runAssistant();
|
|
integrationExpect($test['result'] === null && DiagnosisAiLogic::getAssistantErrorCode() === 'AI_ASSISTANT_CANCELLED', 'cancellation during real blocking compatibility terminates safely');
|
|
integrationExpect(array_column($integrationRequests, 'streaming') === [true, false] && $integrationClosed === 2
|
|
&& $test['ends'] === 2 && $test['stops'] === [false] && $test['deltas'] === [], 'cancel closes blocking curl, leaves remote stop unconfirmed and sends no retry or late text');
|
|
echo "Diagnosis AI strict files + cancellation / actual blocking compatibility integration: OK\n";
|
|
}
|