Files
agent-news/bot/lib/claude_runner.dart
matt2dev be09de2ad7 Initial agent-news: Dart Telegram bot + Claude Code runner + MCP bridge
/news spawns claude-code as a local subprocess (single pod, no cross-container
exec on K3s); Claude delivers the digest itself via the send_to_telegram MCP tool.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 20:40:32 +03:00

88 lines
3.4 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'config.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<ClaudeResult> 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,
'--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 base = topic == null || topic.isEmpty
? config.newsPrompt
: 'Проведи ресёрч последних новостей и трендов по теме "$topic" за последние '
'7 дней. Используй поиск в интернете. Составь краткую выжимку: 5-8 пунктов, '
'у каждого — суть в 1-2 предложениях и ссылка на источник.';
return '$base\n\n'
'Когда выжимка готова, ОБЯЗАТЕЛЬНО вызови инструмент send_to_telegram с '
'параметрами chat_id=$chatId и text=<готовый текст в Markdown, с заголовком>. '
'Это единственный способ доставить материал — не выводи финальный ответ никак '
'иначе, только через вызов этого инструмента.';
}
}