0% нашли этот документ полезным (0 голосов)
7 просмотров17 страниц

Claude Code Guide Final

Документ представляет собой практическое руководство по проектированию AI-агентов, основанное на анализе production-кода компании Anthropic. Он охватывает различные аспекты, такие как проектирование инструментов, управление контекстом в длинных сессиях и архитектуру подагентов, предоставляя конкретные паттерны и решения для реальных инженерных проблем. Основное внимание уделяется эффективному использованию ресурсов и снижению потребления токенов в процессе работы агентов.

Загружено:

btolybay
Авторское право
© All Rights Reserved
Мы серьезно относимся к защите прав на контент. Если вы подозреваете, что это ваш контент, заявите об этом здесь.
Доступные форматы
Скачать в формате PDF, TXT или читать онлайн в Scribd
0% нашли этот документ полезным (0 голосов)
7 просмотров17 страниц

Claude Code Guide Final

Документ представляет собой практическое руководство по проектированию AI-агентов, основанное на анализе production-кода компании Anthropic. Он охватывает различные аспекты, такие как проектирование инструментов, управление контекстом в длинных сессиях и архитектуру подагентов, предоставляя конкретные паттерны и решения для реальных инженерных проблем. Основное внимание уделяется эффективному использованию ресурсов и снижению потребления токенов в процессе работы агентов.

Загружено:

btolybay
Авторское право
© All Rights Reserved
Мы серьезно относимся к защите прав на контент. Если вы подозреваете, что это ваш контент, заявите об этом здесь.
Доступные форматы
Скачать в формате PDF, TXT или читать онлайн в Scribd

Как строить AI-агентов: практические

паттерны из production-кода Anthropic


Этот гайд написан для тех, кто прямо сейчас проектирует собственных AI-
агентов или инструменты автоматизации. Мы не будем заниматься абстрактной
теорией. Вместо этого мы посмотрим, как решают реальные инженерные
проблемы разработчики из Anthropic в кодовой базе Claude Code.

В основе этого материала лежит анализ более 20 тысяч строк production-кода.


Цель — вытащить оттуда конкретные архитектурные решения, хитрые трюки и
неочевидные tradeoff-ы, которые вы можете применить в своих пайплайнах
(например, для глубокого ресёрча или автоматизации браузера).

Оглавление

1. Как проектировать инструменты (Tools), чтобы агент не ломался

2. Как управлять контекстом в длинных сессиях (Compaction)

3. Архитектура подагентов: как распараллелить глубокий ресёрч

4. Безопасность: как не дать агенту удалить вашу базу данных

5. Как расширять агента: хуки и MCP

6. Как ускорить запуск и работу: трюки из production

1. Как проектировать инструменты (Tools), чтобы


агент не ломался

Когда вы строите агента-оркестратора, его возможности определяются набором


доступных инструментов. В простых туториалах инструмент — это просто
функция, которая принимает строку и возвращает строку. В реальности всё
сложнее.

Влад Куклев | [Link]/prod1337


Проблема раздувания контекста

Если у вашего агента 50+ инструментов (особенно если вы используете MCP-


серверы), системный промпт мгновенно забивается описаниями этих
инструментов. Описание одного сложного MCP-сервера может весить 15-60
килобайт.

Как это решено: Паттерн “Deferred Tools” (отложенная загрузка). Модель


изначально видит только имена доступных инструментов и короткие подсказки
(search hints). Когда агенту нужен конкретный инструмент, он отправляет
специальный блок tool_reference . Система перехватывает его, подгружает
полную JSON-схему инструмента и возвращает её в контекст.

Это снижает потребление токенов на старте сессии на порядки и позволяет


подключать к агенту сотни инструментов одновременно.

Принцип Fail-Closed и фабрика buildTool()

При проектировании интерфейса инструмента критически важно правильно


задать значения по умолчанию. В кодовой базе Claude Code фабрика
buildTool() использует принцип fail-closed:

const TOOL_DEFAULTS = {
isEnabled: () => true,
isConcurrencySafe: (_input?) => false, // НЕ безопасен для параллелизма
isReadOnly: (_input?) => false, // Считается деструктивным
isDestructive: (_input?) => false,
checkPermissions: (input, _ctx?) =>
[Link]({ behavior: 'allow', updatedInput: input }),
toAutoClassifierInput: (_input?) => '', // Пропустить в классификаторе
}

export function buildTool<D extends AnyToolDef>(def: D): BuiltTool<D> {


return { ...TOOL_DEFAULTS, userFacingName: () => [Link], ...def }
}

Обратите внимание: isConcurrencySafe по умолчанию false . Если вы явно не


указали, что инструмент можно запускать параллельно с другими, система будет
выполнять его строго последовательно. isReadOnly тоже false — инструмент

Влад Куклев | [Link]/prod1337


считается потенциально деструктивным, пока не доказано обратное. Этот
подход защищает от случайных race conditions, когда агент решает запустить
чтение файла и его удаление в одном параллельном батче.

Минимальное определение нового инструмента выглядит так:

const MyTool = buildTool({


name: 'MyTool',
maxResultSizeChars: 100_000,
get inputSchema() { return [Link]({ query: [Link]() }) },
async call(input, context) {
const result = await doWork([Link])
return { data: result }
},
async description() { return 'Поиск по базе знаний' },
async prompt() { return 'Используй этот инструмент для...' },
mapToolResultToToolResultBlockParam(content, id) { /* ... */ },
renderToolUseMessage(input, opts) { return null },
})

Все входные данные проходят через Zod-валидацию до любых других проверок.


При ошибке модель получает форматированное сообщение с описанием
ожидаемой схемы — и может исправить свой вызов на следующей итерации.

Параллельное выполнение: StreamingToolExecutor

Когда модель возвращает несколько tool_use блоков в одном ответе, система


решает, что можно запустить параллельно, а что — строго последовательно:

class StreamingToolExecutor {
private canExecuteTool(isConcurrencySafe: boolean): boolean {
const executingTools = [Link](t => [Link] === 'executing')
return (
[Link] === 0 ||
(isConcurrencySafe && [Link](t => [Link]))
)
}
}

Влад Куклев | [Link]/prod1337


Правило простое: concurrent-safe инструменты (Read, Glob, Grep) могут
выполняться параллельно друг с другом. Non-concurrent инструменты (Edit,
Write, Bash) выполняются эксклюзивно. Если один инструмент из параллельной
группы падает с ошибкой, [Link]() отменяет остальные
— но не убивает родительский query loop.

2. Как управлять контекстом в длинных сессиях


(Compaction)

Ограничение контекстного окна — ключевая проблема при разработке агентов.


Когда агент-исследователь анализирует десяток GitHub-репозиториев или
читает длинные PDF-документы, контекст забивается за пару итераций.

Anthropic использует многоуровневый конвейер компактификации (сжатия)


контекста. Каждый следующий уровень применяется только если предыдущий
не помог.

Уровень 1: Tool Result Budget

Никогда не отдавайте агенту сырой результат выполнения команды, если он


слишком большой. Если скрипт парсинга вернул 10 мегабайт текста, модель
превысит лимит токенов или потеряет фокус.

Паттерн: обрезать результат до разумного лимита (например, 10 000 символов).


Полный результат сохраняется на диск в скрытую папку, а в контекст агенту
передаётся усечённая версия плюс абсолютный путь к полному файлу. Если
агенту нужны детали, он использует инструмент чтения файлов, чтобы
прочитать конкретные строки.

Уровень 2: Snip Compact

По мере продвижения диалога старые результаты выполнения инструментов


теряют актуальность. Система проходит по истории сообщений и заменяет
длинные ответы инструментов на плейсхолдер [snipped] . Это
инкрементальная операция, которая не требует вызова LLM.

Влад Куклев | [Link]/prod1337


Уровень 3: Microcompact — три стратегии

В коде реализованы три стратегии microcompact:

Стратегия 1 — Time-based. Если пауза между сообщениями превышает порог


(prompt cache истёк), система очищает старые tool results, сохраняя только
последние N:

function maybeTimeBasedMicrocompact(messages, querySource) {


const trigger = evaluateTimeBasedTrigger(messages, querySource)
if (!trigger) return null

const keepRecent = [Link](1, [Link])


const keepSet = new Set([Link](-keepRecent))
const clearSet = new Set(
[Link](id => ![Link](id))
)
// Заменяем содержимое на stub:
return { ...block, content: '[Old tool result content cleared]' }
}

Стратегия 2 — Cached Microcompact. Если кэш ещё жив, используется API


cache_edits , которая позволяет удалять старые результаты из истории, не
инвалидируя при этом дорогой prompt cache. Это снижает стоимость длинных
сессий за счет сохранения кэша промпта. Фича доступна только через feature
gate:

if (feature('CACHED_MICROCOMPACT')) {
const mod = await getCachedMCModule()
if ([Link]()
&& [Link](model)) {
return await cachedMicrocompactPath(messages, querySource)
}
}

Стратегия 3 — Server-side API Microcompact. Самая новая: вместо модификации


сообщений на клиенте, система передаёт серверу инструкцию
context_management с типом clear_tool_uses_20250919 или
clear_thinking_20251015 , и сервер сам решает, что удалить.

Влад Куклев | [Link]/prod1337


Уровень 4: Auto Compact (LLM-суммаризация)

Когда простые методы исчерпаны, в дело вступает LLM. Конкретные пороги из


кода:

const AUTOCOMPACT_BUFFER_TOKENS = 13_000


const WARNING_THRESHOLD_BUFFER_TOKENS = 20_000
const MANUAL_COMPACT_BUFFER_TOKENS = 3_000

function getAutoCompactThreshold(model: string): number {


const effectiveContextWindow = getEffectiveContextWindowSize(model)
return effectiveContextWindow - AUTOCOMPACT_BUFFER_TOKENS
}

Оценка токенов делается на клиенте с коэффициентом запаса 4⁄3 (conservative


padding). Точный подсчёт через API CountTokens слишком дорог для каждого
решения о компактификации.

Промпт для суммаризации требует структурированного ответа из 9 секций:


Primary Request, Key Technical Concepts, Files and Code Sections, Errors and Fixes,
Problem Solving, All User Messages (вербатим-цитаты — это критично), Pending
Tasks, Current Work, Optional Next Step.

Уровень 5: Session Memory Compact (Zero-API-call)

Самый экономичный вариант. Вместо вызова модели для создания summary,


используется уже существующий Session Memory файл (который модель
обновляет после каждого turn):

Влад Куклев | [Link]/prod1337


const DEFAULT_SM_COMPACT_CONFIG = {
minTokens: 10_000,
minTextBlockMessages: 5,
maxTokens: 40_000,
}

async function trySessionMemoryCompaction(messages, agentId, threshold) {


await waitForSessionMemoryExtraction()
const sessionMemory = await getSessionMemoryContent()
if (!sessionMemory || await isSessionMemoryEmpty(sessionMemory)) {
return null // Fallback на обычный autocompact
}
// Собираем summary из session memory — без API-вызова
}

Инцидент из production: Одно время Auto Compact работал без ограничений.


При сбоях он уходил в бесконечный цикл, тратя до 250 000 API-вызовов в день.
Решение — жёсткий circuit breaker:

const MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3

async function autoCompactIfNeeded(...) {


if (tracking?.consecutiveFailures >= MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES)
{
return { wasCompacted: false } // Сдаёмся
}
}

Комментарий из кода: “BQ 2026-03-10: 1,279 sessions had 50+ consecutive failures
(up to 3,272) in a single session, wasting ~250K API calls/day globally.” Обязательно
реализуйте circuit breaker для любых фоновых LLM-вызовов в ваших пайплайнах.

3. Архитектура подагентов: как распараллелить


глубокий ресёрч

Когда вы строите пайплайн для глубокого исследования (Deep Research), вам


нужен агент-оркестратор, который раздаёт задачи агентам-исследователям. В

Влад Куклев | [Link]/prod1337


коде это реализовано через модуль AgentTool .

Существует два принципиально разных подхода к созданию подагентов.

Fresh Subagent (С чистого листа)

Подагент запускается с пустым контекстом. Вы передаёте ему только системный


промпт и конкретную задачу. Плюсы: Идеальная изоляция, минимальное
потребление токенов. Можно использовать дешёвую модель (например, GPT-4o-
mini или Claude Haiku) для простых задач парсинга. Минусы: Подагент не знает
контекста общей задачи и может задавать глупые вопросы или дублировать
работу.

Fork Subagent (Форк с наследованием)

Это более продвинутый паттерн. Подагент получает точную копию всей истории
сообщений родительского агента на момент создания. Плюсы: Подагент
полностью в контексте. Благодаря механизму prompt caching (когда префикс
сообщений совпадает байт-в-байт), вы экономите от 50% до 80% стоимости
входных токенов, так как история родителя уже закэширована на серверах
провайдера. Минусы: Требует аккуратной работы с состоянием, чтобы подагент
не начал модифицировать те же файлы, что и родитель.

Вот как это выглядит в коде. Ядро подагента — это обычный рекурсивный вызов
того же query() , но с изолированным контекстом:

async function* runAgent({


agentDefinition, promptMessages, toolUseContext,
canUseTool, isAsync, querySource, model, availableTools,
}): AsyncGenerator<Message> {
// 1. Инициализация MCP-серверов агента
// 2. Фильтрация инструментов (через agent restrictions)
// 3. Сборка system prompt
// 4. Регистрация frontmatter hooks
// 5. Рекурсивный вызов query()
// 6. Yield сообщений родителю
// 7. Cleanup (MCP, shell tasks, hooks)
}

Влад Куклев | [Link]/prod1337


Для fork-агентов используется специальная структура CacheSafeParams , которая
гарантирует попадание в prompt cache:

type CacheSafeParams = {
systemPrompt: SystemPrompt
userContext: { [k: string]: string }
systemContext: { [k: string]: string }
toolUseContext: ToolUseContext
forkContextMessages: Message[] // Копия истории родителя
}

// Каждый fork клонирует file state и создаёт независимый denial tracking:


fileStateCache: cloneFileStateCache([Link]),
denialTracking: createDenialTrackingState(),

Изоляция рабочих директорий (Worktrees)

Если ваши исследователи пишут код или скачивают файлы, они могут
конфликтовать друг с другом. Паттерн решения: для каждого агента создаётся
собственный изолированный git worktree в скрытой директории (например,
.claude/worktrees/{slug}/ ). Тяжёлые зависимости (вроде node_modules )
подключаются через симлинки, чтобы не тратить место на диске.

ID задач генерируются с type-prefix и криптографическим рандомом — 36^8


комбинаций (около 2.8 триллиона), что защищает от brute-force symlink-атак:

function generateTaskId(type: TaskType): string {


const prefix = getTaskIdPrefix(type) // 'b'=bash, 'a'=agent, 'r'=remote...
const bytes = randomBytes(8)
// ...
}

Инцидент из production: Отсутствие лимитов на создание агентов привело к


тому, что система породила 292 агента за 2 минуты. Каждый агент потреблял
около 20 мегабайт оперативной памяти (а при активной работе — до 125
мегабайт). Результат: 36.8 гигабайт RSS и падение системы. Урок: Всегда
устанавливайте жесткий hard cap на количество параллельных агентов и

Влад Куклев | [Link]/prod1337


реализуйте механизм graceful rejection (отказ в создании с понятной ошибкой), а
также сборку мусора для завершённых задач.

4. Безопасность: как не дать агенту удалить вашу


базу данных

Даже если ваш агент работает в песочнице, вопросы безопасности критичны —


особенно если он имеет доступ к браузеру или терминалу. В production-системах
безопасность строится по принципу Defense in Depth (глубокоэшелонированная
защита).

YOLO Classifier (ML-классификатор разрешений)

Вместо того чтобы спрашивать пользователя при каждом чихе (что убивает идею
автономности), используется двухэтапный классификатор на базе LLM:

1. Fast decision: Быстрый прогон через легковесную модель.

2. Thinking analysis: Если есть сомнения, модель пишет цепочку рассуждений


(chain of thought), оценивая риски команды.

Важный нюанс защиты от prompt injection: при анализе безопасности текст,


сгенерированный самим агентом-ассистентом, исключается из проверки.
Анализируется только сама команда и её аргументы. Каждый инструмент
реализует метод toAutoClassifierInput() , который возвращает компактное
представление для классификатора. Пустая строка '' означает “пропустить
этот инструмент в классификаторе” — это default.

Многоуровневая валидация (на примере FileEditTool)

Вот реальный pipeline валидации из кода. FileEditTool проходит 11 проверок до


того, как разрешить редактирование:

Влад Куклев | [Link]/prod1337


async validateInput(input, toolUseContext) {
// 1. Team memory secrets check
// 2. old_string === new_string → reject (бессмысленная правка)
// 3. Deny rule check (путь к файлу)
// 4. UNC path security (Windows NTLM credential theft)
// 5. File size check (1 GiB max)
// 6. File existence check
// 7. Previous read check (readFileState)
// 8. Modification timestamp check (файл изменён с момента чтения?)
// 9. old_string existence in file
// 10. Multiple matches without replace_all
// 11. Settings file validation
}

Проверка номер 7 — обязательное требование: модель не может редактировать


файл, не прочитав его. Это предотвращает слепые правки. Проверка номер 8
ловит ситуацию, когда файл был изменён внешним процессом между чтением и
записью.

Ещё одна неочевидная деталь: findActualString() обрабатывает


нормализацию кавычек — модель может отправить " вместо " или ' вместо
' , и система это корректно обработает.

Защита от TOCTOU (Time-of-Check to Time-of-Use)

Если агент просит выполнить команду cat [Link] , а вы её разрешаете, файл


[Link] может быть подменен симлинком на /etc/shadow в момент
выполнения.

Правильная валидация путей включает:

Разрешение всех симлинков до проверки.

Блокировку shell-экспансий (запрет конструкций вроде $() , , ${} ).

Проверку на case-insensitive файловых системах (macOS/Windows).

Запрет NTFS Alternate Data Streams ( [Link]:evil ).

Блокировку device-файлов ( /dev/zero , /dev/random , /dev/urandom ,


/dev/stdin и ещё 9 путей).

Влад Куклев | [Link]/prod1337


5. Как расширять агента: хуки и MCP

Чтобы ваш агент не превратился в монолитный неподдерживаемый кусок кода,


логику взаимодействия с внешним миром нужно выносить наружу.

Model Context Protocol (MCP)

MCP — это стандарт, который позволяет агентам подключаться к внешним


источникам данных (базам, API, локальным файлам) без написания кастомных
интеграций под каждый инструмент. В Claude Code поддерживается 8 типов
транспорта: stdio, SSE, HTTP (Streamable HTTP), WebSocket, SDK, In-Process, IDE-
specific, и [Link] proxy.

Подключение к серверу мемоизируется, чтобы не создавать дубликаты:

export const connectToServer = memoize(


async (name, serverRef, serverStats) => { ... },
(name, serverRef) => `${name}-${[Link](serverRef)}`
)

Каждое MCP-соединение проходит через 5 возможных состояний (discriminated


union):

type MCPServerConnection =
| ConnectedMCPServer // Рабочее соединение
| FailedMCPServer // Ошибка подключения
| NeedsAuthMCPServer // Требуется OAuth
| PendingMCPServer // В процессе подключения
| DisabledMCPServer // Отключен пользователем

Для stdio-серверов реализована трёхступенчатая эскалация завершения —


SIGINT (100ms ожидание) → SIGTERM (400ms) → SIGKILL . Общее время cleanup
ограничено 600ms, чтобы не блокировать CLI. Stderr буферизируется с cap в
64MB, чтобы verbose MCP-серверы не съели всю память.

Ещё одна деталь: при закрытии соединения очищаются все мемоизированные


кеши — tools, resources, commands, и само соединение:

Влад Куклев | [Link]/prod1337


[Link] = () => {
[Link](name)
[Link](name)
[Link](name)
[Link](key)
}

Система хуков

Вместо хардкода логики в основном цикле агента, используются хуки


(PreToolUse, PostToolUse, UserPromptSubmit, PermissionRequest, PreCompact,
PostCompact, SessionStart, SessionEnd, Stop, AgentSpawn).

Система поддерживает шесть типов хуков: command (shell), HTTP, prompt, agent,
callback и function. Самый универсальный — command hook. Его Zod-схема:

const BashCommandHookSchema = [Link]({


type: [Link]('command'),
command: [Link](),
if: IfConditionSchema(),
shell: [Link](['bash', 'powershell']).optional(),
timeout: [Link]().positive().optional(),
once: [Link]().optional(), // Одноразовый хук
async: [Link]().optional(), // Фоновое выполнение
asyncRewake: [Link]().optional(), // Фон + разбудить модель при exit 2
})

Протокол взаимодействия: программа получает JSON на stdin и возвращает


exit code:

0 — success (продолжить выполнение).

2 — blocking error (немедленно остановить операцию, передать stderr


модели).

Любой другой — non-blocking error (показать stderr пользователю,


продолжить).

На Windows bash-хуки выполняются через Git Bash, а не через [Link].


PowerShell-хуки используют pwsh с флагами -NoProfile -NonInteractive .

Влад Куклев | [Link]/prod1337


Хук получает набор переменных окружения: CLAUDE_PROJECT_DIR (корень
проекта), CLAUDE_PLUGIN_ROOT (корень плагина), CLAUDE_PLUGIN_DATA
(директория данных). Это позволяет подключать внешние скрипты (линтеры,
сканеры секретов, валидаторы) к пайплайну агента без изменения его исходного
кода.

Интересная оптимизация: когда все хуки — внутренние (callback/function, без


shell-вызовов), pipeline работает в fast path с задержкой около 1.8 микросекунд
вместо 6 микросекунд для shell-хуков.

6. Как ускорить запуск и работу: трюки из


production

Параллельная инициализация

При запуске агента критически важно минимизировать время до первого ответа.


В Claude Code используется паттерн “fire-and-forget void” — некритичные
операции запускаются параллельно без await :

// Параллельно, без ожидания:


void populateOAuthAccountInfoIfNeeded()
void initJetBrainsDetection()
void detectCurrentRepository()

// Синхронно, строго по порядку (критический путь):


configureGlobalMTLS() // MUST be before first TLS
configureGlobalAgents() // Depends on mTLS
preconnectAnthropicApi() // Fire-and-forget AFTER the above

Функция init() обёрнута в lodash-es/memoize — повторный вызов мгновенно


возвращает кешированный Promise.

Dead Code Elimination через Feature Gates

Для разных сборок (internal vs external) используется система compile-time


feature gates через виртуальный модуль bun:bundle :

Влад Куклев | [Link]/prod1337


import { feature } from 'bun:bundle'

// При сборке для external: feature('VOICE_MODE') → false


// Bun tree-shakes весь код внутри if (false) {...}
const VoiceProvider = feature('VOICE_MODE')
? require('../context/[Link]').VoiceProvider
: ({ children }) => children

Паттерн require(...) as typeof import(...) даёт полную типизацию при


compile-time elimination. Bun не только убирает код, но и строковые литералы
внутри мёртвых блоков не попадают в финальный бинарь — это критично для
security (скрытие внутренних кодовых названий экспериментов).

Zero-copy Stream

Для передачи данных от API к UI используется кастомный Stream<T> — single-


consumer async iterator с zero-copy оптимизацией:

class Stream<T> implements AsyncIterator<T> {


private readonly queue: T[] = []
private readResolve?: (value: IteratorResult<T>) => void

enqueue(value: T): void {


if ([Link]) {
// Consumer уже ждёт — передаём напрямую, без буфера
const resolve = [Link]
[Link] = undefined
resolve({ done: false, value })
} else {
[Link](value)
}
}
}

Если consumer уже ждёт (есть readResolve ), данные передаются напрямую без
промежуточного буфера. Ограничение: stream можно итерировать только один
раз — single-consumer design для предотвращения race conditions.

Влад Куклев | [Link]/prod1337


Deferred Flush для I/O

При записи логов и транскриптов используется createBufferedWriter() с


хитрой оптимизацией: при overflow буфера вместо синхронного writeFn
(который может быть appendFileSync ) вызывается setImmediate() . Это
предотвращает jank mid-render:

function flushDeferred(): void {


const detached = buffer
buffer = []
setImmediate(() => {
writeFn([Link]('')) // Async, не блокирует render
})
}

Заключение

Создание надёжных AI-агентов сильно отличается от написания скриптов-


обёрток вокруг OpenAI API. Главные вызовы кроются не в качестве промптов, а в
инженерии: управлении растущим контекстом, изоляции параллельных задач,
безопасном выполнении сгенерированного кода и обработке неизбежных
ошибок (rate limits, context window exceeded, hallucinations).

Используя паттерны вроде отложенной загрузки инструментов, многоуровневой


компактификации контекста и форкирования подагентов с общим кэшем, вы
сможете строить пайплайны, которые работают стабильно и потребляют
меньше токенов.

Влад Куклев | [Link]/prod1337

Вам также может понравиться