Files
agent-news/bot/lib/claude_runner.dart
matt2dev 7bfe0dfe0c Fix HOME volume mount, genericize topic, add progress statuses, extract lexicon
- Volume was mounted at ~/.claude only, but claude-code writes auth to
  ~/.claude.json too — that file lived on the ephemeral container layer and
  was lost on every recreate. Now the whole $HOME is on the named volume,
  and claude-config moved outside HOME (/app) so it isn't shadowed by it.
- Default /news topic was hardcoded to AI; now it's a configurable
  DEFAULT_TOPIC (plain "главные новости дня"), topic is fully free-form.
- Prompt now instructs Claude to send interim " ..." progress updates via
  send_to_telegram while researching, not just the final digest — a bare
  request like "call this tool" was already taking 3+ minutes end-to-end,
  so silence read as broken.
- All user-facing/prompt text moved into bot/lib/lexicon.dart.

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

85 lines
2.8 KiB
Dart
Raw Permalink 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';
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<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,
'--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');
}
}