strcmp(self::canonical($a), self::canonical($b))); } return $value; } public static function fingerprint(array $row): string { return hash('sha256', self::canonical(self::clinical($row))); } public static function isManual(array $row): bool { return (int) ($row['id'] ?? 0) > 0 && empty($row['delete_time']) && (int) ($row['void_status'] ?? 0) === 0 && (int) ($row['is_system_auto'] ?? 0) === 0 && self::decode($row['herbs'] ?? []) !== []; } public static function aggregate(array $states): string { if ($states === []) { return 'preparing'; } if (array_intersect($states, self::ACTIVE_TASKS) !== []) { return 'running'; } $success = count(array_filter($states, static fn ($v): bool => $v === 'success')); if ($success === 2) { return 'success'; } if ($success > 0) { return 'partial'; } return count(array_filter($states, static fn ($v): bool => $v === 'cancelled')) === count($states) ? 'cancelled' : 'failed'; } public static function retryAt(int $attempts, int $now, bool $retryable, int $maxAttempts): ?int { return $retryable && $attempts < $maxAttempts ? $now + ($attempts <= 1 ? 30 : 120) : null; } public static function errorMessage(string $code): string { return match ($code) { 'PATIENT_BINDING_REQUIRED' => '需完善患者与诊单关联', 'ACCESS_REVOKED' => '资料访问权限已变更', 'SOURCE_CHANGED' => '资料或处方已更新,请查看新版本', 'BUDGET_PAUSED' => '已达到分析预算,等待额度恢复', 'CONFIG_INVALID', 'CONFIG_DISABLED', 'UPSTREAM_AUTH_FAILED' => '模型配置不可用,请联系管理员', 'UPSTREAM_TIMEOUT' => '模型响应超时,可稍后重试', 'INVALID_RESPONSE', 'RESPONSE_INVALID', 'INVALID_EVIDENCE_OUTPUT', 'INVALID_FILE_EVIDENCE_OUTPUT', 'INVALID_REPORT_OUTPUT' => '模型返回内容未通过校验', 'CONTEXT_TOO_LARGE', 'INPUT_TOKEN_BUDGET_EXCEEDED', 'SYNTHESIS_BUDGET_EXCEEDED' => '资料超过本次处理预算', 'FINAL_CONTEXT_EXCEEDS_BUDGET' => '资料来源与缺口说明超过汇总预算,请联系管理员调整处理配置', 'LEASE_EXPIRED' => '工作进程中断,任务等待恢复', default => '分析暂未完成,可查看资料缺口或重试', }; } }