feat: old channel

This commit is contained in:
hashbro
2026-10-07 05:19:52 +08:00
parent 2d3b6e1f2c
commit ba5d3c5731
30 changed files with 1517 additions and 896 deletions
+139
View File
@@ -0,0 +1,139 @@
<?php
namespace App\Services;
use App\Models\Channel;
use RuntimeException;
use ZipArchive;
class ChannelEmbedZipService
{
/**
* @return list<string>
*/
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"
."<script src=\"./index.js\"></script>\n\n"
."渠道 {$id} 已写入 index.js。iframe 投放仍可用原落地页链接。\n";
}
/**
* @return list<string>
*/
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;
}
}
+90 -41
View File
@@ -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<string>|null $channelIds null = all builder_type=old rows
* @return array{channels: list<array<string, mixed>>}
*/
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<string>|null $channelIds
* @return list<string>
*/
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];
}
}