Управляю VDS с телефона: Telegram-бот + Claude Code CLI. Claude.. Claude. claude code.. Claude. claude code. DevOps.. Claude. claude code. DevOps. PostgreSQL.. Claude. claude code. DevOps. PostgreSQL. python.. Claude. claude code. DevOps. PostgreSQL. python. telegram.. Claude. claude code. DevOps. PostgreSQL. python. telegram. telegrambot.. Claude. claude code. DevOps. PostgreSQL. python. telegram. telegrambot. vds.. Claude. claude code. DevOps. PostgreSQL. python. telegram. telegrambot. vds. vps.. Claude. claude code. DevOps. PostgreSQL. python. telegram. telegrambot. vds. vps. искусственный интеллект.. Claude. claude code. DevOps. PostgreSQL. python. telegram. telegrambot. vds. vps. искусственный интеллект. сезон ии в разработке.. Claude. claude code. DevOps. PostgreSQL. python. telegram. telegrambot. vds. vps. искусственный интеллект. сезон ии в разработке. сисадмин.. Claude. claude code. DevOps. PostgreSQL. python. telegram. telegrambot. vds. vps. искусственный интеллект. сезон ии в разработке. сисадмин. Системное администрирование.

Я не devops, поэтому хотел получать ответы на человеческом языке в любое время. Ты в дороге, приходит алерт, нужно срочно посмотреть логи или проверить статус сервиса. Достаёшь телефон, открываешь SSH-клиент, набираешь команды…

В итоге, я написал Telegram-бота, который принимает запросы на человеческом языке и выполняет их через Claude Code CLI. Теперь вместо journalctl -u nginx --since "1 hour ago" | grep error я просто пишу в Telegram: «Покажи ошибки nginx за последний час». Выложил в opensource.

В статье расскажу про архитектуру и примеры.

Claude Code CLI

Консольный инструмент от Anthropic, который даёт Claude доступ к файловой системе и терминалу. AI-агент, который может читать файлы, выполнять bash-команды и анализировать результаты.Внутри него можно создавать еще агентов, можно помещать его работать в конкретный проект…

Я использую подписку Claude Max ($200/месяц), в которую входит почти безлимитный доступ к Claude Code CLI. Щедро, в отличии от OpenAI. Токены не считаю, подписки хватает, поэтому извините – в комментариях не отвечу на этот вопрос.

Архитектура

┌─────────────┐     ┌─────────────────┐     ┌─────────────────┐
│  Telegram   │────▶│   Python Bot    │────▶│ Claude Code CLI │
│   Client    │◀────│  (handlers +    │◀────│   (subprocess)  │
└─────────────┘     │   services)     │     └─────────────────┘
                    └────────┬────────┘              │
                             │                       ▼
                    ┌────────▼────────┐     ┌───────────────┐
                    │   PostgreSQL    │     │    Server     │
                    │   (sessions)    │     │  (filesystem) │
                    └─────────────────┘     └───────────────┘

Бот состоит из нескольких слоёв:

bot/
├── main.py              # Точка входа, инициализация
├── config.py            # Конфигурация из ENV
├── handlers/
│   ├── commands.py      # /start, /reset, /status, /cancel
│   ├── messages.py      # Обработка текстовых сообщений
│   └── files.py         # Загрузка и анализ файлов
├── services/
│   ├── claude.py        # Вызов Claude CLI
│   ├── session.py       # Управление сессиями
│   └── formatter.py     # Форматирование для Telegram
└── database/
    └── pool.py          # Connection pool PostgreSQL

Разделение на handlers и services. Handlers знают про Telegram (Update, Context), services нет. Это позволяет тестировать логику отдельно от Telegram API и переиспользовать services, если понадобится другой интерфейс.

Вызов Claude CLI: subprocess + stdin

Важный для продакшен сервера был вопрос – как безопасно передать пользовательский ввод в Claude CLI.

Плохо shell=True:

# ОПАСНО! Не делайте так
subprocess.run(f'claude "{user_message}"', shell=True)

Если user_message содержит "; rm -rf / #, может пойти что-то не так.

Subprocess.PIPE + stdin:

process = await asyncio.create_subprocess_exec(
    CLAUDE_CLI_PATH,
    stdin=asyncio.subprocess.PIPE,
    stdout=asyncio.subprocess.PIPE,
    stderr=asyncio.subprocess.PIPE,
    cwd=working_dir,
)

stdout, stderr = await process.communicate(
    input=full_prompt.encode('utf-8')
)

Здесь create_subprocess_exec запускает процесс напрямую, без shell. Промпт передаётся через stdin как данные, а не как часть команды. Даже если в сообщении будут спецсимволы shel, они не интерпретируются.

Дополнительно использую asyncio. Бот не блокируется, пока Claude что-то делает. Можно обрабатывать команды от нескольких пользователей параллельно (если настроить whitelist на несколько ID – про это дальше будет).

Safety Prompt: как ограничить от безумия

Claude Code по умолчанию может делать что угодно: редактировать файлы, запускать команды, устанавливать пакеты.

Safety instructions, которые добавляются к каждому запросу:

safety_instructions = """IMPORTANT CONTEXT: You are being accessed through Telegram bot.
The user is writing from their phone via Telegram messenger.
Responses will be shown in Telegram chat, so keep them concise.

CRITICAL RULES - YOU MUST FOLLOW:
1. DO NOT execute ANY system commands unless the message explicitly contains 
   trigger words: "выполни", "сделай", "запусти", "исправь", "создай", 
   "удали", "restart", "перезапусти"
2. If user just asks about status - ONLY provide information, don't modify
3. NEVER run systemctl, apt, rm, or modifying commands without explicit request
4. Default mode is READ-ONLY - only analyze and inform
"""

if needs_execution:
    safety_instructions += "CURRENT MODE: Execution allowedn"
else:
    safety_instructions += "CURRENT MODE: Information only (no execution)n"

Флаг needs_execution определяется простой проверкой:

execute_keywords = [
    "выполни", "сделай", "запусти", "исправь", 
    "создай", "удали", "restart", "перезапусти"
]
needs_execution = any(kw in user_message.lower() for kw in execute_keywords)

Если ключевых слов нет, к сообщению добавляется префикс:

if not needs_execution:
    user_message = f"[ТОЛЬКО ИНФОРМАЦИЯ, НЕ ВЫПОЛНЯТЬ КОМАНДЫ] {user_message}"

Это не 100% защита. Claude может ошибиться (но пока не было). На практике работает хорошо. Хотя иногда переживаю.

Управление процессами

Claude может думать долго, особенно если анализирует большие логи. Нужны таймауты и возможность отмены.

Таймаут:

try:
    stdout, stderr = await asyncio.wait_for(
        process.communicate(input=prompt.encode('utf-8')),
        timeout=CLAUDE_TIMEOUT  # 300 секунд по умолчанию
    )
except asyncio.TimeoutError:
    process.terminate()
    try:
        await asyncio.wait_for(process.wait(), timeout=5)
    except asyncio.TimeoutError:
        process.kill()  # SIGKILL если не завершился по SIGTERM

Отмена по команде /cancel:

Активные процессы хранятся в словаре по user_id:

_active_processes: Dict[int, asyncio.subprocess.Process] = {}

При запуске Claude процесс регистрируется:

_active_processes[user_id] = process

Команда /cancel находит и завершает процесс:

async def cancel_process(user_id: int) -> bool:
    process = _active_processes.get(user_id)
    if process and process.returncode is None:
        process.terminate()
        try:
            await asyncio.wait_for(process.wait(), timeout=5)
        except asyncio.TimeoutError:
            process.kill()
        return True
    return False

Graceful shutdown:

При остановке бота (SIGTERM/SIGINT) нужно корректно завершить все процессы:

async def terminate_all_processes():
    for user_id, process in list(_active_processes.items()):
        if process.returncode is None:
            process.terminate()
            try:
                await asyncio.wait_for(process.wait(), timeout=5)
            except asyncio.TimeoutError:
                process.kill()
    _active_processes.clear()

Это вызывается в post_shutdown хуке python-telegram-bot.

Сессии и контекст

Claude должен помнить контекст разговора. Если я спросил про nginx, а потом написал «А что с ошибками?» он должен понять, что речь про nginx.

Структура сессии:

@dataclass
class Session:
    user_id: int
    context: List[Dict]        # История сообщений
    working_dir: str           # Текущая директория
    message_count: int         # Счётчик сообщений
    created_at: datetime
    last_activity: datetime

Хранение:

Двухуровневое: in-memory cache + PostgreSQL.

class SessionManager:
    def __init__(self):
        self._cache: Dict[int, Session] = {}
    
    def get_session(self, user_id: int) -> Session:
        # Сначала проверяем кэш
        if user_id in self._cache:
            return self._cache[user_id]
        
        # Потом БД
        row = execute_one(
            "SELECT * FROM sessions WHERE user_id = %s", 
            (user_id,)
        )
        if row:
            session = Session.from_dict(dict(row))
            self._cache[user_id] = session
            return session
        
        # Создаём новую
        # ...

Кэш нужен для скорости. Не дёргать БД на каждое сообщение. PostgreSQL для персистентности между перезапусками бота.

Контекст в промпте:

При формировании запроса к Claude добавляю последние N сообщений из контекста:

def build_prompt(user_message: str, context: List[Dict]) -> str:
    context_text = ""
    if context:
        recent = context[-CLAUDE_MAX_CONTEXT_MESSAGES:]  # последние 10
        for msg in recent:
            context_text += f"User: {msg['user']}n"
            # Обрезаем длинные ответы в контексте
            assistant_msg = msg['assistant'][:500] + "..." 
                if len(msg['assistant']) > 500 else msg['assistant']
            context_text += f"Assistant: {assistant_msg}n"
    
    return safety_instructions + context_text + f"Current request: {user_message}"

Обрезка ответов в контексте важна, иначе промпт раздувается и Claude начинает работать медленнее.

Форматирование для Telegram

Telegram поддерживает HTML-разметку. Claude возвращает Markdown. Нужен конвертер.

Проблема 1: Экранирование

В Telegram HTML символы <>& нужно экранировать, но только вне тегов:

def escape_html(text: str) -> str:
    return text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")

Проблема 2: Code blocks

Claude возвращает код в тройных бэктиках. Telegram использует <pre> и <code>.

Решение — сначала извлечь все code blocks, заменить на плейсхолдеры, отформатировать остальной текст, потом вернуть code blocks:

def format_code_blocks(text: str) -> Tuple[str, List[str]]:
    code_blocks = []
    pattern = r"```(w*)n?(.*?)```"
    
    def replacer(match):
        lang = match.group(1) or ""
        code = match.group(2).strip()
        idx = len(code_blocks)
        code_blocks.append((lang, code))
        return f"<<<CODE_BLOCK_{idx}>>>"
    
    text = re.sub(pattern, replacer, text, flags=re.DOTALL)
    return text, code_blocks

def restore_code_blocks(text: str, code_blocks: List) -> str:
    for idx, (lang, code) in enumerate(code_blocks):
        placeholder = f"<<<CODE_BLOCK_{idx}>>>"
        formatted = f"<pre>{escape_html(code)}</pre>"
        text = text.replace(placeholder, formatted)
    return text

Проблема 3: Лимит 4096 символов

Telegram не принимает сообщения длиннее 4096 символов. Нужно разбивать, но аккуратно:

def split_message(text: str, max_length: int = 4096) -> List[str]:
    parts = []
    while len(text) > max_length:
        # Ищем хорошую точку разбиения
        split_idx = max_length
        
        # Пробуем разбить по параграфу
        para_idx = text.rfind("nn", 0, max_length)
        if para_idx > max_length // 2:
            split_idx = para_idx
        # Или по переносу строки
        elif (nl_idx := text.rfind("n", 0, max_length)) > max_length // 2:
            split_idx = nl_idx
        
        part = text[:split_idx]
        
        # Проверяем незакрытые теги
        open_tags = re.findall(r"<(b|i|code|pre)>", part)
        close_tags = re.findall(r"</(b|i|code|pre)>", part)
        
        # Закрываем незакрытые теги в конце части
        for tag in reversed(open_tags):
            if open_tags.count(tag) > close_tags.count(tag):
                part += f"</{tag}>"
        
        parts.append(part.strip())
        text = text[split_idx:].strip()
    
    if text:
        parts.append(text)
    return parts

База данных

Две таблицы:

-- Сессии пользователей
CREATE TABLE sessions (
    user_id BIGINT PRIMARY KEY,
    context JSONB DEFAULT '[]'::jsonb,
    working_dir TEXT DEFAULT '/root',
    message_count INT DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    last_activity TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Логи команд (для истории и отладки)
CREATE TABLE command_logs (
    id SERIAL PRIMARY KEY,
    user_id BIGINT,
    command TEXT,
    response TEXT,
    execution_time_ms INT,
    error TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Контекст хранится как JSONB, PostgreSQL умеет с ним работать эффективно.

Connection pool настроен через psycopg2:

_pool = pool.ThreadedConnectionPool(
    minconn=1, 
    maxconn=10, 
    **DB_CONFIG
)

Почему PostgreSQL, а не SQLite? Claude Code сам предложил при проектировании. Для однопользовательского бота SQLite хватило бы, но PostgreSQL надёжнее при конкурентных запросах и проще масштабировать если понадобится.

Примеры использования

Реальные задачи, которые решаю через бота:

Мониторинг:

Управляю VDS с телефона: Telegram-бот + Claude Code CLI - 1

Анализ логов:

Управляю VDS с телефона: Telegram-бот + Claude Code CLI - 2

Работа с базой:

Управляю VDS с телефона: Telegram-бот + Claude Code CLI - 3

Ограничения

Скорость. Claude думает 3-10 секунд. Для простого ls это долго.

Стоимость. Нужна подписка Claude или оплата API.

Не для критичных операций. Ну тут сами решайте.

Не 100% защита. Safety prompt просто инструкция, а гарантия. Claude обычно следует правилам, но бывает и нет.

Как попробовать

Репозиторий: github.com/gmen1057/claude-cli-telegrambot

Требования:

  • VPS с Linux

  • Python 3.9+

  • PostgreSQL

  • Claude Code CLI

  • Telegram Bot Token

Быстрый старт:

git clone https://github.com/gmen1057/claude-cli-telegrambot.git
cd claude-cli-telegrambot
python -m venv venv && source venv/bin/activate
pip install -r requirements.txt
cp .env.example .env  # настроить переменные
python -m bot.main

Есть Docker-вариант с docker-compose.

Итого

  • asyncio.create_subprocess_exec + stdin для безопасного вызова CLI

  • Safety prompt с ключевыми словами для разделения read/write операций

  • Двухуровневое хранение сессий: memory cache + PostgreSQL

  • Форматтер с защитой code blocks и разбиением длинных сообщений

  • Таймауты и graceful shutdown для стабильности

Проект open-source, буду рад issues и PR.

Ссылки:

Автор: men10577

Источник

Rambler's Top100