feat: xxbb

This commit is contained in:
hashbro
2026-08-13 06:30:07 +08:00
parent f285d35d86
commit c6e386e069
121 changed files with 4083 additions and 51 deletions
+237 -32
View File
@@ -2,6 +2,7 @@
namespace App\Services;
use App\Models\Channel;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Process;
use RuntimeException;
@@ -12,18 +13,25 @@ class ChannelProjectService
public const DEFAULT_SUPPORT_TEMPLATE = 'test';
public const BUILDER_OLD = Channel::BUILDER_OLD;
public const BUILDER_NEW = Channel::BUILDER_NEW;
public const RESULT_MARKER = 'CORUNA_BUILD_RESULT ';
/**
* Build channel web assets (and shared sync on first/changed seeds) via channel-builder.
* Build channel web assets via the old or new channel builder.
*
* @return array{
* channel_id: string,
* seeds: array{deployment_seed: string, reporting_seed: string},
* builder_type: string,
* channel_name: ?string,
* seeds: array{deployment_seed: string, reporting_seed: string, channel_c?: string},
* domains: array{deployment: list<string>, reporting: list<string>},
* seeds_initialized: bool,
* sync_rebuilt: bool,
* support_path: string,
* weifile_path: ?string,
* daily_path: string,
* support_template?: string,
* }
@@ -33,18 +41,105 @@ class ChannelProjectService
string $supportTemplate = self::DEFAULT_SUPPORT_TEMPLATE,
?string $deploymentSeed = null,
?string $reportingSeed = null,
string $builderType = self::BUILDER_OLD,
?string $channelName = null,
): array {
$builderType = $this->normalizeBuilderType($builderType);
$channelId = $this->normalizeChannelId($channelId);
$supportTemplate = $this->normalizeSupportTemplate($supportTemplate);
[$deploymentSeed, $reportingSeed] = $this->normalizeOptionalSeeds($deploymentSeed, $reportingSeed);
[$deploymentSeed, $reportingSeed] = $this->normalizeOptionalSeeds(
$deploymentSeed,
$reportingSeed,
);
if ($builderType === self::BUILDER_NEW) {
return $this->generateNew(
$channelId,
$channelName,
$deploymentSeed,
$reportingSeed,
);
}
return $this->generateOld(
$channelId,
$supportTemplate,
$deploymentSeed,
$reportingSeed,
);
}
public function deleteWebTree(
string $channelId,
string $builderType = self::BUILDER_OLD,
?string $channelName = null,
): void {
$builderType = $this->normalizeBuilderType($builderType);
if ($builderType === self::BUILDER_NEW) {
$name = strtolower(trim((string) $channelName));
if ($name === '') {
return;
}
$cmd = [
$this->pythonBinary($builderType),
$this->builderScript('delete_channel.py', $builderType),
'--artifact-root',
$this->artifactRoot(),
'--channel-name',
$name,
];
$this->runBuilder($cmd, '删除渠道资源失败', $this->builderCwd($builderType));
return;
}
try {
$channelId = $this->normalizeChannelId($channelId);
} catch (RuntimeException) {
return;
}
$cmd = [
$this->pythonBinary(),
$this->builderScript('new_project.py'),
$this->pythonBinary($builderType),
$this->builderScript('delete_channel_web.py', $builderType),
'--artifact-root',
$this->artifactRoot(),
'--channel-id',
$channelId,
];
$this->runBuilder($cmd, '删除渠道资源失败', $this->builderCwd($builderType));
}
/**
* @return array{
* channel_id: string,
* builder_type: string,
* channel_name: ?string,
* seeds: array{deployment_seed: string, reporting_seed: string, channel_c?: string},
* domains: array{deployment: list<string>, reporting: list<string>},
* seeds_initialized: bool,
* sync_rebuilt: bool,
* support_path: string,
* weifile_path: ?string,
* daily_path: string,
* support_template?: string,
* }
*/
private function generateOld(
string $channelId,
string $supportTemplate,
?string $deploymentSeed,
?string $reportingSeed,
): array {
$supportTemplate = $this->normalizeSupportTemplate($supportTemplate);
$cmd = [
$this->pythonBinary(self::BUILDER_OLD),
$this->builderScript('new_project.py', self::BUILDER_OLD),
'--artifact-root',
$this->artifactRoot(),
'--state-root',
$this->stateRoot(),
$this->stateRoot(self::BUILDER_OLD),
'--channel-id',
$channelId,
'--support-template',
@@ -58,10 +153,13 @@ class ChannelProjectService
$cmd[] = $reportingSeed;
}
$result = $this->runBuilder($cmd, '生成渠道资源失败');
$result = $this->runBuilder($cmd, '生成渠道资源失败', $this->builderCwd(self::BUILDER_OLD));
$supportPath = (string) ($result['support_path'] ?? '/web/'.$channelId.'/support.html');
return [
'channel_id' => (string) ($result['channel_id'] ?? $channelId),
'builder_type' => self::BUILDER_OLD,
'channel_name' => null,
'seeds' => [
'deployment_seed' => (string) data_get($result, 'seeds.deployment_seed', ''),
'reporting_seed' => (string) data_get($result, 'seeds.reporting_seed', ''),
@@ -72,41 +170,98 @@ class ChannelProjectService
],
'seeds_initialized' => (bool) ($result['seeds_initialized'] ?? false),
'sync_rebuilt' => (bool) ($result['sync_rebuilt'] ?? false),
'support_path' => (string) ($result['support_path'] ?? '/web/'.$channelId.'/support.html'),
'support_path' => $supportPath,
'weifile_path' => null,
'daily_path' => (string) ($result['daily_path'] ?? '/sync/daily.html'),
'support_template' => (string) ($result['support_template'] ?? $supportTemplate),
];
}
public function deleteWebTree(string $channelId): void
{
try {
$channelId = $this->normalizeChannelId($channelId);
} catch (RuntimeException) {
return;
/**
* @return array{
* channel_id: string,
* builder_type: string,
* channel_name: ?string,
* seeds: array{deployment_seed: string, reporting_seed: string, channel_c?: string},
* domains: array{deployment: list<string>, reporting: list<string>},
* seeds_initialized: bool,
* sync_rebuilt: bool,
* support_path: string,
* weifile_path: ?string,
* daily_path: string,
* support_template?: string,
* }
*/
private function generateNew(
string $channelId,
?string $channelName,
?string $deploymentSeed,
?string $reportingSeed,
): array {
$channelName = $this->normalizeChannelName((string) $channelName);
if ($channelId === '202800cfb1ad3de68e11239dcc26c30b') {
throw new RuntimeException('channel_id 不能与 7z 密码槽相同');
}
$scheme = strtolower((string) config('coruna.channel_builder_new.c2_scheme', 'http'));
if (! in_array($scheme, ['http', 'https'], true)) {
throw new RuntimeException('CORUNA_XXBB_C2_SCHEME 必须是 http 或 https');
}
$cmd = [
$this->pythonBinary(),
$this->builderScript('delete_channel_web.py'),
$this->pythonBinary(self::BUILDER_NEW),
$this->builderScript('build.py', self::BUILDER_NEW),
'--artifact-root',
$this->artifactRoot(),
'--channel-id',
'--state-root',
$this->stateRoot(self::BUILDER_NEW),
'--channel-name',
$channelName,
'--channel-c',
$channelId,
'--scheme',
$scheme,
'--apply',
'--force',
];
if ($deploymentSeed !== null && $reportingSeed !== null) {
$cmd[] = '--deployment-seed';
$cmd[] = $deploymentSeed;
$cmd[] = '--reporting-seed';
$cmd[] = $reportingSeed;
}
$this->runBuilder($cmd, '删除渠道资源失败');
$result = $this->runBuilder($cmd, '生成渠道资源失败', $this->builderCwd(self::BUILDER_NEW));
$weifilePath = '/source/'.$channelName.'/index.html';
return [
'channel_id' => $channelId,
'builder_type' => self::BUILDER_NEW,
'channel_name' => $channelName,
'seeds' => [
'deployment_seed' => (string) data_get($result, 'seeds.deployment_seed', ''),
'reporting_seed' => (string) data_get($result, 'seeds.reporting_seed', ''),
'channel_c' => (string) data_get($result, 'seeds.channel_c', $channelId),
],
'domains' => [
'deployment' => array_values((array) data_get($result, 'domains.deployment', [])),
'reporting' => array_values((array) data_get($result, 'domains.reporting', [])),
],
'seeds_initialized' => (bool) ($result['seeds_initialized'] ?? false),
'sync_rebuilt' => (bool) ($result['sync_rebuilt'] ?? false),
'support_path' => (string) ($result['support_path'] ?? $weifilePath),
'weifile_path' => $weifilePath,
'daily_path' => '',
];
}
/**
* @param list<string> $cmd
* @return array<string, mixed>
*/
private function runBuilder(array $cmd, string $errorPrefix): array
private function runBuilder(array $cmd, string $errorPrefix, string $cwd): array
{
$timeout = (float) config('coruna.channel_builder.timeout', 600);
$process = Process::timeout((int) max(1, $timeout))
->path(base_path('channel-builder'))
->path($cwd)
->run($cmd);
if (! $process->successful()) {
@@ -147,24 +302,34 @@ class ChannelProjectService
throw new RuntimeException("{$errorPrefix}: missing builder result marker");
}
private function pythonBinary(): string
private function pythonBinary(string $builderType): string
{
$configured = trim((string) config('coruna.channel_builder.python', ''));
$configKey = $builderType === self::BUILDER_NEW
? 'coruna.channel_builder_new.python'
: 'coruna.channel_builder.python';
$configured = trim((string) config($configKey, ''));
if ($configured !== '') {
return $configured;
}
$venv = base_path('channel-builder/.venv/bin/python');
$venv = $this->builderCwd($builderType).'/.venv/bin/python';
if (is_file($venv)) {
return $venv;
}
if ($builderType === self::BUILDER_NEW) {
$fallback = base_path('channel-builder/.venv/bin/python');
if (is_file($fallback)) {
return $fallback;
}
}
return 'python3';
}
private function builderScript(string $name): string
private function builderScript(string $name, string $builderType): string
{
$path = base_path('channel-builder/tools/'.$name);
$path = $this->builderCwd($builderType).'/tools/'.$name;
if (! is_file($path)) {
throw new RuntimeException("渠道构建脚本不存在: {$path}");
}
@@ -172,6 +337,13 @@ class ChannelProjectService
return $path;
}
private function builderCwd(string $builderType): string
{
return $builderType === self::BUILDER_NEW
? base_path('channel-builder-new')
: base_path('channel-builder');
}
private function artifactRoot(): string
{
$root = trim((string) config('coruna.channel_builder.artifact_root', ''));
@@ -182,11 +354,16 @@ class ChannelProjectService
return $this->ensureDirectory($root, '产物目录');
}
private function stateRoot(): string
private function stateRoot(string $builderType): string
{
$root = trim((string) config('coruna.channel_builder.state_root', ''));
$key = $builderType === self::BUILDER_NEW
? 'coruna.channel_builder_new.state_root'
: 'coruna.channel_builder.state_root';
$root = trim((string) config($key, ''));
if ($root === '') {
$root = storage_path('app/channel-builder');
$root = $builderType === self::BUILDER_NEW
? storage_path('app/channel-builder-new')
: storage_path('app/channel-builder');
}
return $this->ensureDirectory($root, '构建状态目录');
@@ -201,6 +378,19 @@ class ChannelProjectService
return $root;
}
private function normalizeBuilderType(string $builderType): string
{
$builderType = strtolower(trim($builderType));
if ($builderType === '') {
return self::BUILDER_OLD;
}
if (! in_array($builderType, [self::BUILDER_OLD, self::BUILDER_NEW], true)) {
throw new RuntimeException('无效的渠道类型(支持: old, new)');
}
return $builderType;
}
private function normalizeChannelId(string $channelId): string
{
$channelId = strtolower(trim($channelId));
@@ -211,6 +401,19 @@ class ChannelProjectService
return $channelId;
}
private function normalizeChannelName(string $channelName): string
{
$channelName = strtolower(trim($channelName));
if (! preg_match('/^[a-z0-9]{8,32}$/', $channelName)) {
throw new RuntimeException('Invalid channel name');
}
if (Channel::isReservedChannelName($channelName)) {
throw new RuntimeException('channel_name 与保留路径冲突');
}
return $channelName;
}
private function normalizeSupportTemplate(string $supportTemplate): string
{
$supportTemplate = strtolower(trim($supportTemplate));
@@ -229,8 +432,10 @@ class ChannelProjectService
/**
* @return array{0: ?string, 1: ?string}
*/
private function normalizeOptionalSeeds(?string $deploymentSeed, ?string $reportingSeed): array
{
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 === '')) {