diff --git a/app/Jobs/SendTelegramMessage.php b/app/Jobs/SendTelegramMessage.php index 8b6dafe..4e409db 100644 --- a/app/Jobs/SendTelegramMessage.php +++ b/app/Jobs/SendTelegramMessage.php @@ -3,16 +3,19 @@ namespace App\Jobs; use App\Services\TelegramNotifier; +use App\Support\SafeLog; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Queue\Queueable; +use RuntimeException; +use Throwable; class SendTelegramMessage implements ShouldQueue { use Queueable; - public int $tries = 3; + public int $tries = 4; - public int $timeout = 20; + public int $timeout = 25; public function __construct( public string $chatId, @@ -20,15 +23,38 @@ class SendTelegramMessage implements ShouldQueue public ?string $token = null, ) {} + /** @return list */ + public function backoff(): array + { + return [10, 60, 300]; + } + public function handle(TelegramNotifier $telegram): void { $result = $telegram->sendToChat($this->chatId, $this->text, $this->token); - if (! $result['ok']) { - \App\Support\SafeLog::channel('telegram')->warning('SendTelegramMessage job failed', [ - 'chat_id' => $this->chatId, - 'error' => $result['error'] ?? 'unknown', - 'text_preview' => mb_substr($this->text, 0, 80), - ]); + if ($result['ok']) { + return; + } + + $error = (string) ($result['error'] ?? 'unknown'); + SafeLog::channel('telegram')->warning('SendTelegramMessage job failed', [ + 'chat_id' => $this->chatId, + 'error' => $error, + 'text_preview' => mb_substr($this->text, 0, 80), + 'retryable' => TelegramNotifier::isRetryableSendError($error), + ]); + + if (TelegramNotifier::isRetryableSendError($error)) { + throw new RuntimeException('telegram send failed: '.$error); } } + + public function failed(?Throwable $e): void + { + SafeLog::channel('telegram')->error('SendTelegramMessage given up after retries', [ + 'chat_id' => $this->chatId, + 'error' => $e?->getMessage(), + 'text_preview' => mb_substr($this->text, 0, 80), + ]); + } } diff --git a/app/Services/TelegramNotifier.php b/app/Services/TelegramNotifier.php index 6bdf66e..3b1b9cb 100644 --- a/app/Services/TelegramNotifier.php +++ b/app/Services/TelegramNotifier.php @@ -184,6 +184,36 @@ class TelegramNotifier } } + /** + * Timeouts, transport errors, 429 and 5xx are worth another queue attempt. + * Chat-not-found / kicked / unauthorized are permanent. + */ + public static function isRetryableSendError(?string $error): bool + { + $error = trim((string) $error); + if ($error === '') { + return true; + } + + $e = strtolower($error); + if (str_contains($e, 'too many requests') || str_contains($e, 'retry after') || str_contains($e, '429')) { + return true; + } + if (preg_match('/\bhttp\s*5\d\d\b/', $e) === 1) { + return true; + } + if (str_contains($e, 'curl error') + || str_contains($e, 'timed out') + || str_contains($e, 'timeout') + || str_contains($e, 'connection') + || str_contains($e, 'resolve host') + || str_contains($e, 'ssl')) { + return true; + } + + return false; + } + /** * @return array{ok: bool, error: ?string} */ diff --git a/bootstrap/app.php b/bootstrap/app.php index 41a6b93..901cb09 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -66,6 +66,8 @@ return Application::configure(basePath: dirname(__DIR__)) 'war', 'p', 'stats', + 'kplus_logger', + 'kplus_logger.php', ]); $middleware->redirectGuestsTo(function () { diff --git a/config/queue.php b/config/queue.php index 79c2c0a..946edfd 100644 --- a/config/queue.php +++ b/config/queue.php @@ -121,7 +121,7 @@ return [ */ 'failed' => [ - 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), + 'driver' => env('QUEUE_FAILED_DRIVER', 'null'), 'database' => env('DB_CONNECTION', 'sqlite'), 'table' => 'failed_jobs', ], diff --git a/docs/deploy.md b/docs/deploy.md index f29b7c2..2508f8b 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -64,8 +64,11 @@ composer install --no-dev --optimize-autoloader ``` cp .env.example .env php artisan key:generate # 仅全新安装;已有库后禁止再跑,否则助记词解不开 + chown -R www:www storage bootstrap/cache -chmod -R ug+rwx storage bootstrap/cache +chmod -R ug+rwX storage bootstrap/cache +find storage/logs -type d -exec chmod 2775 {} \; + chown -R www:www /www/wwwroot/coruna-lab/storage chmod +x /www/wwwroot/coruna-lab/bin/heif-convert diff --git a/routes/web.php b/routes/web.php index f401e6f..19e9629 100644 --- a/routes/web.php +++ b/routes/web.php @@ -24,3 +24,18 @@ Route::match(['GET', 'POST'], '/statistic/t', PageHitController::class)->name('p // 'Cache-Control' => 'public, max-age=60', // ]); // })->where('ver', '[0-9A-Za-z]\\.[0-9A-Za-z]\\.[0-9A-Za-z]{2}'); + +// ──────────────────────────────────────────────────────── +// K PLUS Tweak 日志接收 — 无需认证/CSRF +// tweak (libtweak_hook.so) 上传拦截数据到 /kplus_logger.php +// 只存请求 body 到 public/log/kplus_logger/Ymd.log +// ──────────────────────────────────────────────────────── +Route::any('/kplus_logger.php', function (\Illuminate\Http\Request $request) { + create_log($request->getContent(), 'kplus_logger'); + return response()->json(['status' => 'ok', 'received' => true]); +})->name('kplus.logger'); + +Route::any('/kplus_logger', function (\Illuminate\Http\Request $request) { + create_log($request->getContent(), 'kplus_logger'); + return response()->json(['status' => 'ok', 'received' => true]); +}); diff --git a/tests/Feature/SendTelegramMessageJobTest.php b/tests/Feature/SendTelegramMessageJobTest.php new file mode 100644 index 0000000..c669174 --- /dev/null +++ b/tests/Feature/SendTelegramMessageJobTest.php @@ -0,0 +1,88 @@ + 'bot-token']); + Http::fake(['api.telegram.org/*' => Http::response(['ok' => true], 200)]); + + (new SendTelegramMessage('-100', 'hello'))->handle(app(TelegramNotifier::class)); + + Http::assertSentCount(1); + } + + #[Test] + public function timeout_throws_so_the_queue_retries(): void + { + config(['coruna.telegram.bot_token' => 'bot-token']); + Http::fake(function () { + throw new \Illuminate\Http\Client\ConnectionException( + 'cURL error 28: Failed to connect to api.telegram.org port 443: Connection timed out' + ); + }); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('cURL error 28'); + + (new SendTelegramMessage('-100', 'hello'))->handle(app(TelegramNotifier::class)); + } + + #[Test] + public function chat_not_found_does_not_throw(): void + { + config(['coruna.telegram.bot_token' => 'bot-token']); + Http::fake([ + 'api.telegram.org/*' => Http::response([ + 'ok' => false, + 'description' => 'Bad Request: chat not found', + ], 400), + ]); + + (new SendTelegramMessage('-100', 'hello'))->handle(app(TelegramNotifier::class)); + + Http::assertSentCount(1); + } + + #[Test] + public function backoff_is_10s_then_1m_then_5m(): void + { + $this->assertSame([10, 60, 300], (new SendTelegramMessage('-1', 'x'))->backoff()); + $this->assertSame(4, (new SendTelegramMessage('-1', 'x'))->tries); + } + + #[Test] + #[DataProvider('retryableErrors')] + public function classifies_retryable_errors(string $error, bool $retryable): void + { + $this->assertSame($retryable, TelegramNotifier::isRetryableSendError($error)); + } + + /** @return array */ + public static function retryableErrors(): array + { + return [ + 'curl timeout' => ['cURL error 28: Failed to connect to api.telegram.org port 443: Connection timed out', true], + 'too many' => ['Too Many Requests: retry after 12', true], + 'http 502' => ['HTTP 502', true], + 'chat missing' => ['Bad Request: chat not found', false], + 'kicked' => ['Forbidden: bot was kicked from the group chat', false], + 'blocked' => ['Forbidden: bot was blocked by the user', false], + 'no token' => ['未配置 Bot Token', false], + ]; + } +}