366 lines
17 KiB
Markdown
366 lines
17 KiB
Markdown
# 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://<IP-этой-машины>: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": "<uuid>_оригинал",
|
|||
|
|
"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/<stored_name>`, в 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-<project8>-*` (в 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
|
|||
|
|
```
|