90, 'models' => [ 'qwen' => ['name' => 'r37-local-qwen', 'label' => 'R37 local Qwen'], 'openai' => ['name' => 'r37-local-openai', 'label' => 'R37 local OpenAI'], ]]; } class DiagnosisLogic { public static function detail($params, $adminInfo): array { return $GLOBALS['r37Current']; } } class PatientAiReportLogic { public static function contextForAuthorizedDiagnosis($diagnosis, $adminId, $adminInfo): array { return $GLOBALS['r37Context']; } public static function compactSourceForPrompt(...$args): array { throw new \RuntimeException('Unexpected compaction; finite source is below the existing budget.'); } } } namespace app\common\service { class DifyChatService { public static array $calls = []; public static array $responses = []; public static int $totalCalls = 0; public static bool $streaming = false; private static function take(): array { if (self::$responses === []) throw new \RuntimeException('Unexpected extra local transport invocation.'); return array_shift(self::$responses); } public static function chat($profile, $inputs, $query, $user, $files = [], $options = []): array { ++self::$totalCalls; self::$calls[] = compact('profile', 'inputs', 'query', 'user', 'files', 'options') + ['transport' => 'chat']; return self::take(); } public static function streamChat($profile, $inputs, $query, $user, $delta, $abort, $files = [], $options = []): array { ++self::$totalCalls; self::$calls[] = compact('profile', 'inputs', 'query', 'user', 'files', 'options') + ['transport' => 'stream']; $result = self::take(); if (empty($result['ok'])) return $result; self::$streaming = true; try { foreach (mb_str_split($result['content'], 19, 'UTF-8') as $chunk) { if (($abort !== null && $abort()) || $delta($chunk) === false) { return ['ok' => false, 'error_code' => 'AI_ASSISTANT_CANCELLED']; } } } finally { self::$streaming = false; } return $result; } } } namespace { require dirname(__DIR__) . '/vendor/autoload.php'; use app\adminapi\logic\tcm\DiagnosisAiLogic; use app\adminapi\service\AiRequiredSource; use app\adminapi\service\TcmEvidenceGuard; use app\adminapi\service\TcmClinicalFactStateGuard; use app\common\service\DifyChatService; $checks = []; function r37Check(bool $passed, string $name): void { $GLOBALS['checks'][] = compact('passed', 'name'); } function r37Reset(array $responses): void { DifyChatService::$calls = []; DifyChatService::$responses = $responses; } function r37Fixture(string $variant): void { $GLOBALS['r37Current'] = [ 'id' => 37, 'patient_id' => 999, 'diagnosis_date' => '2026-10-26', 'age' => 46, 'chief_complaint' => $variant === 'tongue_missing' ? '口干两周。' : '未记录', 'tongue' => $variant === 'symptoms_missing' ? '舌淡红,苔薄白。' : '未记录', 'pulse' => '脉细', 'allergy_history' => '不详', 'past_history' => '2020年外院糖尿病诊断记录。', 'current_medications' => '二甲双胍500mg,每日2次。', 'fasting_blood_sugar' => 7.9, 'systolic_pressure' => 132, 'diastolic_pressure' => 82, ]; $sections = []; $metadata = []; foreach (AiRequiredSource::LABELS as $key => $label) { $value = [['marker' => 'R37_FULL_SOURCE_' . $key]]; if ($key === 'diagnosis_info') $value[] = $GLOBALS['r37Current']; if ($key === 'prescriptions') $value[] = ['prescription_date' => '2020-02-01', 'prescription_name' => '历史用药记录', 'usage' => '格列美脲已停用。']; if ($key === 'daily_tracking') $value[] = ['record_date' => '2026-10-25', 'fasting_blood_sugar' => 8.3, 'note' => '晚餐主食较多,晚饭后步行20分钟。']; if ($key === 'chat') $value[] = ['msg_time' => '2020-02-01', 'text' => '历史自述口渴,非本次采集。']; $sections[] = compact('key', 'label', 'value'); $metadata[] = ['key' => $key, 'label' => $label, 'availability' => 'present', 'count' => count($value), 'status' => 'prepared']; } $GLOBALS['r37Context'] = [ 'snapshot' => ['required_content' => $sections], 'source_summary' => ['required_sections' => $metadata, 'snapshot_complete' => true], 'source_diagnosis_ids' => [12, 37], 'files' => [['type' => 'image', 'url' => '/offline-r37/attachment.png']], ]; } // Namespace transport/auth/context helpers reused from the independently saved // source_ids_repro.php. New content-contract expectations below are root-authored. $checks = []; $responses = []; $ids = ['f1', 'f1, f2', "f1\nf2", 'f1、f2;f3', '["f1","f2"]', "```json\n[\"f1\",\"f2\"]\n```", "```text\nf1, f2\n```", "\xEF\xBB\xBFf1 f2"]; foreach ($ids as $text) { r37Check(TcmEvidenceGuard::isSourceIdListOnly($text), 'closed ID-only shape recognized: '.json_encode($text)); } r37Fixture('both_missing'); foreach (['case_analysis','glucose_trend','medication_review','complication_risk','summary','lifestyle_advice'] as $task) { foreach (['ordinary','stream'] as $mode) { r37Reset([['ok'=>true,'content'=>'f1, f2']]); $visible=''; $p=DiagnosisAiLogic::prepareAssistant(37,$task,'请分析已有资料及其局限。',7,[]); $result=$mode==='ordinary' ? DiagnosisAiLogic::assistant(37,$task,'请分析已有资料及其局限。',7,[]) : DiagnosisAiLogic::streamPreparedAssistant($p,static function($delta)use(&$visible){$visible.=$delta;return true;}); r37Check($result===null && $visible==='' && DiagnosisAiLogic::getAssistantErrorCode()==='TASK_ANALYSIS_MISSING' && count(DifyChatService::$calls)===1, "$task/$mode rejects exact independent counterexample honestly"); } } foreach ($ids as $text) foreach (['ordinary','stream'] as $mode) { r37Reset([['ok'=>true,'content'=>$text]]);$visible=''; $p=DiagnosisAiLogic::prepareAssistant(37,'glucose_trend','请分析已有血糖记录及局限。',7,[]); $result=$mode==='ordinary' ? DiagnosisAiLogic::assistant(37,'glucose_trend','请分析已有血糖记录及局限。',7,[]) : DiagnosisAiLogic::streamPreparedAssistant($p,static function($delta)use(&$visible){$visible.=$delta;return true;}); r37Check($result===null && $visible==='' && DiagnosisAiLogic::getAssistantErrorCode()==='TASK_ANALYSIS_MISSING', "$mode ID-only variant rejected before output: ".json_encode($text)); $guard=TcmEvidenceGuard::assess($r37Current,$r37Context['snapshot']); $review=TcmEvidenceGuard::review($guard,$text); r37Check(!$review['valid'] && $review['answer']==='' && in_array('TASK_ANALYSIS_MISSING',array_column($review['issues'],'code'),true), 'compatibility review does not manufacture ID-only analysis'); } foreach (['当前分析引用资料编号f1和f2;既往记录与本次判断保留时点限制。', 'f1, f2仅为引文编号,已有血糖记录的采样条件仍待核实。'] as $text) foreach (['ordinary','stream'] as $mode) { r37Reset([['ok'=>true,'content'=>$text]]);$visible=''; $p=DiagnosisAiLogic::prepareAssistant(37,'glucose_trend','请分析已有资料及局限。',7,[]); $result=$mode==='ordinary' ? DiagnosisAiLogic::assistant(37,'glucose_trend','请分析已有资料及局限。',7,[]) : DiagnosisAiLogic::streamPreparedAssistant($p,static function($delta)use(&$visible){$visible.=$delta;return true;}); r37Check(is_array($result) && $result['answer']===$text && ($mode==='ordinary'||$visible===$text), "$mode actual prose containing source IDs remains byte-identical"); } $analysis='可比较记录\n2026-10-25空腹血糖8.3 mmol/L,2026-10-26为7.9 mmol/L;两个记录点不能代表长期趋势。'; $analysis=str_replace('\\n',"\n",$analysis); foreach (['f11, f23', '{"fact_ids":["f11","f23"]}', '["f11","f23"]'] as $citation) { foreach ([$citation."\n\n".$analysis,$analysis."\n\n".$citation] as $text) { foreach (['ordinary','stream'] as $mode) { r37Reset([['ok'=>true,'content'=>$text]]);$visible=''; $p=DiagnosisAiLogic::prepareAssistant(37,'glucose_trend','请分析已有记录和局限。',7,[]); $result=$mode==='ordinary'?DiagnosisAiLogic::assistant(37,'glucose_trend','请分析已有记录和局限。',7,[]) :DiagnosisAiLogic::streamPreparedAssistant($p,static function($d)use(&$visible){$visible.=$d;return true;}); r37Check(is_array($result)&&$result['answer']===$text&&($mode==='ordinary'||$visible===$text), "$mode complete analysis with leading/trailing citation paragraph preserved"); } } } foreach (["f11, f23\n\nf24, f25", "{\"fact_ids\":[\"f11\"]}\n\n[\"f23\"]"] as $text) { foreach (['ordinary','stream'] as $mode) { r37Reset([['ok'=>true,'content'=>$text]]);$visible=''; $p=DiagnosisAiLogic::prepareAssistant(37,'glucose_trend','请分析已有记录和局限。',7,[]); $result=$mode==='ordinary'?DiagnosisAiLogic::assistant(37,'glucose_trend','请分析已有记录和局限。',7,[]) :DiagnosisAiLogic::streamPreparedAssistant($p,static function($d)use(&$visible){$visible.=$d;return true;}); r37Check($result===null&&$visible===''&&DiagnosisAiLogic::getAssistantErrorCode()==='TASK_ANALYSIS_MISSING', "$mode source-only multi-paragraph response not delivered"); } } foreach (['模型拒绝后只返回{"fact_ids":[]}。','资料处理仅输出{"fact_ids":["f1"]}。'] as $text) foreach (['ordinary','stream'] as $mode) { r37Reset([['ok'=>true,'content'=>$text]]);$visible=''; $p=DiagnosisAiLogic::prepareAssistant(37,'glucose_trend','请分析已有记录和局限。',7,[]); $result=$mode==='ordinary'?DiagnosisAiLogic::assistant(37,'glucose_trend','请分析已有记录和局限。',7,[]) :DiagnosisAiLogic::streamPreparedAssistant($p,static function($d)use(&$visible){$visible.=$d;return true;}); r37Check($result===null&&$visible===''&&DiagnosisAiLogic::getAssistantErrorCode()==='TASK_ANALYSIS_MISSING', "$mode exclusive protocol-only wrapper is not analysis"); } echo json_encode(['suite'=>'RootR37SourceIdAnalysisContent','passed'=>count(array_filter($checks,fn($c)=>$c['passed'])), 'total'=>count($checks),'checks'=>$checks,'transport_calls'=>DifyChatService::$totalCalls, 'live_model_calls'=>0,'network_calls'=>0],JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT)."\n"; exit(in_array(false,array_column($checks,'passed'),true)?1:0); }