['summary' => true, 'diagnosis' => true, 'treatment_advice' => true, 'risk_assessment' => ['*' => ['label' => true, 'level' => true, 'evidence_references' => ['*' => true]]], 'evidence_references' => ['*' => true], 'missing_information' => ['*' => true]], 'candidate' => ['status' => true, 'reason' => true, 'prescription_name' => true, 'prescription_type' => true, 'dose_basis' => true, 'usage_instruction' => true, 'times_per_day' => true, 'usage_days' => true, 'rationale' => true, 'risk_warnings' => ['*' => true], 'evidence_references' => ['*' => true], 'herbs' => ['*' => ['name' => true, 'dosage' => true, 'unit' => true, 'dose_basis' => true, 'processing' => true, 'formula_type' => true, 'instructions' => true, 'evidence_references' => ['*' => true]]]], 'coverage' => ['status' => true, 'complete' => true, 'source_complete' => true, 'source_ids' => ['*' => true], 'token_budget_method' => true, 'clinical_interpretation_verified' => true, 'missing' => ['*' => ['source_id' => true, 'code' => true, 'critical' => true]], 'files' => ['*' => ['file_id' => true, 'type' => true, 'status' => true, 'transmitted' => true, 'version_verified' => true, 'reason' => true]]], 'usage' => ['total_calls' => true, 'calls' => ['*' => ['stage' => true, 'input_hash' => true, 'latency_ms' => true, 'ok' => true, 'file_count' => true, 'input_token_upper_bound' => true, 'error_code' => true, 'usage' => ['prompt_tokens' => true, 'completion_tokens' => true, 'total_tokens' => true]]]], 'knowledge_source' => ['title' => true, 'url' => true, 'path' => true, 'commit' => true, 'sections' => ['*' => true]], 'ancient_book_sources' => ['*' => ['title' => true, 'chapter' => true, 'excerpt' => true, 'path' => true, 'line' => true, 'commit' => true, 'url' => true]], ]; public static function capabilities(int $adminId): array { $actor = PrescriptionAiAccess::actor($adminId); if ($actor === null) { throw new DomainException('账号已停用或无权访问'); } return ['agreement_visible' => self::visibleForActor($adminId), 'can_manage_visibility' => PrescriptionAiAccess::allowed($adminId, $actor, 'manageVisibility')]; } public static function agreementVisible(int $adminId): bool { return PrescriptionAiAccess::actor($adminId) !== null && self::visibleForActor($adminId); } public static function getConfig(int $adminId): array { self::requireManage($adminId); return self::loadPolicy(); } public static function setConfig(array $params, int $adminId): array { self::requireManage($adminId); $policy = self::normalizePolicy($params); foreach (['role_ids' => 'system_role', 'admin_ids' => 'admin'] as $field => $table) { if ($policy[$field] === []) { continue; } $query = Db::name($table)->whereIn('id', $policy[$field])->whereNull('delete_time'); if ($table === 'admin') { $query->where('disable', 0); } if ((int) $query->count() !== count($policy[$field])) { throw new DomainException($table === 'admin' ? '授权用户不存在或已停用,请移除失效用户' : '授权角色不存在,请移除失效角色'); } } // Store one object so readers never observe half of a role/user policy update. ConfigService::set(self::CONFIG_TYPE, self::CONFIG_NAME, $policy); return $policy; } public static function options(int $adminId): array { self::requireManage($adminId); // The endpoint is intentionally bounded. Never silently truncate an authorization selector. $roles = Db::name('system_role')->whereNull('delete_time')->field('id,name')->order('id') ->limit(self::MAX_ROLES + 1)->select()->toArray(); $admins = Db::name('admin')->whereNull('delete_time')->where('disable', 0)->field('id,name,account')->order('id') ->limit(self::MAX_ADMINS + 1)->select()->toArray(); if (count($roles) > self::MAX_ROLES || count($admins) > self::MAX_ADMINS) { throw new DomainException('授权选项超过上限(500个角色、5000个用户),请联系管理员'); } return ['roles' => array_map(static fn (array $r): array => ['id' => (int) $r['id'], 'name' => (string) $r['name']], $roles), 'admins' => array_map(static fn (array $a): array => ['id' => (int) $a['id'], 'name' => (string) $a['name'], 'account' => (string) $a['account']], $admins)]; } public static function normalizePolicy(array $params): array { if (array_diff(array_keys($params), ['role_ids', 'admin_ids']) !== [] || !array_key_exists('role_ids', $params) || !array_key_exists('admin_ids', $params)) { throw new DomainException('授权配置须包含且仅包含角色和用户列表'); } return ['role_ids' => self::ids($params['role_ids'], self::MAX_ROLES), 'admin_ids' => self::ids($params['admin_ids'], self::MAX_ADMINS)]; } /** Build the response from known saved-body fields, never merge an arbitrary decrypted object. */ public static function reportBody(array $body, bool $agreementVisible): array { $projected = self::projectValue($body, self::REPORT_BODY_SCHEMA); if ($agreementVisible && array_key_exists('comparison', $body)) { $projected['comparison'] = $body['comparison']; } return $projected; } public static function projectBatch(array $batch, bool $agreementVisible): array { if (!$agreementVisible) { $batch = self::withoutComparison($batch); foreach ($batch['models'] ?? [] as $key => $model) { // A failed task's reason is a generation error; a successful result's reason is a comparison. if (($model['error_code'] ?? '') === '') { unset($batch['models'][$key]['reason']); } } } $batch['agreement_visible'] = $agreementVisible; return $batch; } private static function withoutComparison(array $value): array { foreach ($value as $key => $item) { if (in_array($key, self::COMPARISON_FIELDS, true)) { unset($value[$key]); } elseif (is_array($item)) { $value[$key] = self::withoutComparison($item); } } return $value; } private static function projectValue($value, $schema) { if ($schema === true) { return is_scalar($value) || $value === null ? $value : null; } if (!is_array($value)) { return null; } if (array_key_exists('*', $schema)) { return array_map(static fn ($item) => self::projectValue($item, $schema['*']), array_values($value)); } $result = []; foreach ($schema as $key => $shape) { if (array_key_exists($key, $value)) { $result[$key] = self::projectValue($value[$key], $shape); } } return $result; } private static function requireManage(int $adminId): void { $actor = PrescriptionAiAccess::actor($adminId); if ($actor === null || !PrescriptionAiAccess::allowed($adminId, $actor, 'manageVisibility')) { throw new DomainException('无权管理处方AI一致度授权'); } } private static function visibleForActor(int $adminId): bool { $policy = self::loadPolicy(); if (in_array($adminId, $policy['admin_ids'], true)) { return true; } if ($policy['role_ids'] === []) { return false; } // Read current membership and active roles, never the login token's role snapshot. return Db::name('admin_role')->alias('ar')->join('system_role r', 'r.id=ar.role_id') ->where('ar.admin_id', $adminId)->whereIn('r.id', $policy['role_ids'])->whereNull('r.delete_time')->count() > 0; } private static function loadPolicy(): array { $empty = ['role_ids' => [], 'admin_ids' => []]; $stored = ConfigService::get(self::CONFIG_TYPE, self::CONFIG_NAME, $empty); if (!is_array($stored)) { return $empty; } try { return self::normalizePolicy($stored); } catch (DomainException $e) { return $empty; } } private static function ids($raw, int $max): array { if (!is_array($raw) || !array_is_list($raw) || count($raw) > $max) { throw new DomainException('授权列表格式或数量无效'); } $ids = []; foreach ($raw as $id) { if ((!is_int($id) && !is_string($id)) || !preg_match('/^[1-9]\d{0,17}$/D', (string) $id)) { throw new DomainException('授权标识须为有效正整数'); } $ids[] = (int) $id; } $ids = array_values(array_unique($ids)); sort($ids, SORT_NUMERIC); return $ids; } }