admin
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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, '"') + '"></div></div>' +
|
||||
'<div class="layui-form-item"><label class="layui-form-label">状态</label><div class="layui-input-block">' +
|
||||
|
||||
@@ -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',
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user