- 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>
84 lines
2.9 KiB
Dart
84 lines
2.9 KiB
Dart
import 'dart:convert';
|
||
import 'dart:io';
|
||
import 'package:mcp_dart/mcp_dart.dart';
|
||
|
||
final _token = Platform.environment['TELEGRAM_BOT_TOKEN'] ?? '';
|
||
final _http = HttpClient();
|
||
|
||
CallToolResult _ok(String text) => CallToolResult(content: [TextContent(text: text)]);
|
||
|
||
CallToolResult _err(String text) =>
|
||
CallToolResult(content: [TextContent(text: 'Ошибка: $text')], isError: true);
|
||
|
||
List<String> _splitMessage(String text) {
|
||
const maxLen = 4000;
|
||
if (text.length <= maxLen) return [text];
|
||
final chunks = <String>[];
|
||
var start = 0;
|
||
while (start < text.length) {
|
||
final end = (start + maxLen).clamp(0, text.length);
|
||
chunks.add(text.substring(start, end));
|
||
start = end;
|
||
}
|
||
return chunks;
|
||
}
|
||
|
||
Future<void> _sendMessage(int chatId, String text) async {
|
||
for (final chunk in _splitMessage(text)) {
|
||
final req = await _http.postUrl(
|
||
Uri.parse('https://api.telegram.org/bot$_token/sendMessage'),
|
||
);
|
||
req.headers.contentType = ContentType.json;
|
||
req.write(jsonEncode({
|
||
'chat_id': chatId,
|
||
'text': chunk,
|
||
'parse_mode': 'Markdown',
|
||
}));
|
||
final res = await req.close();
|
||
final body = await res.transform(utf8.decoder).join();
|
||
if (res.statusCode >= 400) {
|
||
throw Exception('Telegram sendMessage → ${res.statusCode}: $body');
|
||
}
|
||
}
|
||
}
|
||
|
||
void main() {
|
||
final server = McpServer(
|
||
const Implementation(name: 'agent-news', version: '0.1.0'),
|
||
options: const McpServerOptions(
|
||
capabilities: ServerCapabilities(tools: ServerCapabilitiesTools()),
|
||
),
|
||
);
|
||
|
||
server.registerTool(
|
||
'send_to_telegram',
|
||
description:
|
||
'Отправляет текст (Markdown) в указанный Telegram-чат. Единственный '
|
||
'способ доставить результат пользователю. Можно и нужно вызывать '
|
||
'несколько раз за сессию: сначала короткими статусами по ходу работы '
|
||
'("⏳ ищу источники…"), в конце — с финальным оформленным текстом.',
|
||
inputSchema: JsonSchema.object(
|
||
properties: {
|
||
'chat_id': JsonSchema.integer(description: 'ID чата Telegram, куда отправить сообщение'),
|
||
'text': JsonSchema.string(description: 'Готовый текст в Markdown-разметке Telegram'),
|
||
},
|
||
required: ['chat_id', 'text'],
|
||
),
|
||
callback: (args, _) async {
|
||
if (_token.isEmpty) {
|
||
return _err('TELEGRAM_BOT_TOKEN не задан в окружении MCP-сервера');
|
||
}
|
||
try {
|
||
final chatId = args['chat_id'] as int;
|
||
final text = args['text'].toString();
|
||
await _sendMessage(chatId, text);
|
||
return _ok('Отправлено в чат $chatId (${text.length} символов).');
|
||
} catch (e) {
|
||
return _err('$e');
|
||
}
|
||
},
|
||
);
|
||
|
||
server.connect(StdioServerTransport());
|
||
}
|