import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'config.dart'; import 'lexicon.dart'; enum ClaudeOutcome { success, busy, timeout, failure } class ClaudeResult { final ClaudeOutcome outcome; final String detail; ClaudeResult._(this.outcome, this.detail); factory ClaudeResult.success(String detail) => ClaudeResult._(ClaudeOutcome.success, detail); factory ClaudeResult.busy() => ClaudeResult._(ClaudeOutcome.busy, 'уже выполняется другой запрос'); factory ClaudeResult.timeout() => ClaudeResult._(ClaudeOutcome.timeout, 'превышено время ожидания'); factory ClaudeResult.failure(String detail) => ClaudeResult._(ClaudeOutcome.failure, detail); } // Спавнит claude-code CLI как сабпроцесс. Финальный текст в чат уходит НЕ отсюда — // сам claude вызывает MCP-тул send_to_telegram (см. mcp_server/). Этот раннер только // следит, что процесс успешно завершился, и сериализует запросы (одна сессия за раз). class ClaudeRunner { final Config config; bool _busy = false; ClaudeRunner(this.config); bool get isBusy => _busy; Future runNews({required int chatId, String? topic}) async { if (_busy) return ClaudeResult.busy(); _busy = true; try { final prompt = _buildPrompt(chatId: chatId, topic: topic); final process = await Process.start( 'claude', [ '-p', prompt, '--mcp-config', config.mcpConfigPath, '--strict-mcp-config', '--dangerously-skip-permissions', ], environment: {'TELEGRAM_BOT_TOKEN': config.telegramToken}, ); final stdoutFuture = process.stdout.transform(utf8.decoder).join(); final stderrFuture = process.stderr.transform(utf8.decoder).join(); int exitCode; try { exitCode = await process.exitCode.timeout( Duration(seconds: config.claudeTimeoutSeconds), ); } on TimeoutException { process.kill(ProcessSignal.sigterm); return ClaudeResult.timeout(); } final out = await stdoutFuture; final err = await stderrFuture; if (exitCode != 0) { return ClaudeResult.failure(err.isNotEmpty ? err : out); } return ClaudeResult.success(out); } finally { _busy = false; } } String _buildPrompt({required int chatId, String? topic}) { final effectiveTopic = (topic == null || topic.isEmpty) ? config.defaultTopic : topic; return [ Lexicon.researchInstruction(effectiveTopic), Lexicon.progressInstruction(chatId), Lexicon.finalDeliveryInstruction(chatId), ].join('\n\n'); } }