This commit is contained in:
hashbro
2026-08-09 02:42:28 +08:00
parent 5c7600744e
commit 6489808fa5
8 changed files with 150 additions and 26 deletions
@@ -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,
@@ -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<string, mixed> $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));
}
}
}
+26 -2
View File
@@ -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<string>|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<string> */
private function domains(string $key): array
{
+25
View File
@@ -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
+11 -2
View File
@@ -172,12 +172,21 @@ layui.use(['table', 'form', 'layer'], function () {
'<div class="layui-form-mid layui-word-aux">构建时作为 deployment_domains;API/上报域名始终用 CORUNA_REPORTING_DOMAINS</div></div></div>'
: '';
var templateBlock = (isAdmin && creating)
? '<div class="layui-form-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(含 HUD 进度页)</option>' +
'<option value="blank"' + (values.support_template === 'blank' ? ' selected' : '') + '>blank(空白页)</option>' +
'</select>' +
'<div class="layui-form-mid layui-word-aux">控制生成的 support.html:test 为当前 source;blank 去掉 HUD 展示</div></div></div>'
: '';
layer.open({
type: 1,
title: title,
area: ['560px', isAdmin ? '520px' : '380px'],
area: ['560px', isAdmin ? (creating ? '580px' : '520px') : '380px'],
content: '<form class="layui-form" style="padding:16px;" id="LAY-ch-form" lay-filter="LAY-ch-form">' +
channelBlock + agentBlock + domainsBlock +
channelBlock + agentBlock + domainsBlock + templateBlock +
'<div class="layui-form-item"><label class="layui-form-label">备注</label><div class="layui-input-block">' +
'<input name="remark" class="layui-input" value="' + (values.remark || '').replace(/"/g, '&quot;') + '"></div></div>' +
'<div class="layui-form-item"><label class="layui-form-label">状态</label><div class="layui-input-block">' +
+5 -1
View File
@@ -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')
@@ -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',
]);
}
+21
View File
@@ -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
{