Add Clean PDF tool design spec
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,111 @@
|
|||||||
|
# Чистый PDF — инструмент «URL → PDF» в LMS
|
||||||
|
|
||||||
|
Дата: 20260706
|
||||||
|
Статус: дизайн утверждается
|
||||||
|
Референс: Clean PDF (`pdf.brainysnipe.ru`) из поста https://t.me/flowing_abyss/94, инструкция https://flowing-abyss.com/Clean-PDF-for-Zotero
|
||||||
|
|
||||||
|
## Что делаем
|
||||||
|
|
||||||
|
Инструмент для студентов школы: превращает любую веб-страницу в чистый PDF — без рекламы, меню и мусора, с нормальной типографикой, оглавлением, форматами A4/Letter и светлой/тёмной темой. Два сценария плюс открытый API:
|
||||||
|
|
||||||
|
1. **Веб-форма** в кабинете (`/tools/clean-pdf`): вставил URL — скачал PDF.
|
||||||
|
2. **Zotero**: персональный API-ключ + скрипт для плагина Actions & Tags — правый клик на записи → PDF генерируется и прикрепляется к ней.
|
||||||
|
3. **Открытый API** для продвинутых: `curl` с Bearer-ключом, документация на странице инструмента.
|
||||||
|
|
||||||
|
## Принятые решения
|
||||||
|
|
||||||
|
| Вопрос | Решение |
|
||||||
|
|---|---|
|
||||||
|
| Архитектура | Всё внутри LMS (подход B): страница и API в Next.js, рендер — отдельный контейнер browserless в том же compose |
|
||||||
|
| Кому доступ | Любой платный студент: есть `CourseEnrollment` на курс со slug ≠ `FREE_COURSE_SLUG`; admin/curator — без ограничений |
|
||||||
|
| Видимость | Часть Obsidian Toolbox, живёт под существующим флагом `TOOLBOX_VISIBLE`; включение на проде — отдельное решение позже |
|
||||||
|
| Лимит | 100 генераций/месяц на студента, env `PDF_MONTHLY_LIMIT` (без пересборки), плюс burst-лимит 5/мин |
|
||||||
|
|
||||||
|
## Пайплайн генерации
|
||||||
|
|
||||||
|
```
|
||||||
|
URL студента
|
||||||
|
→ Chromium (browserless) загружает страницу (JS отрабатывает)
|
||||||
|
→ Next.js забирает итоговый HTML
|
||||||
|
→ Defuddle (npm, через JSDOM) выделяет главный контент
|
||||||
|
→ чистый HTML-шаблон: типографика, A4/Letter, light/dark, оглавление из заголовков
|
||||||
|
→ Playwright page.pdf() в том же browserless
|
||||||
|
→ файл application/pdf в ответ
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Defuddle** — библиотека kepano (автор Obsidian), та же, что у референса: https://github.com/kepano/defuddle
|
||||||
|
- **browserless/chromium** — отдельный контейнер в docker-compose: `MAX_CONCURRENT_SESSIONS=2`, встроенная очередь, `mem_limit` ~1 ГБ, порт наружу не публикуется.
|
||||||
|
- Next.js подключается по WebSocket через `playwright-core` (только клиент, Chromium в образ LMS не попадает — образ не растёт).
|
||||||
|
- Таймаут всего пайплайна ~90 сек; понятные JSON-ошибки: URL недоступен, лимит исчерпан, нет платного доступа.
|
||||||
|
|
||||||
|
## Данные (Prisma)
|
||||||
|
|
||||||
|
Новая модель:
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
model PdfApiKey {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
userId String @unique
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
key String @unique // формат sbpdf_<random>, показывается студенту
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Ключ создаётся лениво при первом заходе на страницу инструмента.
|
||||||
|
- Перегенерация заменяет ключ (старый перестаёт работать сразу).
|
||||||
|
- Учёт использований — существующая `ToolUsage` с `tool = "clean-pdf"`; месячный счётчик = COUNT за календарный месяц.
|
||||||
|
- Проверка платного доступа выполняется на каждый запрос → рефанд или блокировка аккаунта автоматически отключает и веб, и ключ. Источник истины один — БД LMS.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
`GET /api/pdf?url=<...>&format=A4|Letter&theme=light|dark`
|
||||||
|
|
||||||
|
- **Авторизация, два пути**: заголовок `Authorization: Bearer sbpdf_...` (Zotero, curl) **или** активная сессия Better Auth (веб-форма — студенту ключ для веба не нужен).
|
||||||
|
- Роут добавляется в `PUBLIC_ROUTES` в `middleware.ts` (иначе 307 → /login — известные грабли).
|
||||||
|
- Ответ: `application/pdf` + заголовки `X-Uses-Count` / `X-Max-Uses` (их же показывает Zotero-скрипт).
|
||||||
|
- Ошибки: JSON `{ error }` c честными статусами (401 нет ключа/сессии, 403 нет платного доступа, 429 лимит, 422 плохой URL, 504 не отрендерилось).
|
||||||
|
|
||||||
|
## Страница /tools/clean-pdf
|
||||||
|
|
||||||
|
По образцу существующих инструментов тулбокса (реестр `TOOLS`, `ToolCard`, страница + клиентская форма):
|
||||||
|
|
||||||
|
1. **Форма**: поле URL, выбор формата и темы, кнопка «Скачать PDF», счётчик «использовано N из 100 в этом месяце».
|
||||||
|
2. **Блок «Zotero»**: персональный ключ (показать/скопировать/перегенерировать), пошаговая инструкция по Actions & Tags, готовый скрипт с уже подставленными адресом школы и ключом студента (адаптация скрипта референса: `SERVICE_URL = https://school.second-brain.ru`, endpoint `/api/pdf`).
|
||||||
|
3. **Блок «API»**: пример curl.
|
||||||
|
|
||||||
|
Дизайн — строго ДС-2 (перед вёрсткой перечитать `SBT/02-Стандарты/Дизайн-LMS/DESIGN.md`).
|
||||||
|
|
||||||
|
## Безопасность
|
||||||
|
|
||||||
|
Сервис скачивает произвольные URL изнутри прод-стенда — главный риск SSRF:
|
||||||
|
|
||||||
|
- Только `http://` и `https://`.
|
||||||
|
- Перед загрузкой — резолв DNS и блокировка приватных/зарезервированных диапазонов: localhost/127.x, 10.x, 172.16–31.x, 192.168.x, 169.254.x (метаданные облаков), ::1, fc00::/7, плюс docker-хостнеймы стенда (`db`, `app`, `browserless`).
|
||||||
|
- Внутри browserless — перехват сетевых запросов страницы с той же фильтрацией (защита от редиректов и подгрузок на внутренние адреса).
|
||||||
|
- Порт browserless наружу не публикуется, доступен только приложению по внутренней сети compose.
|
||||||
|
- Куки/учётные данные пользователя на целевую страницу не передаются.
|
||||||
|
- Санитайз имени файла в `Content-Disposition`.
|
||||||
|
- Лимиты: 100/мес (env) + burst 5/мин на пользователя; сверху — очередь browserless (2 конкурентных рендера).
|
||||||
|
|
||||||
|
## Деплой
|
||||||
|
|
||||||
|
- `docker-compose.yml` (dev) и `docker-compose.prod.yml`: сервис `browserless` (образ `ghcr.io/browserless/chromium`), внутренняя сеть, лимиты памяти.
|
||||||
|
- env: `BROWSER_WS_URL`, `PDF_MONTHLY_LIMIT=100`, существующий `TOOLBOX_VISIBLE`.
|
||||||
|
- Prisma-миграция (`PdfApiKey`).
|
||||||
|
- Стандартная схема деплоя LMS: сборка на Hetzner → `docker save | ssh | docker load` на Hoster.kz; browserless на Hoster.kz — обычный `docker pull`.
|
||||||
|
- Hot-standby на Hetzner получает тот же compose (репликация БД уже покрывает `PdfApiKey` и `ToolUsage`).
|
||||||
|
|
||||||
|
## Тестирование
|
||||||
|
|
||||||
|
- Юнит: SSRF-валидатор (таблица адресов → допуск/блок), пайплайн Defuddle → HTML-шаблон на фикстурах.
|
||||||
|
- Интеграция на staging: рендер 3–5 реальных статей (лонгрид, статья с картинками, JS-тяжёлая страница), проверка лимитов и обоих путей авторизации.
|
||||||
|
- Zotero-скрипт — ручная проверка на живой библиотеке.
|
||||||
|
- Веб-форма — e2e через agent-browser в `--headed` (headless режет антиспам — известные грабли).
|
||||||
|
|
||||||
|
## Вне рамок (YAGNI)
|
||||||
|
|
||||||
|
- Отдельный домен `pdf.second-brain.ru` — не нужен, всё на `school.second-brain.ru`.
|
||||||
|
- Хранение сгенерированных PDF на сервере — файл отдаётся сразу и не сохраняется.
|
||||||
|
- Пер-курсовый гейтинг, тарифные лимиты, платные квоты — не сейчас.
|
||||||
|
- Освещение в рассылке/анонсы — после включения `TOOLBOX_VISIBLE` на проде, отдельной задачей.
|
||||||
Reference in New Issue
Block a user