5, 'models' => ['qwen' => ['name' => 'synthetic', 'label' => '合成模型']]]; } class DiagnosisLogic { public static function detail($params, $adminInfo): array { $GLOBALS['lifecycle'][] = 'diagnosis_loaded'; return ['id' => $params['id'], 'patient_id' => 99, 'gender' => '女', 'age' => 42]; } } class PatientAiReportLogic { public static function contextForAuthorizedDiagnosis($diagnosis, $adminId, $adminInfo): array { $GLOBALS['lifecycle'][] = 'aggregate'; if (!empty($GLOBALS['cancelInAggregate'])) $GLOBALS['lifecycleCancelled'] = true; return ['snapshot' => ['clinical_source' => '合成资料,无真实患者'], 'source_summary' => ['diagnosis_count' => 2, 'doctor_note_count' => 3, 'snapshot_complete' => true, 'may_be_truncated' => false], 'source_diagnosis_ids' => [11, 12], 'files' => []]; } } } namespace { require dirname(__DIR__) . '/vendor/autoload.php'; use app\adminapi\logic\tcm\DiagnosisAiLogic; use app\adminapi\logic\firstvisit\MyPatientLogic; function lifecycleExpect(bool $ok, string $message): void { if (!$ok) throw new RuntimeException($message); } $lifecycle = []; $lifecycleCancelled = false; $cancelInAggregate = false; $options = ['should_abort' => static fn (): bool => $GLOBALS['lifecycleCancelled'], 'on_authorized' => static function (array $metadata): bool { $GLOBALS['lifecycle'][] = 'start'; lifecycleExpect($metadata === ['diagnosis_id' => 11, 'task' => 'summary', 'model_key' => 'qwen', 'required_sections' => []], 'early start exposes authorized identity and no unaggregated section claims'); return true; }, 'on_progress' => static function (array $event): void { $GLOBALS['lifecycle'][] = $event; }]; $prepared = DiagnosisAiLogic::prepareAssistant(11, 'summary', '', 7, ['root' => 1], null, $options); lifecycleExpect(is_array($prepared), 'authorized synthetic preparation succeeds'); lifecycleExpect(array_slice($lifecycle, 0, 5) === ['scope_checked', 'diagnosis_loaded', 'start', ['phase' => 'preparing', 'status' => 'aggregating', 'message' => '正在聚合本次授权范围内的患者资料'], 'aggregate'], 'start and truthful preparation progress occur after scope but before aggregation'); lifecycleExpect($prepared['source_form'] === 'full_context' && $prepared['source_summary']['doctor_note_count'] === 3, 'real aggregate counts and full source form are preserved'); lifecycleExpect(end($lifecycle)['status'] === 'ready', 'ready occurs after successful aggregation'); $lifecycle = []; MyPatientLogic::$allowed = false; lifecycleExpect(DiagnosisAiLogic::prepareAssistant(11, 'summary', '', 7, ['root' => 1], null, $options) === null && $lifecycle === ['scope_checked'], 'denied DataScope produces no start/progress or aggregation'); lifecycleExpect(DiagnosisAiLogic::cancelAssistant(11, '12345678-1234-4234-8234-123456789abc', 7, ['root' => 1]) === null && DiagnosisAiLogic::getError() === '诊单不存在或无权访问', 'cancel has the same canonical DataScope restriction'); MyPatientLogic::$allowed = true; $lifecycle = []; $cancelInAggregate = true; lifecycleExpect(DiagnosisAiLogic::prepareAssistant(11, 'summary', '', 7, ['root' => 1], null, $options) === null && DiagnosisAiLogic::getAssistantErrorCode() === 'AI_ASSISTANT_CANCELLED', 'cancellation during aggregation blocks final prompt preparation'); lifecycleExpect(end($lifecycle) === 'aggregate', 'cancelled aggregation never claims prepared or submitted'); echo "Diagnosis AI assistant early start / DataScope / preparation lifecycle: OK\n"; }