# Helpomatica Локальная веб-панель в духе Claude Projects: проекты, инструкции, файлы, общие чаты, пользователи с ролями и ответы **Cursor Agent**. Интерфейс на русском. Это не SPA-фреймворк и не облачный продукт — один процесс FastAPI раздаёт статику и JSON API, данные лежат на диске сервера. Ответы агента **не полностью офлайн**: нужен `CURSOR_API_KEY` с [Cursor Dashboard → Integrations](https://cursor.com/dashboard/integrations). Веб-панель, логин, файлы и предразбор логов работают и без ключа. --- ## Как запустить локально Скопируйте `.env.example` в `.env` и вставьте ключ: ```env CURSOR_API_KEY=crsr_your_key_here CURSOR_MODEL=auto ADMIN_USERNAME=admin ADMIN_PASSWORD=admin ``` Затем: ```powershell .\run.ps1 ``` Скрипт ставит зависимости в `.deps` (`pip install --target .deps`), выставляет `PYTHONPATH` и поднимает uvicorn с `--reload` на **`0.0.0.0:8010`**. - На этой машине: http://127.0.0.1:8010 - С других ПК в LAN: `http://:8010` - Вход по умолчанию: **`admin` / `admin`** Если PowerShell блокирует локальные сценарии: ```powershell powershell -ExecutionPolicy Bypass -File .\run.ps1 ``` Если страница не открывается с другого ПК, разрешите входящий TCP 8010 в брандмауэре Windows: ```powershell netsh advfirewall firewall add rule name="Helpomatica 8010" dir=in action=allow protocol=TCP localport=8010 ``` Загрузки и агент выполняются **на машине сервера**, не в браузере клиента. Файлы копируются во временный workspace из `data/uploads/`. --- ## Docker Нужен файл `.env` (из `.env.example`). Данные не кладутся в образ: том `./data:/app/data` хранит `db.json` и загрузки. ```bash docker compose up -d --build ``` Откройте http://localhost:8010 и войдите. После пересборки контейнера проекты, чаты и файлы остаются в `./data`. Healthcheck бьёт в `GET /` (страница логина, без авторизации). `GET /api/auth/me` для проверки не подходит — без cookie он отдаёт 401. Подробности про Cursor Agent в Linux-контейнере — в разделе [Cursor SDK и Docker](#cursor-sdk-и-docker). --- ## Что это такое Панель для команды на одной машине / в LAN: - **Проекты** — название, описание, инструкции, модель, иконка. - **Инструкции** — общий контекст для всех чатов проекта. - **Файлы** — материалы проекта (видны во всех чатах) или только этого чата. - **Чаты** — общие на проект: любой вошедший пользователь читает и пишет в тех же разговорах. - **Пользователи** — роли `admin` и `user`. Админ видит имена авторов сообщений; обычный пользователь видит «Вы» / «Коллега». - **Ответы** — локальный Cursor Agent (`AsyncAgent`) со стримингом, размышлениями, историей инструментов и кнопкой «Стоп». Дополнительно в UI: поиск по чатам, экспорт чата в Markdown, чипы контекстных файлов, копирование кода, цитаты строк лога, диаграммы Mermaid, формулы KaTeX, светлая/тёмная тема, индикатор статуса агента. --- ## Архитектура ``` Браузер (static/index.html + app.js + style.css) │ cookie helpomatica_session ▼ FastAPI (main.py) ──► data/db.json │ data/uploads/ │ %TEMP%/helpomatica-agent/hlp-* (в Docker: /tmp/...) ▼ cursor-sdk: AsyncClient.launch_bridge → AsyncAgent.send → SSE ▼ Cursor API (нужен CURSOR_API_KEY) ``` - **Бэкенд:** FastAPI, статика смонтирована на `/static`, корень `GET /` отдаёт `static/index.html`. Фронтенд — ванильный JS, без React/Vue. - **Хранение:** JSON-файл `data/db.json` (атомарная запись через `.tmp`) и файловая система `data/uploads/`. Папка `data/` в Git не входит. - **Сессии:** список в `db.json`, cookie HttpOnly `helpomatica_session` (`SameSite=Lax`, без `Secure` — HTTP по IP в LAN), срок 30 дней, не больше 200 сессий. Пароль: PBKDF2-HMAC-SHA256, 200 000 итераций. - **Доступ:** все авторизованные видят все проекты и общие чаты. Отдельных ACL на проект нет. Файлы с `scope=chat` попадают в агент только этого чата; `scope=project` — во все чаты проекта. - **Параллельность:** глобального single-flight нет. У каждого `chat_id` своя очередь: один активный запрос и максимум один в ожидании (второй лишний → HTTP 409). Разные чаты запускают отдельные local bridge. - **Слушает** `0.0.0.0:8010`. `SESSION_SECRET` в `.env` — заготовка, **main.py его не читает**. --- ## Стек Версии из текущего `pip install` в `.deps` (в `requirements.txt` пакеты **без пинов**): | Слой | Что | |------|-----| | Язык | Python **3.12** (локально 3.12.10). `cursor-sdk` требует **≥ 3.10** | | API | FastAPI **0.141.1**, Starlette, Pydantic v2 | | Сервер | uvicorn[standard] **0.52.1** | | Агент | cursor-sdk **1.0.27** (`AsyncAgent`, `AsyncClient.launch_bridge`, `LocalAgentOptions`) + httpx | | Конфиг | python-dotenv **1.2.2** | | Загрузки | python-multipart **0.0.32** | | Предразбор логов | `log_preanalysis.py` (stdlib) | | Фронтенд | `static/app.js`, `static/style.css` — без сборки | | CDN | Mermaid 11, KaTeX 0.16.22, шрифты Google (Manrope, Playfair Display) | | Данные | JSON + файлы на диске | Локальный запуск (`run.ps1`) кладёт пакеты в `.deps` и добавляет каталог в `sys.path`. Docker ставит те же пакеты в venv через `pip install -r requirements.txt`. --- ## Форма `data/db.json` Корень — объект со списками. При пустом файле / первом старте создаётся админ из `ADMIN_USERNAME` / `ADMIN_PASSWORD`. ```json { "users": [ { "id": "hex", "username": "admin", "display_name": "Admin", "password_hash": "salt$pbkdf2hex", "role": "admin", "created_at": "ISO-8601" } ], "sessions": [{ "id": "token", "user_id": "…", "created_at": "…" }], "projects": [{ "id": "…", "name": "…", "description": "…", "instructions": "…", "icon": "✦", "model": "auto", "created_by": "…", "created_at": "…", "updated_at": "…" }], "chats": [{ "id": "…", "project_id": "…", "title": "…", "sort_order": 0, "created_by": "…", "updated_by": "…", "created_at": "…", "updated_at": "…" }], "messages": [{ "id": "…", "chat_id": "…", "role": "user|assistant", "content": "…", "attachments": [{ "id": "…", "name": "…" }], "user_id": "… или null", "author_name": "Admin|Помощник|…", "thinking": "…", "thinking_duration_ms": 17519, "activity": [{ "id": "a1", "type": "thinking|shell|…", "title": "…", "detail": "…", "status": "running|completed" }], "created_at": "…" }], "files": [{ "id": "…", "project_id": "…", "chat_id": "null или id чата", "scope": "project|chat", "name": "line_u_codes.log.1", "label": "", "stored_name": "_оригинал", "content_type": "…", "size": 123, "created_at": "…" }], "analysis_cache": [{ "key": "sha256", "file_sig": "sha256", "project_id": "…", "job_id": "", "question": "нормализованный текст", "answer": "…", "preanalysis": "markdown", "summary": "…", "created_at": "…" }] } ``` Байты файлов лежат в `data/uploads/`, в UI показывается оригинальное `name`. --- ## Как проходит запрос к агенту 1. Пользователь пишет сообщение. Фронт сохраняет его `POST /api/chats/{id}/messages` (роль `user`, вложения, автор). 2. Сразу же `POST /api/chats/{id}/respond` — SSE-поток (`text/event-stream`). 3. Сервер занимает слот чата (или ставит в очередь на один). Событие `status: queued` / `starting`. 4. Собирается workspace: `prepare_agent_workspace` создаёт `%TEMP%/helpomatica-agent/hlp--*` (в Linux/Docker — `/tmp/helpomatica-agent/hlp-…`). Туда hardlink/копия загрузок (и в `./`, и в `./files/`), плюс подсказки `README.md`, `PROJECT_FILES.md`, `AGENTS.md`, `.cursorignore`. 5. Если вопрос похож на разбор логов (`log_preanalysis.looks_like_log_job`) и в скоупе есть `*.log*`: - ищется `analysis_cache`; - иначе поток-скан логов с SSE `progress` («Читаю лог… N%»); - результат пишется в `./PREANALYSIS.md`. 6. Промпт: инструкции проекта + выдержки файлов + последние 30 сообщений + правило «cwd уже workspace, читай PREANALYSIS.md первым». 7. В **отдельном потоке** (на Windows — `WindowsProactorEventLoopPolicy`) вызывается `AsyncClient.launch_bridge(workspace=cwd)` и `AsyncAgent.create` / `agent.send`. События моста читаются в этом потоке; основной event loop забирает их через `asyncio.to_thread(queue.get)`. 8. В браузер уходят SSE: - `thinking` — дельты `thinking-delta`; - `activity` — инструменты, shell, статус, шаги «Логи»; - `delta` — токены ответа (`text-delta`); - `thinking_done`, `done` / `cancelled` / `error`. 9. Готовый текст сохраняется как сообщение ассистента (`thinking`, `activity`, `thinking_duration_ms`). При успехе пишется кэш разбора (до 80 записей). Кнопка «Стоп» — `POST /api/chats/{id}/cancel`. --- ## Workspace агента и файлы - Каталог: `tempfile.gettempdir()/helpomatica-agent/hlp-…`. - Копируются файлы **проекта** + файлы **этого чата** + вложения сообщений. - Вложения, chat-scope и логи копируются всегда; остальные файлы проекта режутся лимитами 40 МБ / файл и 80 МБ суммарно. - Если hardlink недоступен — `shutil.copy2`. - Агент работает только с этим cwd; исходники панели и диск клиента ему не отдаются. Цитаты в ответе вида `:15231:line_u_codes.log.1` или `::log:line_u_codes.log.1:15231::` фронт превращает в ссылку на `GET /api/files/{id}/excerpt?start=&end=` (фрагмент ±25 строк). --- ## Авторизация и API (кратко) Публично: `GET /`, `/static/*`, `POST /api/auth/login`. Остальные `/api/*` требуют cookie. | Метод | Путь | Назначение | |-------|------|------------| | POST | `/api/auth/login` | вход, Set-Cookie | | POST | `/api/auth/logout` | выход | | GET | `/api/auth/me` | текущий пользователь | | GET/POST | `/api/users` | список / создание (только admin) | | GET | `/api/agent/status` | ключ, SDK, busy, очереди чатов | | CRUD | `/api/projects`, `/api/chats` | проекты, чаты, порядок | | GET | `/api/chats/{id}/export` | Markdown | | POST | `/api/chats/{id}/messages` | сохранить сообщение | | POST | `/api/chats/{id}/respond` | SSE-ответ агента | | POST | `/api/chats/{id}/cancel` | остановить генерацию | | GET | `/api/search?q=` | поиск по названиям и тексту (от 2 символов, до 30) | | POST | `/api/projects/{id}/files` | загрузка (`scope` + `chat_id`) | | POST | `/api/chats/{id}/files/bind` | привязать файлы к чату | | GET | `/api/files/{id}` | скачать | | GET | `/api/files/{id}/excerpt` | фрагмент лога по строкам | Модель проекта выбирается в UI (`auto`, `composer-2.5`, `composer-2`, `gpt-5.2`, `claude-4.6-sonnet`) и уходит в `AgentOptions.model`. По умолчанию — `CURSOR_MODEL` из `.env`. --- ## Cursor SDK и Docker На Windows wheel `cursor-sdk` кладёт в `cursor_sdk/_vendor/bridge/bin/`: - `cursor-sdk-bridge.cmd` → запускает **`node.exe`** (вендорный, ~89 МБ) - `../dist/bin/cursor-sdk-bridge.js` — сам мост (Node, не отдельный .exe агента) `resolve_bridge_path()` ищет: `CURSOR_SDK_BRIDGE_BIN`, затем bundled `bin/cursor-sdk-bridge` (POSIX) / `cursor-sdk-bridge.cmd` (Windows), затем PATH. **Не копируйте `.deps` с Windows-хоста в Linux-образ** — там `node.exe`. Dockerfile ставит пакет заново: Linux-wheel cursor-sdk (x64/arm64) должен принести свой `node` + launcher `cursor-sdk-bridge`. Если в контейнере мост не стартует (нет bundled node, ошибка glibc, нет `cursor-sdk-bridge`): - веб-UI, логин, файлы и **предразбор логов** всё равно работают; - ответы Cursor Agent нужно снимать на Windows-хосте через `.\run.ps1`. Образ не содержит `.env`. Ключ передаётся `env_file: .env`. --- ## Безопасность - Сразу смените пароль админа (`admin`/`admin` только для первого старта). - Не коммитьте `.env` (уже в `.gitignore`). Ключ Cursor — секрет. - Панель слушает весь LAN: ограничьте доступ брандмауэром. - Cookie без `Secure`: нормально для HTTP в LAN, не для публичного HTTPS без доработки. - Это не multi-tenant isolation: любой `user` видит все проекты и чаты. - Генерации картинок в панели нет; агенту это явно запрещено в промпте. --- ## Файлы репозитория | Путь | Роль | |------|------| | `main.py` | FastAPI-приложение | | `log_preanalysis.py` | потоковый скан логов → `PREANALYSIS.md` | | `static/` | HTML/CSS/JS | | `run.ps1` | локальный запуск на Windows | | `requirements.txt` | зависимости | | `.env.example` | шаблон переменных (без секретов) | | `Dockerfile`, `docker-compose.yml`, `.dockerignore` | контейнер | | `test_preanalysis.py` | короткий тест предразбора логов | | `data/` | БД и загрузки на диске сервера — **не в Git** (только `data/.gitkeep`) | --- ## Выгрузка в Gitea В репозиторий кладите только исходники продукта: `main.py`, `log_preanalysis.py`, `static/`, `requirements.txt`, `run.ps1`, Docker-файлы, `.env.example`, `.gitignore`, `README.md`. **Не коммитьте и не копируйте в Gitea:** - `.env` — там `CURSOR_API_KEY` и пароль админа - `data/` — чаты, загрузки, `db.json` (том Docker `./data` тоже только локальный) - `.deps/` — локальный `pip install --target` (ставится заново из `requirements.txt`) После клона: ```powershell copy .env.example .env # вставьте CURSOR_API_KEY и смените ADMIN_PASSWORD docker compose up -d --build # или локально на Windows: .\run.ps1 ```