Initial Helpomatica source.

This commit is contained in:
2026-08-14 11:06:58 +07:00
commit d0de81d0a7
15 changed files with 10071 additions and 0 deletions
+365
View File
@@ -0,0 +1,365 @@
# 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, 200000 итераций.
- **Доступ:** все авторизованные видят все проекты и общие чаты.
Отдельных 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
```