From 6489808fa597859ef6e3e4b3383a1ab95a62c30a Mon Sep 17 00:00:00 2001 From: hashbro Date: Sun, 9 Aug 2026 02:42:28 +0800 Subject: [PATCH] admin --- .../Controllers/Admin/ChannelController.php | 4 +- .../Hooks/TelegramWebhookController.php | 63 +++++++++++++------ app/Services/ChannelProjectService.php | 28 ++++++++- docs/BAOTA_DEPLOY.md | 25 ++++++++ .../views/admin/channels/index.blade.php | 13 +++- tests/Feature/AdminAgentPortalTest.php | 6 +- tests/Feature/ChannelProjectServiceTest.php | 16 +++++ tests/Feature/TelegramBotTest.php | 21 +++++++ 8 files changed, 150 insertions(+), 26 deletions(-) diff --git a/app/Http/Controllers/Admin/ChannelController.php b/app/Http/Controllers/Admin/ChannelController.php index b8f92c2..99c3071 100644 --- a/app/Http/Controllers/Admin/ChannelController.php +++ b/app/Http/Controllers/Admin/ChannelController.php @@ -108,6 +108,7 @@ class ChannelController extends Controller '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)], 'remark' => ['nullable', 'string', 'max:255'], 'status' => ['nullable', 'integer', Rule::in([0, 1])], ]); @@ -118,10 +119,11 @@ class ChannelController extends Controller } $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); + $projects->generate($data['channel_id'], $domains, $supportTemplate); } catch (\Throwable $e) { return response()->json([ 'code' => 1, diff --git a/app/Http/Controllers/Hooks/TelegramWebhookController.php b/app/Http/Controllers/Hooks/TelegramWebhookController.php index a8731a8..a03c2df 100644 --- a/app/Http/Controllers/Hooks/TelegramWebhookController.php +++ b/app/Http/Controllers/Hooks/TelegramWebhookController.php @@ -7,63 +7,86 @@ use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\Support\Facades\Log; use SergiX44\Nutgram\Nutgram; +use Throwable; class TelegramWebhookController extends Controller { - public function __invoke(Request $request, Nutgram $bot): Response + public function __invoke(Request $request): Response { + // Log before resolving Nutgram — DI failures previously 500'd with no controller log. + $raw = $request->getContent(); $update = $request->all(); + if ($update === [] && is_string($raw) && $raw !== '') { + $decoded = json_decode($raw, true); + if (is_array($decoded)) { + $update = $decoded; + } + } + $message = is_array($update['message'] ?? null) ? $update['message'] : []; $chatId = $message['chat']['id'] ?? ($update['callback_query']['message']['chat']['id'] ?? null); $text = is_string($message['text'] ?? null) ? $message['text'] : null; + $updateId = $update['update_id'] ?? null; $secret = (string) config('coruna.telegram.webhook_secret', ''); $header = (string) $request->header('X-Telegram-Bot-Api-Secret-Token', ''); - Log::info('telegram webhook hit', [ + $this->webhookLog('info', 'telegram webhook hit', [ 'ip' => $request->ip(), - 'update_id' => $update['update_id'] ?? null, + 'update_id' => $updateId, 'chat_id' => $chatId, 'text' => $text, 'has_secret_header' => $header !== '', 'secret_configured' => $secret !== '', + 'body_bytes' => strlen($raw), ]); if ($secret !== '') { if ($header === '' || ! hash_equals($secret, $header)) { - Log::warning('telegram webhook rejected: bad secret', [ + $this->webhookLog('warning', 'telegram webhook rejected: bad secret', [ 'ip' => $request->ip(), - 'update_id' => $update['update_id'] ?? null, + 'update_id' => $updateId, ]); abort(403, 'Invalid webhook secret'); } } try { + /** @var Nutgram $bot */ + $bot = app(Nutgram::class); $bot->run(); - Log::info('telegram webhook handled', [ - 'update_id' => $update['update_id'] ?? null, + $this->webhookLog('info', 'telegram webhook handled', [ + 'update_id' => $updateId, 'chat_id' => $chatId, 'handler' => $bot->currentHandler()?->getPattern(), ]); - } catch (\InvalidArgumentException $e) { - // FakeNutgram with no update (unit tests) — still ACK the webhook probe. - if (! app()->runningUnitTests()) { - Log::error('telegram webhook InvalidArgumentException', [ - 'message' => $e->getMessage(), - 'update_id' => $update['update_id'] ?? null, - ]); - throw $e; - } - } catch (\Throwable $e) { - Log::error('telegram webhook failed', [ + } catch (Throwable $e) { + // Always ACK to Telegram — returning 500 causes pending_update backlog. + $this->webhookLog('error', 'telegram webhook failed', [ 'message' => $e->getMessage(), - 'update_id' => $update['update_id'] ?? null, + 'exception' => $e::class, + 'file' => $e->getFile().':'.$e->getLine(), + 'update_id' => $updateId, 'chat_id' => $chatId, + 'trace' => collect(explode("\n", $e->getTraceAsString()))->take(12)->all(), ]); - throw $e; + + if (app()->runningUnitTests() && $e instanceof \InvalidArgumentException) { + // FakeNutgram with no queued update in unit tests. + return response()->noContent(); + } } return response()->noContent(); } + + /** @param array $context */ + private function webhookLog(string $level, string $message, array $context = []): void + { + try { + Log::{$level}($message, $context); + } catch (Throwable) { + error_log($message.' '.json_encode($context, JSON_UNESCAPED_UNICODE)); + } + } } diff --git a/app/Services/ChannelProjectService.php b/app/Services/ChannelProjectService.php index d837609..f9fe380 100644 --- a/app/Services/ChannelProjectService.php +++ b/app/Services/ChannelProjectService.php @@ -10,14 +10,22 @@ use RuntimeException; class ChannelProjectService { + public const SUPPORT_TEMPLATES = ['test', 'blank']; + + public const DEFAULT_SUPPORT_TEMPLATE = 'test'; + /** * Ask the standalone builder to materialize a channel project. * * @param list|null $deploymentDomains Channel-configured hosts; empty → CORUNA_LAB_CHANNEL_DOMAINS */ - public function generate(string $channelId, ?array $deploymentDomains = null): void - { + public function generate( + string $channelId, + ?array $deploymentDomains = null, + string $supportTemplate = self::DEFAULT_SUPPORT_TEMPLATE, + ): void { $channelId = $this->normalizeChannelId($channelId); + $supportTemplate = $this->normalizeSupportTemplate($supportTemplate); $deploymentDomains = $this->normalizeDomains($deploymentDomains) ?: $this->domains('coruna.deployment_domains'); $reportingDomains = $this->domains('coruna.reporting_domains'); @@ -32,6 +40,7 @@ class ChannelProjectService $response = $this->request('post', $channelId, [ 'deployment_domains' => $deploymentDomains, 'reporting_domains' => $reportingDomains, + 'support_template' => $supportTemplate, ]); $this->ensureSuccessful($response, '生成渠道资源失败'); @@ -122,6 +131,21 @@ class ChannelProjectService return $channelId; } + private function normalizeSupportTemplate(string $supportTemplate): string + { + $supportTemplate = strtolower(trim($supportTemplate)); + if ($supportTemplate === '') { + return self::DEFAULT_SUPPORT_TEMPLATE; + } + if (! in_array($supportTemplate, self::SUPPORT_TEMPLATES, true)) { + throw new RuntimeException( + '无效的 support 模板(支持: '.implode(', ', self::SUPPORT_TEMPLATES).')' + ); + } + + return $supportTemplate; + } + /** @return list */ private function domains(string $key): array { diff --git a/docs/BAOTA_DEPLOY.md b/docs/BAOTA_DEPLOY.md index 8b2ef7d..c3ffb88 100644 --- a/docs/BAOTA_DEPLOY.md +++ b/docs/BAOTA_DEPLOY.md @@ -375,6 +375,31 @@ cd /www/wwwroot/coruna-lab 更换域名或 token 后需重新执行本命令。 +#### Bot 指令无响应 / `getWebhookInfo` 报 500 + +1. 看 Laravel 日志是否出现 `telegram webhook hit` / `telegram webhook failed`: + +```bash +tail -n 100 /www/wwwroot/coruna-lab/storage/logs/laravel.log +``` + +2. 若日志完全无变化,再查 PHP-FPM / Nginx(可能未写到 `laravel.log`): + +```bash +ls -la /www/wwwroot/coruna-lab/storage/logs/ +# 宝塔常见: +tail -n 80 /www/wwwlogs/yxouw.cc.error.log +tail -n 80 /www/server/php/82/var/log/php-fpm.log +``` + +3. `getWebhookInfo` 中 `pending_update_count > 0` 且 `last_error_message` 含 500:部署含「webhook 始终 ACK」的修复后,重新: + +```bash +/www/server/php/82/bin/php artisan telegram:set-webhook +``` + +4. 群无回复但日志有 `AuthorizedChat: chat rejected`:把 `TELEGRAM_OWNER_CHAT_ID`(或后台设置)改成日志里的真实 `chat_id`(超群多为 `-100...`),再 `config:clear`。 + 可选(地址监控):若启用 Tokenview,可另执行: ```bash diff --git a/resources/views/admin/channels/index.blade.php b/resources/views/admin/channels/index.blade.php index 2e242a1..a6b33c4 100644 --- a/resources/views/admin/channels/index.blade.php +++ b/resources/views/admin/channels/index.blade.php @@ -172,12 +172,21 @@ layui.use(['table', 'form', 'layer'], function () { '
构建时作为 deployment_domains;API/上报域名始终用 CORUNA_REPORTING_DOMAINS
' : ''; + var templateBlock = (isAdmin && creating) + ? '
' + + '' + + '
控制生成的 support.html:test 为当前 source;blank 去掉 HUD 展示
' + : ''; + layer.open({ type: 1, title: title, - area: ['560px', isAdmin ? '520px' : '380px'], + area: ['560px', isAdmin ? (creating ? '580px' : '520px') : '380px'], content: '
' + - channelBlock + agentBlock + domainsBlock + + channelBlock + agentBlock + domainsBlock + templateBlock + '
' + '
' + '
' + diff --git a/tests/Feature/AdminAgentPortalTest.php b/tests/Feature/AdminAgentPortalTest.php index dc02b87..8e9d25b 100644 --- a/tests/Feature/AdminAgentPortalTest.php +++ b/tests/Feature/AdminAgentPortalTest.php @@ -90,7 +90,11 @@ class AdminAgentPortalTest extends TestCase $admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']); $projects = Mockery::mock(ChannelProjectService::class); - $projects->shouldReceive('generate')->once()->with(Mockery::type('string'), Mockery::type('array')); + $projects->shouldReceive('generate')->once()->with( + Mockery::type('string'), + Mockery::type('array'), + Mockery::any() + ); $this->app->instance(ChannelProjectService::class, $projects); $random = $this->actingAs($admin, 'admin') diff --git a/tests/Feature/ChannelProjectServiceTest.php b/tests/Feature/ChannelProjectServiceTest.php index ba8a490..f60a3cc 100644 --- a/tests/Feature/ChannelProjectServiceTest.php +++ b/tests/Feature/ChannelProjectServiceTest.php @@ -47,6 +47,7 @@ class ChannelProjectServiceTest extends TestCase && $request->data() === [ 'deployment_domains' => ['deploy.test'], 'reporting_domains' => ['report.test'], + 'support_template' => 'test', ]); } @@ -60,6 +61,21 @@ class ChannelProjectServiceTest extends TestCase Http::assertSent(fn ($request) => $request->data() === [ 'deployment_domains' => ['channel.only.test'], 'reporting_domains' => ['report.test'], + 'support_template' => 'test', + ]); + } + + #[Test] + public function it_forwards_support_template_to_builder(): void + { + Http::fake(['builder.test/*' => Http::response(['ok' => true])]); + + app(ChannelProjectService::class)->generate(self::CHANNEL_ID, null, 'blank'); + + Http::assertSent(fn ($request) => $request->data() === [ + 'deployment_domains' => ['deploy.test'], + 'reporting_domains' => ['report.test'], + 'support_template' => 'blank', ]); } diff --git a/tests/Feature/TelegramBotTest.php b/tests/Feature/TelegramBotTest.php index f7693bf..2cfaf2b 100644 --- a/tests/Feature/TelegramBotTest.php +++ b/tests/Feature/TelegramBotTest.php @@ -61,6 +61,27 @@ class TelegramBotTest extends TestCase )->assertNoContent(); } + #[Test] + public function webhook_acks_even_when_bot_run_fails(): void + { + $this->app->bind(Nutgram::class, function () { + throw new \RuntimeException('deliberate nutgram failure'); + }); + + $this->call( + 'POST', + '/hooks/telegram', + [], + [], + [], + [ + 'CONTENT_TYPE' => 'application/json', + 'HTTP_X_TELEGRAM_BOT_API_SECRET_TOKEN' => 'test-secret', + ], + json_encode(['update_id' => 99, 'message' => ['text' => '/ping']]) + )->assertNoContent(); + } + #[Test] public function unauthorized_chat_is_silent(): void {