*/ public const RESERVED_CHANNEL_NAMES = [ 'admin', 'user', 'api', 'web', 'sync', 'details', 'weifile', 'hooks', 'link', 'statistic', 'vhx', 'event', 'log', 'storage', 'build', 'hot', 'vendor', 'css', 'js', 'up', 'index', 'assets', 'static', 'source', 'channel', 'out', 't', 'nb', 'a', 'u', 'uj', 'us', 'ub', 'ba', 'result', 'favicon', 'robots', 'sitemap', 'public', 'app', 'bootstrap', 'config', 'database', 'resources', 'routes', 'tests', 'artisan', 'livewire', 'sanctum', 'telescope', 'horizon', 'pulse', ]; protected $fillable = [ 'channel_id', 'builder_type', 'user_id', 'domains', 'status', 'remark', ]; protected $attributes = [ 'builder_type' => self::BUILDER_OLD, ]; protected function casts(): array { return [ 'domains' => 'array', 'status' => 'integer', 'user_id' => 'integer', ]; } public function user(): BelongsTo { return $this->belongsTo(User::class, 'user_id'); } public function isEnabled(): bool { return (int) $this->status === 1; } public function isOfficial(): bool { return (int) $this->user_id === self::OFFICIAL_USER_ID; } public function isNewBuilder(): bool { return $this->builderType() === self::BUILDER_NEW; } public function builderType(): string { $type = strtolower(trim((string) ($this->builder_type ?? ''))); return $type === self::BUILDER_NEW ? self::BUILDER_NEW : self::BUILDER_OLD; } public function agentLabel(): string { if ($this->isOfficial()) { return '官方'; } return $this->user?->username ?: ('#'.$this->user_id); } /** * Domains for support-link display. * Channel row wins; otherwise CORUNA_LAB_CHANNEL_DOMAINS (system settings). * * @return list */ public function resolvedDomains(): array { $stored = $this->normalizeDomainList($this->domains); if ($stored !== []) { return $stored; } return $this->normalizeDomainList(config('coruna.channel_domains', [])); } /** * @param mixed $domains * @return list */ public static function normalizeDomainList(mixed $domains): array { if (! is_array($domains)) { return []; } return array_values(array_unique(array_filter(array_map(static function ($d) { if (! is_string($d)) { return ''; } $d = trim($d); $d = preg_replace('#^https?://#i', '', $d) ?? $d; return rtrim($d, '/'); }, $domains)))); } public function landingPath(): string { if ($this->isNewBuilder()) { return '/weifile/weifile.html'; } return '/web/'.$this->channel_id.'/support.html'; } /** * @return list */ public function supportLinks(): array { if ($this->isNewBuilder()) { return []; } $links = []; $path = $this->landingPath(); $scheme = trim((string) config('coruna.static_site.scheme', 'https')) ?: 'https'; foreach ($this->resolvedDomains() as $domain) { $origin = rtrim($domain, '/'); if ($origin === '') { continue; } if (! preg_match('#^https?://#i', $origin)) { $origin = $scheme.'://'.$origin; } $links[] = $origin.$path; } return array_values(array_unique($links)); } public static function randomChannelId(): string { return bin2hex(random_bytes(16)); } public static function normalizeNewChannelId(string $channelId): ?string { $channelId = strtoupper(trim($channelId)); if (! preg_match('/^[A-Z0-9]{'.self::NEW_CHANNEL_ID_LENGTH.'}$/', $channelId)) { return null; } if (self::isReservedChannelName($channelId)) { return null; } return $channelId; } public static function randomNewChannelId(): string { for ($i = 0; $i < 32; $i++) { $code = strtoupper(substr(bin2hex(random_bytes(5)), 0, self::NEW_CHANNEL_ID_LENGTH)); if (self::normalizeNewChannelId($code) === null) { continue; } if (self::query()->whereRaw('upper(channel_id) = ?', [$code])->exists()) { continue; } return $code; } throw new RuntimeException('无法生成唯一渠道 ID'); } public function sourceZipRelativePath(): string { $dir = trim((string) config('coruna.channel_builder_new.source_dir', 'channel-source-new'), '/'); return $dir.'/'.$this->channel_id.'.zip'; } public static function isReservedChannelName(string $name): bool { return in_array(strtolower($name), self::RESERVED_CHANNEL_NAMES, true); } public static function maxPerAgent(): int { $n = (int) config('coruna.channels.max_per_agent', 5); return max(1, $n); } }