108 lines
5.8 KiB
PHP
108 lines
5.8 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace think\facade {
|
|
class Log {
|
|
public static function warning(string $message, array $context = []): void {}
|
|
public static function write(string $message, string $level = ''): void {}
|
|
}
|
|
}
|
|
namespace app\adminapi\logic\tcm {
|
|
function config(string $key): array { return ['timeout' => 90]; }
|
|
function hrtime(bool $number = false): int|array {
|
|
$now = $GLOBALS['retryClockNs'];
|
|
return $number ? $now : [intdiv($now, 1000000000), $now % 1000000000];
|
|
}
|
|
}
|
|
namespace app\adminapi\service {
|
|
function hrtime(bool $number = false): int|array {
|
|
return \app\adminapi\logic\tcm\hrtime($number);
|
|
}
|
|
}
|
|
namespace app\common\service {
|
|
class DifyChatService {
|
|
public static array $calls = [];
|
|
public static array $stream = [];
|
|
public static array $blocking = [];
|
|
public static function streamChat($profile, $inputs, $query, $user, $onDelta, $abort, $files, $options): array {
|
|
self::$calls[] = ['kind' => 'stream', 'profile' => $profile, 'options' => $options];
|
|
$GLOBALS['retryClockNs'] += (self::$stream['advance_ms'] ?? 0) * 1000000;
|
|
foreach (self::$stream['chunks'] ?? [] as $chunk) $onDelta($chunk);
|
|
return self::$stream;
|
|
}
|
|
public static function chat($profile, $inputs, $query, $user, $files, $options): array {
|
|
self::$calls[] = ['kind' => 'blocking', 'profile' => $profile, 'options' => $options];
|
|
return self::$blocking;
|
|
}
|
|
}
|
|
}
|
|
namespace {
|
|
require dirname(__DIR__) . '/vendor/autoload.php';
|
|
use app\adminapi\logic\tcm\DiagnosisAiLogic as Logic;
|
|
use app\adminapi\service\TcmEvidenceGuard;
|
|
use app\common\service\DifyChatService as Transport;
|
|
$assertions = 0;
|
|
function retryCheck(bool $condition, string $message): void {
|
|
++$GLOBALS['assertions'];
|
|
if (!$condition) throw new \RuntimeException($message);
|
|
}
|
|
$guard = TcmEvidenceGuard::assess(['id' => 1, 'chief_complaint' => '口干',
|
|
'present_illness' => '口干,未记录固定刺痛。', 'tongue' => '舌淡红苔少', 'pulse' => '脉细弱']);
|
|
$prepared = ['task' => 'summary', 'profile' => 'qwen', 'admin_id' => 1,
|
|
'query' => '<USER_QUESTION>总结本次资料</USER_QUESTION>', 'inputs' => [], 'files' => [],
|
|
'user' => 'synthetic-offline-retry', 'upstream_timeout' => 90,
|
|
'tcm_evidence_guard' => $guard];
|
|
foreach (['qwen', 'openai'] as $profile) {
|
|
$prepared['profile'] = $profile;
|
|
foreach (['NEW final', "NEW final\n\nsecond paragraph"] as $new) {
|
|
$GLOBALS['retryClockNs'] = 1000000000;
|
|
Transport::$calls = [];
|
|
Transport::$stream = ['ok' => false, 'error_code' => 'INCOMPLETE_RESPONSE', 'chunks' => ['OLD buffered']];
|
|
Transport::$blocking = ['ok' => true, 'content' => $new];
|
|
$visible = '';
|
|
$out = Logic::streamPreparedAssistant($prepared, static function (string $delta) use (&$visible): bool {
|
|
$visible .= $delta; return true;
|
|
});
|
|
retryCheck($out !== null && $visible === $new && $out['answer'] === $new,
|
|
'Only the exact replacement answer can leave the compatibility attempt.');
|
|
retryCheck(count(Transport::$calls) === 2 && !str_contains($visible, 'OLD'), 'No failed attempt buffer survives.');
|
|
}
|
|
|
|
$GLOBALS['retryClockNs'] = 1000000000;
|
|
Transport::$calls = [];
|
|
Transport::$stream = ['ok' => false, 'error_code' => 'INCOMPLETE_RESPONSE', 'chunks' => ['OLD buffered']];
|
|
Transport::$blocking = ['ok' => true, 'content' => '当前无固定刺痛。'];
|
|
$visible = '';
|
|
$out = Logic::streamPreparedAssistant($prepared, static function (string $delta) use (&$visible): bool {
|
|
$visible .= $delta; return true;
|
|
});
|
|
retryCheck($out === null && Logic::getAssistantErrorCode() === 'TCM_FACT_STATE_CONFLICT' && $visible === '',
|
|
'The new answer is independently checked; neither its conflict nor the old pending text is exposed.');
|
|
|
|
$GLOBALS['retryClockNs'] = 1000000000;
|
|
Transport::$calls = [];
|
|
Transport::$stream = ['ok' => false, 'error_code' => 'INCOMPLETE_RESPONSE', 'chunks' => ['OLD buffered'], 'advance_ms' => 9000];
|
|
Transport::$blocking = ['ok' => true, 'content' => 'NEW final'];
|
|
$out = Logic::streamPreparedAssistant($prepared, static fn (string $text): bool => true,
|
|
null, null, ['timeout' => 90, 'timeout_ms' => 12500]);
|
|
retryCheck($out !== null && Transport::$calls[0]['options']['timeout_ms'] === 12500
|
|
&& Transport::$calls[1]['options']['timeout_ms'] === 3500,
|
|
'An ordinary compatibility retry shares the original precise budget.');
|
|
|
|
$GLOBALS['retryClockNs'] = 1000000000;
|
|
Transport::$calls = [];
|
|
Transport::$stream = ['ok' => false, 'error_code' => 'INCOMPLETE_RESPONSE', 'chunks' => ['OLD buffered']];
|
|
$readyCalls = 0;
|
|
$out = Logic::streamPreparedAssistant($prepared, static fn (string $text): bool => true, null, null,
|
|
['can_start_attempt' => static function () use (&$readyCalls): bool { return ++$readyCalls === 1; }]);
|
|
retryCheck($out === null && Logic::getAssistantErrorCode() === 'UPSTREAM_CLEANUP_PENDING'
|
|
&& count(Transport::$calls) === 1, 'No compatibility generation starts while prior cleanup is unresolved.');
|
|
|
|
Transport::$calls = [];
|
|
$out = Logic::streamPreparedAssistant($prepared, static fn (string $text): bool => true, null, null,
|
|
['can_start_attempt' => static fn (): bool => false]);
|
|
retryCheck($out === null && Transport::$calls === [], 'Readiness is checked before the first network attempt.');
|
|
}
|
|
echo "Assistant retry isolation: {$assertions} assertions passed (offline transport).\n";
|
|
}
|