Media Upload Skill
Загрузка изображений и медиафайлов в S3-совместимое хранилище через mc (MinIO Client) с получением публичной ссылки.
⚠️ CRITICAL: Приоритет URL
ОБЯЗАТЕЛЬНО после загрузки файла:
Если в конфиге есть
public_urlиurl_modeНЕ равен"presigned":- СРАЗУ возвращай короткий CDN URL:
${public_url}/${remote_path} - НЕ генерируй presigned URL
- СРАЗУ возвращай короткий CDN URL:
Только если
url_mode: "presigned"илиpublic_urlотсутствует:- Генерируй presigned URL через
mc share download
- Генерируй presigned URL через
Пример правильного поведения:
Конфиг: { "public_url": "https://cdn.example.com", "url_mode": "public" }
Путь: 2026/02/03/screenshot.png
→ Возвращай: https://cdn.example.com/2026/02/03/screenshot.png
→ НЕ возвращай: https://s3...?X-Amz-Algorithm=...
Поддерживаемые форматы
| Тип | Расширения | MIME-type |
|---|---|---|
| Изображения | png, jpg, jpeg, gif, webp, svg | image/* |
| Документы | application/pdf | |
| Видео | mp4, webm (будущее) | video/* |
Приоритет определения источника файла
- Явный путь — пользователь указал
/path/to/file.pngв запросе - Playwright MCP — последний результат
browser_take_screenshotв текущей сессии - Glob паттерн — "загрузи все png из ./screenshots/"
- Спросить пользователя — если ничего не найдено
Интеграция с Playwright MCP
ВАЖНО: Этот скилл должен активироваться АВТОМАТИЧЕСКИ после создания скриншота через Playwright MCP.
Паттерны запросов пользователя
Когда пользователь просит:
- "открой сайт X и сделай скриншот"
- "сделай скриншот страницы Y"
- "take a screenshot of Z"
Алгоритм:
- Загрузить инструменты Playwright MCP через ToolSearch
- Открыть браузер и перейти на страницу (
browser_navigate) - Сделать скриншот (
browser_take_screenshot) - АВТОМАТИЧЕСКИ загрузить скриншот в S3 (этот скилл)
- Вернуть пользователю публичную ссылку
Формат ответа Playwright
Playwright MCP при вызове browser_take_screenshot возвращает:
Took the viewport screenshot and saved it as /tmp/page-2024-01-31-143052.png
Парси этот формат регулярным выражением:
SCREENSHOT_PATH=$(echo "$PLAYWRIGHT_OUTPUT" | grep -oP 'saved it as \K/[^\s]+\.png')
Пример полного флоу
User: "открой kiiiosk.store и сделай скриншот"
1. ToolSearch: load playwright tools
2. browser_navigate: https://kiiiosk.store
3. browser_take_screenshot → /tmp/page-2026-02-01-123456.png
4. media-upload skill activates automatically
5. MC_REGION=ru-3 mc cp --insecure /tmp/page-... screenshots/claude-screenshots/2026/02/01/...
6. Return: "✅ Screenshot uploaded: https://..."
Конфигурация
Файл конфигурации
Путь: ~/.config/claude-code/media-upload.json
С CDN (рекомендуется):
{
"mc_path": "screenshots/claude-screenshots",
"public_url": "https://cdn.example.com",
"url_mode": "public",
"organize_by": "date",
"max_file_size_mb": 100
}
Без CDN (только presigned):
{
"mc_path": "screenshots/claude-screenshots",
"url_mode": "presigned",
"presigned_expire": "168h",
"organize_by": "date",
"max_file_size_mb": 100
}
Примечание: Если есть public_url, используй url_mode: "public" для коротких постоянных ссылок.
Режимы URL (url_mode)
| Режим | Описание |
|---|---|
public |
Использовать публичный URL (требует CDN домен, не S3 API) |
presigned |
Рекомендуется для Selectel. Генерировать presigned URL через mc share |
auto |
Проверить публичный доступ, fallback на presigned |
presigned_expire — время жизни presigned URL (по умолчанию 168h = 7 дней, максимум для mc)
⚠️ ВАЖНО для Selectel Cloud Storage:
- S3 API Selectel НЕ поддерживает анонимный доступ — все запросы должны быть подписаны
- Настройка "Публичный" в консоли Selectel работает только через домен
selstorage.ru, не через S3 API - Для S3-совместимого доступа используйте
presignedрежим
Переменные окружения (высший приоритет)
MEDIA_UPLOAD_MC_PATH=screenshots/claude-screenshots
MEDIA_UPLOAD_PUBLIC_URL=https://s3.ru-3.storage.selcloud.ru/claude-screenshots
MEDIA_UPLOAD_ORGANIZE_BY=date
MEDIA_UPLOAD_MAX_FILE_SIZE_MB=100
MEDIA_UPLOAD_HISTORY_FILE=~/.media-upload-history.json
MEDIA_UPLOAD_URL_MODE=auto
MEDIA_UPLOAD_PRESIGNED_EXPIRE=168h
Приоритет конфигурации: Environment variables > JSON config > Default values
Организация файлов в S3
# organize_by: "date" (по умолчанию)
screenshots/2024/01/31/screenshot-2024-01-31-143052.png
# organize_by: "type"
screenshots/images/screenshot-2024-01-31-143052.png
screenshots/documents/report.pdf
# organize_by: "flat"
screenshots/screenshot-2024-01-31-143052.png
Security
Credentials
- Credentials хранятся только в
mc alias(настраивается черезmc alias set) - Никогда не хранить access/secret keys в
media-upload.json - Минимальные IAM права для mc alias:
PutObjectна конкретный bucket
Валидация файлов
- Проверка magic bytes файла (соответствие расширению)
- Санитизация имени файла: удаление
../, замена спецсимволов - Пример:
my screenshot (1).png→my-screenshot-1.png
Права доступа
- Config файлы создаются с правами 600 (только владелец)
Лимиты размера файлов
| Порог | Действие |
|---|---|
| > 10 MB | Warning: показать размер |
| > 50 MB | Confirmation: запросить подтверждение |
| > 100 MB | Block: отказать (настраивается в конфигурации) |
mc CLI Contract
Selectel Cloud Storage: Обязательные настройки
⚠️ КРИТИЧЕСКИ ВАЖНО для Selectel:
MinIO Client по умолчанию использует eu-central-1 в подписи, а Selectel требует ru-3.
Вариант 1: Добавить регион в конфиг mc (рекомендуется)
# Добавить регион в существующий alias через jq
jq '.aliases.screenshots.region = "ru-3"' ~/.mc/config.json > /tmp/mc-config.json && \
mv /tmp/mc-config.json ~/.mc/config.json
Вариант 2: Через переменную окружения
export MC_REGION=ru-3
mc cp LOCAL_FILE ALIAS/BUCKET/PATH
SSL проблемы: Если есть ошибки SSL certificate, использовать --insecure:
mc cp --insecure LOCAL_FILE ALIAS/BUCKET/PATH
Используемые команды
# Проверка алиаса
mc alias list | grep ALIAS_NAME
# Загрузка файла (Selectel)
MC_REGION=ru-3 mc cp LOCAL_FILE ALIAS/BUCKET/PATH
# Загрузка файла (Selectel с SSL проблемами)
MC_REGION=ru-3 mc cp --insecure LOCAL_FILE ALIAS/BUCKET/PATH
# Проверка соединения (Setup Wizard)
MC_REGION=ru-3 mc ls ALIAS/BUCKET --limit 1
Exit codes
0— успех1— ошибка (детали в stderr)
Алгоритм загрузки
Шаг 1: Проверка конфигурации
# Проверить наличие конфигурации
CONFIG_FILE="$HOME/.config/claude-code/media-upload.json"
if [[ ! -f "$CONFIG_FILE" ]]; then
# Запустить Setup Wizard
fi
Шаг 2: Чтение конфигурации
# Проверить наличие jq
if ! command -v jq &> /dev/null; then
echo "❌ jq не установлен (требуется для чтения конфигурации)"
echo "Установка: brew install jq / apt install jq"
exit 1
fi
# Проверить валидность JSON
if ! jq empty "$CONFIG_FILE" 2>/dev/null; then
echo "❌ Невалидный JSON в $CONFIG_FILE"
exit 1
fi
# Приоритет: ENV > JSON > defaults
MC_PATH="${MEDIA_UPLOAD_MC_PATH:-$(jq -r '.mc_path // empty' "$CONFIG_FILE")}"
PUBLIC_URL="${MEDIA_UPLOAD_PUBLIC_URL:-$(jq -r '.public_url // empty' "$CONFIG_FILE")}"
ORGANIZE_BY="${MEDIA_UPLOAD_ORGANIZE_BY:-$(jq -r '.organize_by // "date"' "$CONFIG_FILE")}"
MAX_SIZE="${MEDIA_UPLOAD_MAX_FILE_SIZE_MB:-$(jq -r '.max_file_size_mb // 100' "$CONFIG_FILE")}"
# Проверить обязательные поля
if [[ -z "$MC_PATH" ]]; then
echo "❌ mc_path не задан в конфигурации"
exit 1
fi
# public_url необязателен для режима presigned
Шаг 3: Валидация файла
# Проверить существование и доступность
if [[ ! -f "$FILE" ]] || [[ ! -r "$FILE" ]]; then
echo "❌ Файл не найден или недоступен для чтения: $FILE"
exit 1
fi
# Проверить пустой файл
if [[ ! -s "$FILE" ]]; then
echo "❌ Файл пустой, загрузка отменена"
exit 1
fi
# Получить размер файла (кроссплатформенно)
if ! SIZE_BYTES=$(stat -f%z "$FILE" 2>/dev/null || stat -c%s "$FILE" 2>/dev/null); then
echo "❌ Не удалось получить размер файла: $FILE"
exit 1
fi
if ! [[ "$SIZE_BYTES" =~ ^[0-9]+$ ]]; then
echo "❌ Ошибка определения размера файла: $FILE"
exit 1
fi
SIZE_MB=$(( SIZE_BYTES / 1048576 ))
if [[ $SIZE_MB -gt $MAX_SIZE ]]; then
echo "❌ Файл слишком большой: ${SIZE_MB}MB > ${MAX_SIZE}MB"
exit 1
fi
Шаг 4: Санитизация имени файла
sanitize_filename() {
local filename="$1"
# Удалить путь, оставить только имя
filename="${filename##*/}"
# Заменить пробелы и спецсимволы на дефисы
filename=$(echo "$filename" | sed 's/[^a-zA-Z0-9._-]/-/g' | sed 's/--*/-/g' | sed 's/-$//')
echo "$filename"
}
Шаг 5: Формирование пути в S3
EXT="${FILE##*.}"
DATE_PATH=$(date +%Y/%m/%d)
SANITIZED_NAME=$(sanitize_filename "$FILE")
case "$ORGANIZE_BY" in
date) REMOTE_PATH="${DATE_PATH}/${SANITIZED_NAME}" ;;
type)
# Определить тип по расширению
case "${EXT,,}" in
pdf) TYPE_DIR="documents" ;;
png|jpg|jpeg|gif|webp|svg) TYPE_DIR="images" ;;
mp4|webm) TYPE_DIR="videos" ;;
*) TYPE_DIR="other" ;;
esac
REMOTE_PATH="${TYPE_DIR}/${SANITIZED_NAME}"
;;
flat) REMOTE_PATH="${SANITIZED_NAME}" ;;
esac
Шаг 6: Загрузка через mc
# Проверить наличие региона в конфиге mc
MC_ALIAS=$(echo "$MC_PATH" | cut -d'/' -f1)
MC_HAS_REGION=$(jq -r ".aliases.${MC_ALIAS}.region // empty" ~/.mc/config.json 2>/dev/null)
# Если региона нет в конфиге, попробовать добавить (для Selectel)
if [[ -z "$MC_HAS_REGION" ]] && [[ "$MC_ALIAS" == "screenshots" ]]; then
# Добавить регион ru-3 для Selectel
jq ".aliases.${MC_ALIAS}.region = \"ru-3\"" ~/.mc/config.json > /tmp/mc-config.json && \
mv /tmp/mc-config.json ~/.mc/config.json
echo "ℹ️ Добавлен регион ru-3 в конфиг mc для Selectel"
fi
# Захватить stderr для диагностики
MC_OUTPUT=$(mc cp "$FILE" "${MC_PATH}/${REMOTE_PATH}" 2>&1)
MC_EXIT=$?
# Если ошибка региона - попробовать с MC_REGION
if [[ $MC_EXIT -ne 0 ]] && echo "$MC_OUTPUT" | grep -q "region.*wrong"; then
echo "ℹ️ Повторная попытка с MC_REGION=ru-3"
MC_OUTPUT=$(MC_REGION=ru-3 mc cp "$FILE" "${MC_PATH}/${REMOTE_PATH}" 2>&1)
MC_EXIT=$?
fi
# Если SSL ошибка - попробовать с --insecure
if [[ $MC_EXIT -ne 0 ]] && echo "$MC_OUTPUT" | grep -qi "ssl\|certificate"; then
echo "ℹ️ Повторная попытка с --insecure"
MC_OUTPUT=$(mc cp --insecure "$FILE" "${MC_PATH}/${REMOTE_PATH}" 2>&1)
MC_EXIT=$?
fi
if [[ $MC_EXIT -ne 0 ]]; then
echo "❌ Ошибка загрузки файла: $FILE"
echo ""
echo "Детали ошибки:"
echo "$MC_OUTPUT"
exit 1
fi
Шаг 6.1: Генерация URL
URL_MODE="${MEDIA_UPLOAD_URL_MODE:-$(jq -r '.url_mode // "auto"' "$CONFIG_FILE")}"
PRESIGNED_EXPIRE="${MEDIA_UPLOAD_PRESIGNED_EXPIRE:-$(jq -r '.presigned_expire // "168h"' "$CONFIG_FILE")}"
generate_url() {
local remote_path="$1"
local public_url_full="${PUBLIC_URL}/${remote_path}"
case "$URL_MODE" in
public)
echo "$public_url_full"
;;
presigned)
# Генерировать presigned URL
mc share download --expire="$PRESIGNED_EXPIRE" "${MC_PATH}/${remote_path}" 2>&1 | \
grep "^Share:" | cut -d' ' -f2
;;
auto|*)
# Проверить публичный доступ
HTTP_CODE=$(curl -sI -o /dev/null -w "%{http_code}" "$public_url_full" 2>/dev/null || echo "000")
if [[ "$HTTP_CODE" == "200" ]]; then
echo "$public_url_full"
else
# Fallback на presigned
mc share download --expire="$PRESIGNED_EXPIRE" "${MC_PATH}/${remote_path}" 2>&1 | \
grep "^Share:" | cut -d' ' -f2
fi
;;
esac
}
PUBLIC_URL_FULL=$(generate_url "$REMOTE_PATH")
Шаг 7: Запись в историю
# Получить путь к файлу истории (ENV > JSON config > default)
HISTORY_FILE="${MEDIA_UPLOAD_HISTORY_FILE:-$(jq -r '.history_file // empty' "$CONFIG_FILE" 2>/dev/null | sed "s|~|$HOME|")}"
HISTORY_FILE="${HISTORY_FILE:-$HOME/.media-upload-history.json}"
# Создать файл если не существует
if [[ ! -f "$HISTORY_FILE" ]]; then
echo '{"uploads":[]}' > "$HISTORY_FILE"
chmod 600 "$HISTORY_FILE"
fi
# Добавить запись (через jq)
# SIZE_BYTES определен в Шаге 3
if ! jq --arg file "$FILE" \
--arg url "$PUBLIC_URL_FULL" \
--arg size "$SIZE_BYTES" \
--arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
'.uploads += [{"file":$file,"url":$url,"size":($size|tonumber),"timestamp":$ts}]' \
"$HISTORY_FILE" > "${HISTORY_FILE}.tmp"; then
echo "⚠️ Загрузка успешна, но запись в историю не удалась"
rm -f "${HISTORY_FILE}.tmp"
elif ! mv "${HISTORY_FILE}.tmp" "$HISTORY_FILE"; then
echo "⚠️ Не удалось сохранить историю: ошибка перемещения файла"
rm -f "${HISTORY_FILE}.tmp"
fi
Setup Wizard (первый запуск)
Условие запуска:
- Отсутствует файл
~/.config/claude-code/media-upload.json - ИЛИ отсутствуют обязательные поля:
mc_path,public_url
Процесс
- Проверить установку mc:
if ! command -v mc &> /dev/null; then
echo "❌ mc не установлен"
echo "Установка:"
echo " macOS: brew install minio-mc"
echo " Linux: apt install minio-mc"
exit 1
fi
echo "✅ mc is installed"
- Запросить параметры через AskUserQuestion:
mc_path(alias/bucket): напримерscreenshots/screenshotspublic_url: напримерhttps://cdn.example.com/screenshots
- Проверить соединение:
mc ls "${MC_PATH}" --limit 1
if [[ $? -ne 0 ]]; then
echo "❌ Не удалось подключиться к ${MC_PATH}"
echo "Проверьте настройку alias: mc alias set ..."
exit 1
fi
echo "✅ Bucket accessible"
- Сохранить конфигурацию:
mkdir -p ~/.config/claude-code
cat > ~/.config/claude-code/media-upload.json << EOF
{
"mc_path": "${MC_PATH}",
"public_url": "${PUBLIC_URL}",
"organize_by": "date",
"max_file_size_mb": 100
}
EOF
chmod 600 ~/.config/claude-code/media-upload.json
echo "✅ Configuration saved"
Batch Upload
При загрузке нескольких файлов:
# Пример: загрузи все png из ./screenshots/
# Защита от пустого glob
shopt -s nullglob
files=(./screenshots/*.png)
shopt -u nullglob
if [[ ${#files[@]} -eq 0 ]]; then
echo "❌ Файлы не найдены: ./screenshots/*.png"
exit 1
fi
success=()
failed=()
for file in "${files[@]}"; do
if upload_file "$file"; then
success+=("$file")
else
failed+=("$file")
fi
done
Частичный успех
При ошибке части файлов показать:
- Успешные с URL
- Неуспешные с причиной ошибки
- Не откатывать успешные загрузки
Формат вывода:
⚠️ Uploaded 3 of 5 files:
✅ Успешно:
- screenshot-1.png → https://cdn...
- screenshot-2.png → https://cdn...
- screenshot-3.png → https://cdn...
❌ Ошибки:
- screenshot-4.png: Connection timeout
- screenshot-5.png: File too large (120MB > 100MB limit)
История загрузок
Файл: ~/.media-upload-history.json
{
"uploads": [
{
"file": "/tmp/page-2024-01-31-143052.png",
"url": "https://cdn.example.com/screenshots/2024/01/31/screenshot-2024-01-31-143052.png",
"size": 245678,
"timestamp": "2024-01-31T14:30:52Z"
}
]
}
Просмотр истории:
jq '.uploads[-10:]' ~/.media-upload-history.json
Выход после успешной загрузки
Одиночный файл:
Публичный URL (короткий, постоянный):
✅ Image uploaded!
URL: https://s3.example.com/bucket/2024/01/31/screenshot.png
Markdown: 
Size: 245 KB
Presigned URL (длинный, временный):
✅ Image uploaded!
URL: https://s3.example.com/bucket/2024/01/31/screenshot.png?X-Amz-...
⏰ Expires: 7 days
Markdown: 
Size: 245 KB
Tip: Настройте публичный доступ к bucket для коротких постоянных ссылок
Batch:
✅ 5 images uploaded!
| File | URL | Size |
|------|-----|------|
| screenshot-1.png | https://cdn... | 120 KB |
| screenshot-2.png | https://cdn... | 245 KB |
| ... | ... | ... |
Total: 1.2 MB
Markdown (all):


Edge Cases
| Ситуация | Действие |
|---|---|
mc не установлен |
Показать brew install minio-mc / apt install minio-mc |
| Alias не настроен | Показать mc alias set screenshots ... |
| Файл не найден | Спросить путь явно |
| Неподдерживаемый формат | Предупредить, но загрузить если попросят |
| Файл слишком большой | Показать размер, запросить подтверждение (>50MB) или отказать (>100MB) |
| Нет Playwright скриншотов | Спросить путь явно |
| Ошибка загрузки | Показать stderr от mc |
| Спецсимволы в имени | Санитизировать: my file (1).png → my-file-1.png |
| Пустой файл (0 байт) | Показать ошибку: "Файл пустой, загрузка отменена" |
| Bucket приватный (403) | В режиме auto — fallback на presigned URL |
| Presigned URL слишком длинный | Рекомендовать настроить публичный bucket |
| SSL ошибка при проверке | Использовать curl -k или сразу presigned |
| Region mismatch (Selectel) | Установить MC_REGION=ru-3 перед командами mc |
| SSL certificate verify failed | Использовать mc cp --insecure или aws --no-verify-ssl |
Рекомендации по настройке публичного доступа
Для коротких URL без presigned подписей:
Selectel Cloud Storage
⚠️ ВАЖНО: S3 API Selectel НЕ поддерживает анонимный доступ!
Даже если бакет настроен как "Публичный", запросы через S3 API (s3.ru-3.storage.selcloud.ru) требуют подписи.
Варианты решения:
Presigned URLs (рекомендуется):
- Используйте
url_mode: "presigned"в конфигурации - Максимальный срок — 7 дней (ограничение mc/S3)
- URL длинный, но гарантированно работает
- Используйте
Домен selstorage.ru (публичный хостинг):
- В консоли включите "Веб-сайт" для бакета
- URL формата:
https://UUID.selstorage.ru/path/file.png - Требует дополнительной настройки и может иметь проблемы с SSL
CDN Selectel (лучший вариант для продакшена):
- Подключите CDN к бакету
- URL через
*.selcdn.ruподдерживает анонимный доступ - Быстрее и надёжнее
Подробнее: https://qna.habr.com/q/1147270
MinIO
mc anonymous set download ALIAS/BUCKET
AWS S3
aws s3api put-bucket-policy --bucket BUCKET --policy '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::BUCKET/*"
}]
}'