mirror of
https://github.com/EDeev/declaude.git
synced 2026-10-07 20:49:57 +03:00
declaude: конвертер экспорта Claude.ai в статический HTML-архив
This commit is contained in:
commit
b2d9deb4b4
6 changed files with 2751 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
__pycache__/
|
||||
*.pyc
|
||||
claude-backup-v1/
|
||||
claude_archive/
|
||||
536
INTEGRATION.md
Normal file
536
INTEGRATION.md
Normal file
|
|
@ -0,0 +1,536 @@
|
|||
# Claude Archive Converter — интеграция в FastAPI сайт
|
||||
|
||||
## Что нужно сделать
|
||||
|
||||
Встроить модуль конвертации Claude-бэкапа в статический HTML-архив как endpoint FastAPI-сайта.
|
||||
|
||||
**Пользовательский сценарий:**
|
||||
1. Пользователь открывает страницу `/tools/claude-archive`
|
||||
2. Перетаскивает или выбирает zip-файл (скачанный с claude.ai — экспорт данных)
|
||||
3. Видит прогресс-бар обработки
|
||||
4. Получает ссылку для скачивания готового zip-архива с HTML-страницами
|
||||
5. Файл автоматически удаляется с сервера через 10 минут
|
||||
|
||||
---
|
||||
|
||||
## Ядро конвертера
|
||||
|
||||
Файл `declaude.py` уже существует и полностью работоспособен. Его **не нужно переписывать** — нужно только обернуть в веб-слой.
|
||||
|
||||
Ключевые классы из `declaude.py`:
|
||||
- `DataLoader(source_dir)` — загружает `conversations.json`, `projects/*.json`, `users.json`
|
||||
- `MappingManager(path)` — читает/пишет `mapping.json`
|
||||
- `SiteBuilder(output_dir, loader, mapping)` — генерирует всё HTML
|
||||
- `SiteBuilder.build_all()` — запускает полную генерацию
|
||||
|
||||
Пример минимального вызова (уже работает локально):
|
||||
```python
|
||||
from declaude import DataLoader, MappingManager, SiteBuilder
|
||||
from pathlib import Path
|
||||
|
||||
loader = DataLoader(Path("extracted_backup/"))
|
||||
loader.load()
|
||||
mapping = MappingManager(Path("mapping.json"))
|
||||
mapping.load()
|
||||
site = SiteBuilder(Path("output/"), loader, mapping)
|
||||
site.build_all()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Структура файлов, которые нужно создать
|
||||
|
||||
```
|
||||
your_project/
|
||||
├── routers/
|
||||
│ └── claude_archive.py # новый роутер FastAPI
|
||||
├── services/
|
||||
│ └── claude_converter.py # бизнес-логика: распаковка, запуск, упаковка
|
||||
├── templates/
|
||||
│ └── claude_archive.html # страница загрузки (Jinja2 или отдельный HTML)
|
||||
├── static/
|
||||
│ └── claude_archive.js # фронтенд: upload, polling, progress bar
|
||||
└── declaude.py # уже существует — НЕ ТРОГАТЬ
|
||||
```
|
||||
|
||||
Подключи роутер в `main.py`:
|
||||
```python
|
||||
from routers.claude_archive import router as claude_archive_router
|
||||
app.include_router(claude_archive_router, prefix="/tools")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## routers/claude_archive.py
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
import uuid
|
||||
import zipfile
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
from fastapi import APIRouter, UploadFile, File, HTTPException, BackgroundTasks
|
||||
from fastapi.responses import FileResponse, JSONResponse
|
||||
|
||||
router = APIRouter(tags=["claude-archive"])
|
||||
|
||||
# Временная директория для задач
|
||||
WORK_DIR = Path("/tmp/claude_archive_jobs")
|
||||
WORK_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Хранилище состояний задач в памяти (для prod замени на Redis)
|
||||
jobs: Dict[str, dict] = {}
|
||||
|
||||
|
||||
@router.get("/claude-archive")
|
||||
async def page():
|
||||
"""Страница загрузки — отдаёт HTML."""
|
||||
from fastapi.responses import HTMLResponse
|
||||
# Если используешь Jinja2, подключи TemplateResponse
|
||||
html = Path("templates/claude_archive.html").read_text(encoding="utf-8")
|
||||
return HTMLResponse(html)
|
||||
|
||||
|
||||
@router.post("/claude-archive/upload")
|
||||
async def upload(background_tasks: BackgroundTasks, file: UploadFile = File(...)):
|
||||
"""
|
||||
Принимает zip-файл, запускает конвертацию в фоне.
|
||||
Возвращает job_id для polling статуса.
|
||||
"""
|
||||
if not file.filename.endswith(".zip"):
|
||||
raise HTTPException(400, "Ожидается .zip файл")
|
||||
|
||||
# Ограничение размера: 500 MB
|
||||
MAX_SIZE = 500 * 1024 * 1024
|
||||
content = await file.read()
|
||||
if len(content) > MAX_SIZE:
|
||||
raise HTTPException(413, "Файл слишком большой (макс. 500 MB)")
|
||||
|
||||
job_id = str(uuid.uuid4())
|
||||
job_dir = WORK_DIR / job_id
|
||||
job_dir.mkdir()
|
||||
|
||||
# Сохраняем загруженный zip
|
||||
upload_path = job_dir / "upload.zip"
|
||||
upload_path.write_bytes(content)
|
||||
|
||||
# Регистрируем задачу
|
||||
jobs[job_id] = {"status": "queued", "progress": 0, "error": None}
|
||||
|
||||
# Запускаем конвертацию в фоне
|
||||
background_tasks.add_task(run_conversion, job_id, job_dir)
|
||||
|
||||
return JSONResponse({"job_id": job_id})
|
||||
|
||||
|
||||
@router.get("/claude-archive/status/{job_id}")
|
||||
async def status(job_id: str):
|
||||
"""Polling endpoint — возвращает статус задачи."""
|
||||
if job_id not in jobs:
|
||||
raise HTTPException(404, "Задача не найдена")
|
||||
return JSONResponse(jobs[job_id])
|
||||
|
||||
|
||||
@router.get("/claude-archive/download/{job_id}")
|
||||
async def download(job_id: str):
|
||||
"""Скачать готовый архив."""
|
||||
if job_id not in jobs:
|
||||
raise HTTPException(404, "Задача не найдена")
|
||||
if jobs[job_id]["status"] != "done":
|
||||
raise HTTPException(400, "Архив ещё не готов")
|
||||
|
||||
result_zip = WORK_DIR / job_id / "result.zip"
|
||||
if not result_zip.exists():
|
||||
raise HTTPException(404, "Файл не найден")
|
||||
|
||||
return FileResponse(
|
||||
path=str(result_zip),
|
||||
media_type="application/zip",
|
||||
filename="claude_archive.zip",
|
||||
headers={"Content-Disposition": "attachment; filename=claude_archive.zip"}
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## services/claude_converter.py
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
import zipfile
|
||||
import shutil
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
from routers.claude_archive import jobs, WORK_DIR
|
||||
|
||||
|
||||
async def run_conversion(job_id: str, job_dir: Path):
|
||||
"""
|
||||
Фоновая задача: распаковать upload.zip, запустить declaude, упаковать результат.
|
||||
Обновляет jobs[job_id] по ходу выполнения.
|
||||
Удаляет все файлы через 10 минут после завершения.
|
||||
"""
|
||||
try:
|
||||
jobs[job_id]["status"] = "extracting"
|
||||
jobs[job_id]["progress"] = 5
|
||||
|
||||
# 1. Распаковываем загруженный архив
|
||||
extract_dir = job_dir / "extracted"
|
||||
extract_dir.mkdir()
|
||||
with zipfile.ZipFile(job_dir / "upload.zip", "r") as zf:
|
||||
zf.extractall(extract_dir)
|
||||
|
||||
# Если внутри zip есть одна папка — используем её как source
|
||||
children = list(extract_dir.iterdir())
|
||||
source_dir = children[0] if len(children) == 1 and children[0].is_dir() else extract_dir
|
||||
|
||||
jobs[job_id]["status"] = "loading"
|
||||
jobs[job_id]["progress"] = 15
|
||||
|
||||
# 2. Запускаем тяжёлую работу в thread pool (не блокируем event loop)
|
||||
loop = asyncio.get_event_loop()
|
||||
await loop.run_in_executor(None, _build_archive, job_id, source_dir, job_dir)
|
||||
|
||||
except Exception as e:
|
||||
jobs[job_id]["status"] = "error"
|
||||
jobs[job_id]["error"] = str(e)
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
# Удаляем файлы через 10 минут
|
||||
await asyncio.sleep(600)
|
||||
shutil.rmtree(job_dir, ignore_errors=True)
|
||||
jobs.pop(job_id, None)
|
||||
|
||||
|
||||
def _build_archive(job_id: str, source_dir: Path, job_dir: Path):
|
||||
"""
|
||||
Синхронная часть — выполняется в thread pool.
|
||||
Импортируем declaude здесь чтобы не тащить в event loop.
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
# declaude.py лежит в корне проекта — добавляем в path если нужно
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from declaude import DataLoader, MappingManager, SiteBuilder
|
||||
|
||||
output_dir = job_dir / "output"
|
||||
output_dir.mkdir()
|
||||
|
||||
jobs[job_id]["status"] = "building"
|
||||
jobs[job_id]["progress"] = 30
|
||||
|
||||
# Загружаем данные
|
||||
loader = DataLoader(source_dir)
|
||||
loader.load()
|
||||
|
||||
# Пустой маппинг (пользователь может настроить потом через браузер)
|
||||
mapping = MappingManager(job_dir / "mapping.json")
|
||||
mapping.load()
|
||||
|
||||
jobs[job_id]["progress"] = 50
|
||||
|
||||
# Генерируем HTML-архив
|
||||
site = SiteBuilder(output_dir, loader, mapping)
|
||||
site.build_all()
|
||||
|
||||
jobs[job_id]["status"] = "packing"
|
||||
jobs[job_id]["progress"] = 85
|
||||
|
||||
# Упаковываем результат
|
||||
result_zip = job_dir / "result.zip"
|
||||
_zip_directory(output_dir, result_zip)
|
||||
|
||||
jobs[job_id]["status"] = "done"
|
||||
jobs[job_id]["progress"] = 100
|
||||
|
||||
|
||||
def _zip_directory(source_dir: Path, output_path: Path):
|
||||
"""Рекурсивно запаковывает директорию в zip с относительными путями."""
|
||||
with zipfile.ZipFile(output_path, "w", zipfile.ZIP_DEFLATED) as zf:
|
||||
for file_path in source_dir.rglob("*"):
|
||||
if file_path.is_file():
|
||||
arcname = file_path.relative_to(source_dir)
|
||||
zf.write(file_path, arcname)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## templates/claude_archive.html
|
||||
|
||||
Минимальная страница с drag-and-drop загрузкой и прогресс-баром. Адаптируй под свою систему шаблонов (Jinja2, etc.) и дизайн сайта.
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Claude Archive Converter</title>
|
||||
<style>
|
||||
/* Подключи свои глобальные стили, это только специфика виджета */
|
||||
.converter-box {
|
||||
max-width: 560px;
|
||||
margin: 3rem auto;
|
||||
padding: 2rem;
|
||||
border: 1px solid var(--border, #383838);
|
||||
border-radius: 12px;
|
||||
background: var(--bg2, #242424);
|
||||
font-family: inherit;
|
||||
}
|
||||
.drop-zone {
|
||||
border: 2px dashed var(--border, #444);
|
||||
border-radius: 8px;
|
||||
padding: 3rem 1rem;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
.drop-zone.drag-over { border-color: #d97706; background: rgba(217,119,6,0.05); }
|
||||
.drop-zone input[type=file] { display: none; }
|
||||
.progress-bar-wrap {
|
||||
background: var(--bg3, #2e2e2e);
|
||||
border-radius: 6px;
|
||||
height: 8px;
|
||||
overflow: hidden;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.progress-bar-fill {
|
||||
height: 100%;
|
||||
background: #d97706;
|
||||
border-radius: 6px;
|
||||
transition: width 0.3s;
|
||||
width: 0%;
|
||||
}
|
||||
.status-msg { font-size: 0.9rem; color: #a0a0a0; margin: 0.5rem 0; }
|
||||
.btn-download {
|
||||
display: inline-block;
|
||||
padding: 0.6rem 1.4rem;
|
||||
background: #d97706;
|
||||
color: #fff;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.error-msg { color: #f87171; font-size: 0.9rem; margin-top: 0.5rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="converter-box">
|
||||
<h2>Claude Archive Converter</h2>
|
||||
<p style="color:#a0a0a0; font-size:0.9rem; margin-bottom:1.5rem;">
|
||||
Загрузите zip-файл экспорта данных Claude (Settings → Privacy → Export data).
|
||||
Вы получите красивый HTML-архив всех ваших чатов и проектов.
|
||||
</p>
|
||||
|
||||
<div class="drop-zone" id="dropZone">
|
||||
<input type="file" id="fileInput" accept=".zip">
|
||||
<div id="dropLabel">
|
||||
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="#666" stroke-width="1.5" style="margin-bottom:0.75rem">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||||
<polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/>
|
||||
</svg>
|
||||
<div>Перетащите .zip сюда или <strong style="color:#d97706">выберите файл</strong></div>
|
||||
<div style="font-size:0.8rem; color:#666; margin-top:0.4rem">Максимум 500 MB</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="progressSection" style="display:none">
|
||||
<div class="progress-bar-wrap"><div class="progress-bar-fill" id="progressFill"></div></div>
|
||||
<div class="status-msg" id="statusMsg">Загрузка...</div>
|
||||
</div>
|
||||
|
||||
<div id="errorSection" style="display:none">
|
||||
<div class="error-msg" id="errorMsg"></div>
|
||||
<button onclick="resetForm()" style="margin-top:0.5rem; cursor:pointer">Попробовать ещё раз</button>
|
||||
</div>
|
||||
|
||||
<div id="doneSection" style="display:none">
|
||||
<div style="color:#4ade80; margin-bottom:0.5rem">✓ Готово! Архив сформирован.</div>
|
||||
<a class="btn-download" id="downloadLink" href="#">Скачать claude_archive.zip</a>
|
||||
<div style="font-size:0.75rem; color:#666; margin-top:0.5rem">Файл будет удалён с сервера через 10 минут</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/claude_archive.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## static/claude_archive.js
|
||||
|
||||
```javascript
|
||||
const BASE = "/tools/claude-archive";
|
||||
|
||||
// Drag & drop
|
||||
const dropZone = document.getElementById("dropZone");
|
||||
const fileInput = document.getElementById("fileInput");
|
||||
|
||||
dropZone.addEventListener("click", () => fileInput.click());
|
||||
dropZone.addEventListener("dragover", (e) => { e.preventDefault(); dropZone.classList.add("drag-over"); });
|
||||
dropZone.addEventListener("dragleave", () => dropZone.classList.remove("drag-over"));
|
||||
dropZone.addEventListener("drop", (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove("drag-over");
|
||||
const file = e.dataTransfer.files[0];
|
||||
if (file) handleFile(file);
|
||||
});
|
||||
fileInput.addEventListener("change", () => {
|
||||
if (fileInput.files[0]) handleFile(fileInput.files[0]);
|
||||
});
|
||||
|
||||
function handleFile(file) {
|
||||
if (!file.name.endsWith(".zip")) {
|
||||
showError("Ожидается файл .zip");
|
||||
return;
|
||||
}
|
||||
uploadFile(file);
|
||||
}
|
||||
|
||||
function uploadFile(file) {
|
||||
showProgress(5, "Загрузка файла на сервер...");
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
|
||||
fetch(`${BASE}/upload`, { method: "POST", body: formData })
|
||||
.then((r) => {
|
||||
if (!r.ok) return r.json().then((d) => { throw new Error(d.detail || "Ошибка загрузки"); });
|
||||
return r.json();
|
||||
})
|
||||
.then((data) => pollStatus(data.job_id))
|
||||
.catch((err) => showError(err.message));
|
||||
}
|
||||
|
||||
let pollTimer = null;
|
||||
|
||||
function pollStatus(jobId) {
|
||||
const STATUS_LABELS = {
|
||||
queued: "В очереди...",
|
||||
extracting: "Распаковка архива...",
|
||||
loading: "Загрузка данных...",
|
||||
building: "Генерация HTML-страниц...",
|
||||
packing: "Упаковка результата...",
|
||||
done: "Готово!",
|
||||
error: "Ошибка",
|
||||
};
|
||||
|
||||
fetch(`${BASE}/status/${jobId}`)
|
||||
.then((r) => r.json())
|
||||
.then((data) => {
|
||||
const label = STATUS_LABELS[data.status] || data.status;
|
||||
showProgress(data.progress || 0, label);
|
||||
|
||||
if (data.status === "done") {
|
||||
showDone(jobId);
|
||||
} else if (data.status === "error") {
|
||||
showError(data.error || "Неизвестная ошибка");
|
||||
} else {
|
||||
// Продолжаем polling каждые 1.5 секунды
|
||||
pollTimer = setTimeout(() => pollStatus(jobId), 1500);
|
||||
}
|
||||
})
|
||||
.catch((err) => showError("Ошибка polling: " + err.message));
|
||||
}
|
||||
|
||||
// UI helpers
|
||||
function showProgress(pct, msg) {
|
||||
document.getElementById("dropZone").style.display = "none";
|
||||
document.getElementById("progressSection").style.display = "block";
|
||||
document.getElementById("errorSection").style.display = "none";
|
||||
document.getElementById("doneSection").style.display = "none";
|
||||
document.getElementById("progressFill").style.width = pct + "%";
|
||||
document.getElementById("statusMsg").textContent = msg;
|
||||
}
|
||||
|
||||
function showDone(jobId) {
|
||||
document.getElementById("progressSection").style.display = "none";
|
||||
document.getElementById("doneSection").style.display = "block";
|
||||
document.getElementById("downloadLink").href = `${BASE}/download/${jobId}`;
|
||||
}
|
||||
|
||||
function showError(msg) {
|
||||
clearTimeout(pollTimer);
|
||||
document.getElementById("progressSection").style.display = "none";
|
||||
document.getElementById("dropZone").style.display = "block";
|
||||
document.getElementById("errorSection").style.display = "block";
|
||||
document.getElementById("errorMsg").textContent = msg;
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
document.getElementById("errorSection").style.display = "none";
|
||||
document.getElementById("doneSection").style.display = "none";
|
||||
document.getElementById("dropZone").style.display = "block";
|
||||
document.getElementById("progressFill").style.width = "0%";
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Зависимости
|
||||
|
||||
Ничего нового — всё уже есть в FastAPI. Только убедись что в `requirements.txt` есть:
|
||||
|
||||
```
|
||||
fastapi
|
||||
uvicorn
|
||||
python-multipart # нужен для UploadFile
|
||||
```
|
||||
|
||||
`python-multipart` нужен FastAPI для обработки `multipart/form-data` (загрузка файлов). Без него `UploadFile` не работает.
|
||||
|
||||
---
|
||||
|
||||
## Важные решения и ограничения
|
||||
|
||||
### Хранилище состояний задач
|
||||
В коде выше `jobs` — это обычный Python dict в памяти. Это работает для **одного воркера**. Если у тебя несколько воркеров uvicorn (`--workers N`), состояние будет разделено между процессами и polling сломается.
|
||||
|
||||
**Если используешь несколько воркеров:** замени `jobs` dict на Redis:
|
||||
```python
|
||||
import redis, json
|
||||
r = redis.Redis()
|
||||
# запись: r.setex(f"job:{job_id}", 700, json.dumps(state))
|
||||
# чтение: json.loads(r.get(f"job:{job_id}"))
|
||||
```
|
||||
|
||||
### Очистка файлов
|
||||
Временные файлы удаляются через `asyncio.sleep(600)` после завершения задачи. Если сервер перезапустится в процессе — файлы останутся. Добавь startup-хук для очистки старых папок:
|
||||
```python
|
||||
@app.on_event("startup")
|
||||
async def cleanup_old_jobs():
|
||||
shutil.rmtree(WORK_DIR, ignore_errors=True)
|
||||
WORK_DIR.mkdir(parents=True)
|
||||
```
|
||||
|
||||
### Размер архива и таймауты
|
||||
Conversations.json может весить 300+ MB. Обработка 950 диалогов занимает ~70 секунд. Убедись что у nginx/proxy нет таймаута на загрузку файла. Рекомендуемые настройки nginx:
|
||||
```nginx
|
||||
client_max_body_size 600M;
|
||||
proxy_read_timeout 600s;
|
||||
proxy_send_timeout 600s;
|
||||
```
|
||||
|
||||
### Параллельные задачи
|
||||
`run_in_executor` использует `ThreadPoolExecutor` по умолчанию (количество потоков = CPU * 5). При высокой нагрузке создай явный executor с ограничением:
|
||||
```python
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
executor = ThreadPoolExecutor(max_workers=3)
|
||||
await loop.run_in_executor(executor, _build_archive, ...)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Порядок реализации для Claude Code
|
||||
|
||||
1. Скопировать `declaude.py` в корень проекта (или настроить `sys.path` в `claude_converter.py`)
|
||||
2. Создать `routers/claude_archive.py` по шаблону выше
|
||||
3. Создать `services/claude_converter.py`, импортировать `run_conversion` в роутер
|
||||
4. Создать `templates/claude_archive.html` и `static/claude_archive.js`
|
||||
5. Добавить `app.include_router(...)` в `main.py`
|
||||
6. Добавить `python-multipart` в зависимости
|
||||
7. Проверить что nginx разрешает загрузку файлов до 500 MB
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2026 Egor Deev
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
91
README.md
Normal file
91
README.md
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
# declaude
|
||||
|
||||
Автономная утилита для преобразования официального экспорта данных Claude.ai в интерактивный статический HTML-архив.
|
||||
|
||||
Позволяет локально хранить, просматривать и быстро искать диалоги без внешних зависимостей и необходимости поднимать базы данных или веб-серверы.
|
||||
|
||||
## Онлайн-версия
|
||||
|
||||
Не хочется устанавливать Python и разбираться руками — есть готовая веб-версия:
|
||||
**https://fe0.ru/declaude/**. Загружаете zip-экспорт, получаете готовый архив — без установки
|
||||
чего-либо. Если важна приватность данных, запустите скрипт локально по инструкции ниже.
|
||||
|
||||
## Возможности
|
||||
|
||||
- **Полная автономность:** генерирует чистые HTML-страницы для каждого диалога, которые открываются в любом браузере.
|
||||
- **Страницы проектов:** рендеринг контекстных документов и шаблонов системных промптов для каждого проекта.
|
||||
- **Полноценная поддержка Markdown:** заголовки, списки, таблицы, цитаты, блоки кода.
|
||||
- **Подсветка синтаксиса без сторонних библиотек:** встроенная поддержка Python, JavaScript, TypeScript, SQL, Bash, Go и Rust.
|
||||
- **Темы оформления и языки:** переключение между светлой и тёмной темой, а также интерфейс на русском и английском языках (выбор сохраняется в LocalStorage).
|
||||
- **Маппинг проектов:** возможность связывать диалоги с проектами прямо в браузере с выгрузкой файла `mapping.json` и быстрой пересборкой.
|
||||
- **Инкрементальные обновления:** режим `--update` обновляет только добавленные или изменённые диалоги.
|
||||
- **Нулевые зависимости:** работает исключительно на стандартной библиотеке Python 3.8+.
|
||||
|
||||
## Подготовка данных
|
||||
|
||||
1. Откройте **claude.ai → Settings → Privacy → Export data**.
|
||||
2. Дождитесь письма со ссылкой и скачайте zip-архив с выгрузкой.
|
||||
3. Распакуйте архив в отдельную папку. Внутри находятся файлы `conversations.json`, `users.json` и каталог `projects/`.
|
||||
|
||||
## Использование
|
||||
|
||||
### Первая сборка архива
|
||||
|
||||
```bash
|
||||
python declaude.py --build --source ./claude-export
|
||||
```
|
||||
|
||||
После завершения откройте файл `claude_archive/index.html` в браузере.
|
||||
|
||||
### Указание своей папки для архива
|
||||
|
||||
```bash
|
||||
python declaude.py --build --source ./claude-export --output ./my-archive
|
||||
```
|
||||
|
||||
### Инкрементальное обновление из нового архива
|
||||
|
||||
```bash
|
||||
python declaude.py --update new_export.zip
|
||||
```
|
||||
|
||||
Регенерируются только новые и отредактированные страницы, текущая привязка диалогов к проектам в `mapping.json` сохраняется.
|
||||
|
||||
### Назначение диалога проекту через консоль
|
||||
|
||||
```bash
|
||||
python declaude.py --map <conversation_uuid> <project_uuid>
|
||||
python declaude.py --remap
|
||||
```
|
||||
|
||||
Быстрая пересборка страниц проектов (`--remap`) занимает секунды, так как не пересобирает неизменённые диалоги.
|
||||
|
||||
## Структура сгенерированного архива
|
||||
|
||||
```
|
||||
claude_archive/
|
||||
├── index.html # Главная страница: статистика, проекты, последние чаты
|
||||
├── all_conversations.html # Полный список диалогов с поиском и фильтрацией
|
||||
├── conversations/
|
||||
│ └── <uuid>.html # Страницы отдельных диалогов
|
||||
├── projects/
|
||||
│ └── <uuid>/
|
||||
│ └── index.html # Страницы проектов с документами и диалогами
|
||||
└── assets/
|
||||
├── style.css
|
||||
└── app.js
|
||||
```
|
||||
|
||||
## Параметры командной строки
|
||||
|
||||
- `--build` — полная сборка архива из исходного каталога.
|
||||
- `--source ПУТЬ` — путь к каталогу с распакованным экспортом (по умолчанию: `claude-backup-v1`).
|
||||
- `--output ПУТЬ` — каталог для сохранения HTML-архива (по умолчанию: `claude_archive`).
|
||||
- `--update АРХИВ.ZIP` — добавление данных из нового zip-архива с сохранением маппинга.
|
||||
- `--remap` — быстрая пересборка индексной страницы и страниц проектов.
|
||||
- `--map CONV_ID PROJ_ID` — ручное сопоставление диалога с проектом.
|
||||
- `--mapping ПУТЬ` — путь к файлу сопоставлений (по умолчанию: `./mapping.json`).
|
||||
|
||||
## Лицензия
|
||||
|
||||
MIT
|
||||
2098
declaude.py
Normal file
2098
declaude.py
Normal file
File diff suppressed because it is too large
Load diff
1
mapping.json
Normal file
1
mapping.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{}
|
||||
Loading…
Add table
Reference in a new issue