diff --git a/app/Http/Controllers/Hooks/TokenviewWebhookController.php b/app/Http/Controllers/Hooks/TokenviewWebhookController.php
index bfe1c66..613523e 100644
--- a/app/Http/Controllers/Hooks/TokenviewWebhookController.php
+++ b/app/Http/Controllers/Hooks/TokenviewWebhookController.php
@@ -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,
+ ]);
}
}
diff --git a/app/Models/Channel.php b/app/Models/Channel.php
index 7f353ad..e173cfb 100644
--- a/app/Models/Channel.php
+++ b/app/Models/Channel.php
@@ -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 '';
- }
-
- 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 '';
}
public static function randomChannelId(): string
diff --git a/app/Services/ChannelProjectService.php b/app/Services/ChannelProjectService.php
index b0afd75..d05dde3 100644
--- a/app/Services/ChannelProjectService.php
+++ b/app/Services/ChannelProjectService.php
@@ -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|null $channelIds null = all builder_type=new rows
+ * @return array{shared: array, channels: list>}
+ */
+ 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|null $channelIds
+ * @return list
+ */
+ 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';
diff --git a/channel-builder-new/README.md b/channel-builder-new/README.md
index a70b3cf..b2096c6 100644
--- a/channel-builder-new/README.md
+++ b/channel-builder-new/README.md
@@ -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`
diff --git a/channel-builder-new/tools/pack_channel.py b/channel-builder-new/tools/pack_channel.py
index 0ad3088..3feabe0 100644
--- a/channel-builder-new/tools/pack_channel.py
+++ b/channel-builder-new/tools/pack_channel.py
@@ -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()
diff --git a/channel-builder/tools/new_project.py b/channel-builder/tools/new_project.py
index cfb4087..9b120b2 100644
--- a/channel-builder/tools/new_project.py
+++ b/channel-builder/tools/new_project.py
@@ -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 "
diff --git a/docs/BAOTA_DEPLOY.md b/docs/BAOTA_DEPLOY.md
index a3b4462..3d7a279 100644
--- a/docs/BAOTA_DEPLOY.md
+++ b/docs/BAOTA_DEPLOY.md
@@ -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` |
diff --git a/resources/views/admin/channels/index.blade.php b/resources/views/admin/channels/index.blade.php
index 3c17d61..fc1ca83 100644
--- a/resources/views/admin/channels/index.blade.php
+++ b/resources/views/admin/channels/index.blade.php
@@ -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 '