diff --git a/api.md b/api.md new file mode 100644 index 0000000..39beaf4 --- /dev/null +++ b/api.md @@ -0,0 +1,648 @@ +# 📖 MarkClean API — Документация по использованию API-ключа + +> **Версия API:** `v1` +> **Базовый URL:** `https://markclean.online/api` +> **Формат данных:** JSON +> **Аутентификация:** API-ключ (Bearer token / X-API-Key) + +--- + +## 📑 Содержание + +1. [Обзор](#-обзор) +2. [Получение API-ключа](#-получение-api-ключа) +3. [Аутентификация](#-аутентификация) +4. [Конвертация URL в Markdown](#-конвертация-url-в-markdown) +5. [История конвертаций](#-история-конвертаций) +6. [Опции конвертации](#-опции-конвертации) +7. [Обработка ошибок](#-обработка-ошибок) +8. [Квоты и лимиты](#-квоты-и-лимиты) +9. [Примеры кода](#-примеры-кода) +10. [FAQ](#-faq) + +--- + +## 🔍 Обзор + +MarkClean API позволяет программно конвертировать веб-страницы в чистый Markdown, оптимизированный для LLM, RAG-систем и документации. + +**Ключевые возможности:** +- 🌐 Конвертация любой HTML-страницы в Markdown +- 🧹 Автоматическая очистка от мусора (реклама, навигация, футеры) через Readability +- ⚡ Двухуровневое кэширование — повторные запросы не списывают кредиты +- 🎛️ Гибкие опции вывода (заголовок, frontmatter, удаление картинок/ссылок) +- 📊 История всех конвертаций с дедупликацией + +--- + +## 🔑 Получение API-ключа + +API-ключ можно создать **только** через веб-интерфейс (требует авторизации по сессии). + +### Шаги: + +1. Войдите в аккаунт на [markclean.online](https://markclean.online) +2. Перейдите на страницу **API Keys** (или откройте `/api-keys`) +3. Нажмите **"Сгенерировать ключ"** +4. **⚠️ ВАЖНО:** Сохраните ключ немедленно — он будет показан **только один раз!** + +### Формат ключа + +``` +mk_live_Xk9mP2vQ7rT4wL8nZ3sY6tB1cD5fG0hJ +│ └── 32 символа base64url (криптографически случайные) +└── префикс (8 символов) +``` + +**Общая длина:** 40 символов + +> 🔒 **Безопасность:** В базе данных хранится только SHA-256 хеш ключа. Если вы потеряете ключ — восстановить его невозможно, только создать новый. + +--- + +## 🛡️ Аутентификация + +Все запросы к защищённым эндпоинтам должны содержать API-ключ в одном из заголовков: + +### Способ 1: `X-API-Key` (рекомендуется) + +```http +X-API-Key: mk_live_Xk9mP2vQ7rT4wL8nZ3sY6tB1cD5fG0hJ +``` + +### Способ 2: `Authorization: Bearer` + +```http +Authorization: Bearer mk_live_Xk9mP2vQ7rT4wL8nZ3sY6tB1cD5fG0hJ +``` + +Оба способа равнозначны. При отсутствии или невалидности ключа сервер вернёт `401 Unauthorized`. + +--- + +## 🔄 Конвертация URL в Markdown + +Основной эндпоинт API. + +### Запрос + +```http +POST /api/convert +Content-Type: application/json +X-API-Key: mk_live_YOUR_KEY_HERE +``` + +### Тело запроса + +```json +{ + "url": "https://example.com/article", + "include_title": true, + "include_frontmatter": false, + "strip_images": false, + "strip_links": false +} +``` + +| Поле | Тип | Обязательное | По умолчанию | Описание | +|------|-----|:------------:|:------------:|----------| +| `url` | string | ✅ | — | URL веб-страницы для конвертации (http/https) | +| `include_title` | bool | ❌ | `false` | Добавить заголовок страницы как H1 (`# Title`) | +| `include_frontmatter` | bool | ❌ | `false` | Добавить YAML-шапку с метаданными | +| `strip_images` | bool | ❌ | `false` | Удалить все `` теги | +| `strip_links` | bool | ❌ | `false` | Удалить `` теги (текст сохраняется) | + +### Успешный ответ (200 OK) + +```json +{ + "title": "Example Article", + "content": "# Example Article\n\nThis is the converted content...", + "quota_incremented": true +} +``` + +| Поле | Тип | Описание | +|------|-----|----------| +| `title` | string | Заголовок страницы (может отсутствовать) | +| `content` | string | Результат конвертации в Markdown | +| `quota_incremented` | bool | `true` — списан кредит; `false` — cache hit или лимит | + +### Пример cURL + +```bash +curl -X POST https://markclean.online/api/convert \ + -H "Content-Type: application/json" \ + -H "X-API-Key: mk_live_YOUR_KEY_HERE" \ + -d '{ + "url": "https://metanit.com/web/html5/2.6.php", + "include_title": true, + "strip_images": true + }' +``` + +--- + +## 📚 История конвертаций + +Получить список всех конвертаций текущего пользователя с пагинацией. + +### Запрос + +```http +GET /api/converter/history?page=1&page_size=20 +X-API-Key: mk_live_YOUR_KEY_HERE +``` + +| Параметр | Тип | По умолчанию | Описание | +|----------|-----|:------------:|----------| +| `page` | int | `1` | Номер страницы (≥ 1) | +| `page_size` | int | `20` | Количество записей (1–100) | + +### Успешный ответ (200 OK) + +```json +{ + "items": [ + { + "id": "27c1e865-a77e-4d99-87d0-1179aef2a3e6", + "title": "Example Article", + "target_url": "https://example.com/article", + "count_usage": 3, + "created_at": "2026-09-27T10:00:00Z", + "updated_at": "2026-09-27T14:30:00Z" + } + ], + "total_count": 42, + "page": 1, + "page_size": 20, + "has_more": true +} +``` + +| Поле | Тип | Описание | +|------|-----|----------| +| `items` | array | Список конвертаций (отсортированы по `updated_at` DESC) | +| `items[].id` | string | UUID записи | +| `items[].title` | string | Заголовок страницы (или пустая строка) | +| `items[].target_url` | string | Исходный URL | +| `items[].count_usage` | int | Сколько раз этот URL конвертировался | +| `items[].created_at` | string | Первая конвертация (ISO 8601) | +| `items[].updated_at` | string | Последняя конвертация (ISO 8601) | +| `total_count` | int | Общее количество записей | +| `page` | int | Текущая страница | +| `page_size` | int | Размер страницы | +| `has_more` | bool | Есть ли следующие страницы | + +> 💡 **Дедупликация:** Один URL = одна запись. Повторные конвертации увеличивают `count_usage` и обновляют `updated_at`, перемещая запись наверх. + +--- + +## 🎛️ Опции конвертации + +### `include_title: true` + +Добавляет заголовок страницы как H1 в начало документа: + +```markdown +# Example Article + +This is the content... +``` + +### `include_frontmatter: true` + +Добавляет YAML-шапку с метаданными (удобно для статических генераторов типа Hugo/Jekyll): + +```markdown +--- +title: "Example Article" +source: "https://example.com/article" +bytes: 4521 +tokens: 1234 +--- + +# Example Article + +This is the content... +``` + +### `strip_images: true` + +Полностью удаляет все `` теги. Полезно для: +- 🤖 Подготовки текста для LLM (экономия токенов) +- 📄 Создания текстовых версий статей +- 💾 Уменьшения размера результата + +### `strip_links: true` + +Удаляет `` теги, но **сохраняет текст ссылок**: + +**Было:** +```html +Visit our site for more info. +``` + +**Стало:** +```markdown +Visit our site for more info. +``` + +### 🎯 Рекомендуемые комбинации + +| Сценарий | Опции | +|----------|-------| +| **Для LLM / RAG** | `strip_images: true`, `strip_links: true` | +| **Для документации** | `include_title: true`, `include_frontmatter: true` | +| **Для архивирования** | `include_title: true`, `include_frontmatter: true`, `strip_images: false` | +| **Минимальный размер** | `strip_images: true`, `strip_links: true`, `include_title: false` | + +--- + +## ⚠️ Обработка ошибок + +Все ошибки возвращаются в едином формате: + +```json +{ + "error": "machine_readable_code", + "message": "Human-readable description" +} +``` + +### HTTP-статусы + +| Статус | Код ошибки | Описание | Действие | +|:------:|------------|----------|----------| +| `400` | `invalid_payload` | Битый JSON или отсутствует `url` | Проверьте тело запроса | +| `400` | `invalid_url` | Некорректный формат URL | Должен начинаться с `http://` или `https://` | +| `401` | `invalid_api_key` | Ключ отсутствует, невалиден или удалён | Проверьте заголовок `X-API-Key` | +| `401` | `unauthorized` | Нет аутентификации | Передайте API-ключ | +| `402` | `quota_exceeded` | Кредиты закончились + дневной лимит | Купите кредиты в веб-интерфейсе | +| `404` | `key_not_found` | Ключ не найден (для истории) | Создайте ключ заново | +| `415` | `unsupported_content` | URL не возвращает HTML | API работает только с веб-страницами | +| `422` | `conversion_failed` | Не удалось распознать контент | Попробуйте другой URL | +| `429` | `rate_limit_exceeded` | Превышен лимит запросов | Подождите или войдите в аккаунт | +| `502` | `fetch_failed` | Не удалось загрузить страницу | Сайт недоступен или заблокировал запрос | +| `500` | `internal_server_error` | Внутренняя ошибка сервера | Повторите позже | + +### Пример обработки ошибки + +```bash +$ curl -X POST https://markclean.online/api/convert \ + -H "X-API-Key: mk_live_INVALID" \ + -H "Content-Type: application/json" \ + -d '{"url": "https://example.com"}' + +HTTP/1.1 401 Unauthorized +{ + "error": "invalid_api_key", + "message": "Invalid or expired API key" +} +``` + +--- + +## 📊 Квоты и лимиты + +### Система кредитов + +- 💳 **Платные кредиты** — не сгорают, не имеют срока действия +- 📅 **Дневной лимит** (5 запросов/сутки) — сбрасывается каждые 24 часа +- ⚡ **Приоритет списания:** сначала платные кредиты, затем дневной лимит + +### Кэширование и экономия + +API использует **двухуровневое кэширование**: + +| Уровень | Что кэшируется | TTL | Экономия | +|---------|----------------|-----|----------| +| **L1** | Сырой HTML | 30 мин | Нет HTTP-запроса к внешнему сайту | +| **L2** | Финальный Markdown | 5 мин | Мгновенный ответ | + +**Важно:** Кредиты списываются **только** при реальном HTTP-запросе к внешнему сайту. Повторные конвертации того же URL (даже с другими опциями) **бесплатны**, если HTML ещё в кэше. + +### Таблица сценариев + +| Сценарий | Fetch | Кредиты | Скорость | +|----------|:-----:|:-------:|----------| +| Первый запрос URL | ✅ | −1 | ~2-5 сек | +| Повтор с теми же опциями | ❌ | 0 | ~50 мс | +| Повтор с другими опциями | ❌ | 0 | ~200 мс | +| Спустя 30+ минут | ✅ | −1 | ~2-5 сек | + +### Проверка баланса + +Для проверки баланса кредитов используйте веб-интерфейс (эндпоинт `GET /api/billing/credits` доступен только через сессию, не через API-ключ). + +--- + +## 💻 Примеры кода + +### Python (requests) + +```python +import requests + +API_KEY = "mk_live_YOUR_KEY_HERE" +API_BASE = "https://markclean.online/api" + +headers = { + "Content-Type": "application/json", + "X-API-Key": API_KEY, +} + +# Конвертация +response = requests.post( + f"{API_BASE}/convert", + headers=headers, + json={ + "url": "https://example.com/article", + "include_title": True, + "strip_images": True, + }, +) + +if response.ok: + data = response.json() + print(f"Title: {data.get('title')}") + print(f"Credits used: {data['quota_incremented']}") + print(data["content"][:500]) +else: + error = response.json() + print(f"Error {response.status_code}: {error['error']} - {error['message']}") +``` + +### Python (пакетная конвертация) + +```python +import requests +import time + +API_KEY = "mk_live_YOUR_KEY_HERE" +urls = [ + "https://example.com/page1", + "https://example.com/page2", + "https://example.com/page3", +] + +headers = { + "Content-Type": "application/json", + "X-API-Key": API_KEY, +} + +results = [] +for url in urls: + resp = requests.post( + "https://markclean.online/api/convert", + headers=headers, + json={"url": url, "include_title": True}, + ) + if resp.ok: + results.append(resp.json()) + print(f"✅ {url}") + else: + print(f"❌ {url}: {resp.json()['error']}") + time.sleep(0.5) # Вежливый delay + +# Сохраняем в один файл +with open("output.md", "w", encoding="utf-8") as f: + for r in results: + f.write(r["content"] + "\n\n---\n\n") +``` + +### JavaScript / Node.js (fetch) + +```javascript +const API_KEY = 'mk_live_YOUR_KEY_HERE'; +const API_BASE = 'https://markclean.online/api'; + +async function convertUrl(url, options = {}) { + const response = await fetch(`${API_BASE}/convert`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-API-Key': API_KEY, + }, + body: JSON.stringify({ + url, + include_title: options.title ?? false, + include_frontmatter: options.frontmatter ?? false, + strip_images: options.stripImages ?? false, + strip_links: options.stripLinks ?? false, + }), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(`${error.error}: ${error.message}`); + } + + return response.json(); +} + +// Использование +try { + const result = await convertUrl('https://example.com/article', { + title: true, + stripImages: true, + }); + console.log(result.content); +} catch (err) { + console.error('Conversion failed:', err.message); +} +``` + +### Go + +```go +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" +) + +const ( + apiKey = "mk_live_YOUR_KEY_HERE" + apiBase = "https://markclean.online/api" +) + +type ConvertRequest struct { + URL string `json:"url"` + IncludeTitle bool `json:"include_title"` + IncludeFrontmatter bool `json:"include_frontmatter"` + StripImages bool `json:"strip_images"` + StripLinks bool `json:"strip_links"` +} + +type ConvertResponse struct { + Title string `json:"title"` + Content string `json:"content"` + QuotaIncremented bool `json:"quota_incremented"` +} + +func main() { + reqBody, _ := json.Marshal(ConvertRequest{ + URL: "https://example.com/article", + IncludeTitle: true, + StripImages: true, + }) + + req, _ := http.NewRequest("POST", apiBase+"/convert", bytes.NewReader(reqBody)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-API-Key", apiKey) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + panic(err) + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + + if resp.StatusCode != http.StatusOK { + fmt.Printf("Error %d: %s\n", resp.StatusCode, body) + return + } + + var result ConvertResponse + json.Unmarshal(body, &result) + fmt.Println(result.Content) +} +``` + +### cURL (все сценарии) + +```bash +# Базовая конвертация +curl -X POST https://markclean.online/api/convert \ + -H "X-API-Key: mk_live_YOUR_KEY" \ + -H "Content-Type: application/json" \ + -d '{"url": "https://example.com"}' + +# Для LLM (чистый текст) +curl -X POST https://markclean.online/api/convert \ + -H "X-API-Key: mk_live_YOUR_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "url": "https://example.com", + "strip_images": true, + "strip_links": true + }' + +# Для документации (с метаданными) +curl -X POST https://markclean.online/api/convert \ + -H "X-API-Key: mk_live_YOUR_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "url": "https://example.com", + "include_title": true, + "include_frontmatter": true + }' + +# История конвертаций +curl -X GET "https://markclean.online/api/converter/history?page=1&page_size=10" \ + -H "X-API-Key: mk_live_YOUR_KEY" +``` + +--- + +## ❓ FAQ + +### 🔐 Безопасно ли передавать API-ключ? + +**Да**, если соблюдаете правила: +- ✅ Используйте HTTPS (всегда) +- ✅ Храните ключ в переменных окружения, не в коде +- ✅ Не логируйте ключ +- ❌ Не коммитьте ключ в Git +- ❌ Не передавайте ключ в URL-параметрах + +### 🔄 Что делать, если ключ скомпрометирован? + +1. Откройте `/api-keys` в веб-интерфейсе +2. Нажмите **"Пересоздать"** (старый ключ будет удалён, создан новый) +3. Обновите ключ во всех ваших приложениях + +> ⚠️ Старый ключ перестаёт работать мгновенно после пересоздания. + +### 💳 Можно ли использовать API без кредитов? + +Да, у каждого пользователя есть **бесплатный дневной лимит** (5 запросов/сутки), который сбрасывается каждые 24 часа. После исчерпания и дневного лимита, и кредитов — API вернёт `402 quota_exceeded`. + +### ⚡ Почему `quota_incremented: false`? + +Это нормально в трёх случаях: +1. **Cache hit** — этот URL уже конвертировался недавно (бесплатно) +2. **Анонимный запрос** — без API-ключа (лимит по IP) +3. **Ошибка инкремента** — редкая системная ошибка (запрос всё равно выполнен) + +### 🌍 Работает ли API с любым сайтом? + +API работает с любыми **публично доступными HTML-страницами**. Ограничения: +- ❌ Сайты, требующие авторизации +- ❌ Сайты с жёсткой анти-бот защитой (Cloudflare challenge и т.п.) +- ❌ PDF, изображения и другие не-HTML форматы +- ❌ Сайты, блокирующие по User-Agent + +### 📦 Есть ли лимит на размер страницы? + +- Максимум: **5 MB** контента после конвертации +- Типичные статьи: 10-100 KB +- Длинные технические документы: 200-500 KB + +Страницы больше 5 MB не кэшируются, но всё равно конвертируются. + +### 🔗 Как использовать API-ключ с другими инструментами? + +**Postman:** +- Вкладка **Authorization** → Type: **API Key** +- Key: `X-API-Key`, Value: ваш ключ, Add to: **Header** + +**Insomnia:** +- Request → **Auth** → **API Key** +- Header name: `X-API-Key` + +**VS Code (REST Client):** +```http +@apiKey = mk_live_YOUR_KEY + +POST https://markclean.online/api/convert +Content-Type: application/json +X-API-Key: {{apiKey}} + +{ + "url": "https://example.com" +} +``` + +### 🗑️ Что будет с API-ключом после удаления аккаунта? + +**Ключ продолжит работать**, пока не закончатся кредиты. После удаления аккаунта: +- ✅ API-ключ остаётся активным +- ✅ Существующие кредиты можно использовать +- ❌ Пополнить кредиты нельзя (нет доступа к UI) +- ❌ Создать новый ключ нельзя + +Это сделано для честности: вы оплатили кредиты — вы их используете. + +### 🛠️ Есть ли SDK? + +Официальных SDK пока нет, но API достаточно простой для использования через стандартные HTTP-клиенты в любом языке. Примеры выше покрывают самые популярные языки. + +--- + +## 📞 Поддержка + +- 📧 **Email:** gnutenko.s.r@gmail.com +- 💬 **Telegram:** [@Slayder12](https://t.me/Slayder12) +- 📝 **Changelog:** [markclean-doc/changelog.md](https://gitea.gsrhost.ru/gsr/markclean-doc/src/branch/main/changelog.md) + +--- + +**Последнее обновление:** 27 сентября 2026 +**Версия API:** `0.7.5-beta` \ No newline at end of file