true, 'content' => '合成证据片段 ' . $inputs['chunk_index']]; } } } namespace { require dirname(__DIR__) . '/vendor/autoload.php'; use app\adminapi\logic\tcm\DiagnosisAiLogic; use app\adminapi\service\AssistantSseTiming; use app\common\service\DifyChatService; use think\facade\Cache; function compactExpect(bool $ok, string $message): void { if (!$ok) throw new \RuntimeException($message); } $compactionConfig = ['context_compaction_cache_ttl' => 300, 'models' => ['qwen' => ['name' => 'synthetic', 'api_key' => 'fixture-secret']]]; $fit = new \ReflectionMethod(DiagnosisAiLogic::class, 'fitContextForPrompt'); $inputs = new \ReflectionMethod(DiagnosisAiLogic::class, 'buildUpstreamInputs'); $source = 'BEGIN-unique ' . str_repeat('临床合成来源;', 11000) . ' END-unique'; $original = ['case_text' => $source, 'case_lines' => [$source], 'case_json' => json_encode(['case_lines' => [$source]], JSON_UNESCAPED_UNICODE), 'fingerprint' => hash('sha256', $source), 'compaction_cache_scope' => 'authorized-scope-a', 'demographics' => '合成病例', 'source_diagnosis_ids' => [1, 2], 'source_summary' => ['diagnosis_count' => 2, 'snapshot_complete' => true], 'files' => [['type' => 'image', 'url' => 'https://example.test/synthetic.png']]]; $apply = static function (array &$context, string $profile = 'qwen') use ($fit): bool { return $fit->invokeArgs(null, [&$context, $profile, new AssistantSseTiming()]); }; $cold = $original; compactExpect($apply($cold), 'cold compaction succeeds'); compactExpect(count(DifyChatService::$calls) === 2, 'every original chunk is read'); $covered = ''; foreach (DifyChatService::$calls as $call) { preg_match('/\n(.*)\n<\/PATIENT_SOURCE_FRAGMENT>/su', $call['query'], $match); $covered .= $match[1]; compactExpect($call['options']['knowledge_mode'] === 'evidence', 'extractor excludes clinical reference synthesis'); } compactExpect($covered === $source, 'UTF-8 chunks cover the entire exact source in order'); $wire = $inputs->invoke(null, $cold, 'case', 'test'); compactExpect(strlen($wire['herbs_json']) < 2000 && !str_contains($wire['herbs_json'], $source), 'Dify no longer resends the original large source'); compactExpect($cold['fingerprint'] === $original['fingerprint'], 'original provenance stays intact'); compactExpect($cold['files'] === $original['files'] && $cold['source_diagnosis_ids'] === [1, 2], 'attachments and authorization scope are preserved'); compactExpect($cold['source_summary']['analyzed_source_bytes'] === strlen($source), 'complete source byte count is preserved'); $warm = $original; compactExpect($apply($warm) && $warm['compaction_cache_hit'] === true, 'identical freshly authorized source is reused'); compactExpect(count(DifyChatService::$calls) === 2, 'warm request makes zero new compaction calls'); foreach (['source', 'scope', 'model', 'credential'] as $change) { $next = $original; $profile = 'qwen'; if ($change === 'source') $next['case_text'] = 'X' . substr($source, 1); if ($change === 'scope') $next['compaction_cache_scope'] = 'authorized-scope-b'; if ($change === 'model') $profile = 'openai'; if ($change === 'credential') $compactionConfig['models']['qwen']['api_key'] = 'rotated-fixture'; $count = count(DifyChatService::$calls); compactExpect($apply($next, $profile) && count(DifyChatService::$calls) === $count + 2, "$change invalidates cache"); } foreach (array_keys(Cache::$items) as $key) compactExpect(!str_contains($key, 'fixture') && !str_contains($key, '来源'), 'cache keys contain no credentials or source text'); Cache::$items = []; DifyChatService::$fail = true; $broken = $original; compactExpect(!$apply($broken) && Cache::$items === [] && $broken === $original, 'failed extraction cannot poison cache or mutate context'); DifyChatService::$fail = false; Cache::$unavailable = true; $uncached = $original; compactExpect($apply($uncached) && !$uncached['compaction_cache_hit'], 'cache outages fall back to full fresh compaction'); Cache::$unavailable = false; $compactionConfig['context_compaction_cache_ttl'] = 10000; $fresh = $original; compactExpect($apply($fresh) && Cache::$lastTtl === 600, 'cache retention has a hard upper bound'); $compactionConfig['context_compaction_cache_ttl'] = 0; $fresh = $original; $count = count(DifyChatService::$calls); compactExpect($apply($fresh) && count(DifyChatService::$calls) === $count + 2, 'cache can be disabled'); $short = $original; $short['case_text'] = str_repeat('a', 120000); $copy = $short; compactExpect($apply($short) && $short === $copy, 'exact threshold keeps the source untouched'); echo "AI context compaction latency and isolation: OK\n"; }