From f565be5ce65d765ce8cdbe262c4d6e359b86a93b Mon Sep 17 00:00:00 2001 From: fedos Date: Sun, 8 Mar 2026 23:41:30 +0300 Subject: [PATCH] chore: remove docs/ and CLAUDE.md from git tracking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Добавлены в .gitignore — локальная документация и инструкции для Claude не должны быть в репозитории. Co-Authored-By: Claude Sonnet 4.6 --- docs/DEVELOPER.md | 110 --------- docs/architecture.docx | Bin 35815 -> 0 bytes docs/architecture.md | 277 ---------------------- docs/diagrams/mermaid.esm.min.mjs | 14 -- docs/diagrams/preview.html | 36 --- docs/generate_docx.py | 369 ------------------------------ 6 files changed, 806 deletions(-) delete mode 100644 docs/DEVELOPER.md delete mode 100644 docs/architecture.docx delete mode 100644 docs/architecture.md delete mode 100644 docs/diagrams/mermaid.esm.min.mjs delete mode 100644 docs/diagrams/preview.html delete mode 100644 docs/generate_docx.py diff --git a/docs/DEVELOPER.md b/docs/DEVELOPER.md deleted file mode 100644 index e1dfe03..0000000 --- a/docs/DEVELOPER.md +++ /dev/null @@ -1,110 +0,0 @@ -# Руководство разработчика - -## Требования - -- Go 1.21+ -- [Ollama](https://ollama.com) — установлен и запущен локально - -## Первый запуск - -### 1. Скачать модели в Ollama - -```bash -ollama pull gemma:1b # Router LLM + документы + общее -ollama pull qwen2.5-coder:1.5b # Целевая модель для кода -``` - -Проверить что модели доступны: -```bash -ollama list -``` - -### 2. Настроить .env - -```env -PROXY_PORT=11435 -AUTH_TOKEN=my-secret-token -OLLAMA_URL=http://localhost:11434 -ROUTER_MODEL=gemma:1b -CODE_MODEL=qwen2.5-coder:1.5b -DOC_MODEL=gemma:1b -GENERAL_MODEL=gemma:1b -``` - -### 3. Запустить прокси - -```bash -make run -# или напрямую: -go run ./cmd/server -``` - ---- - -## Команды - -```bash -make build # Собрать бинарник ./ollama-proxy -make run # go run ./cmd/server (читает .env) -make test # go test ./... -``` - ---- - -## Тестирование через curl - -```bash -# Проверка работоспособности (без токена) -curl http://localhost:11435/health - -# Без токена → 401 -curl -X POST http://localhost:11435/api/chat \ - -d '{"model":"auto","messages":[{"role":"user","content":"привет"}]}' - -# Auto-маршрутизация → Router LLM (gemma:1b) выберет модель -curl http://localhost:11435/api/chat \ - -H "Authorization: Bearer my-secret-token" \ - -d '{"model":"auto","messages":[{"role":"user","content":"напиши функцию на Go"}]}' -# → классифицирует как "code" → запрос идёт в qwen2.5-coder:1.5b - -# Явная модель (без маршрутизации) -curl http://localhost:11435/api/chat \ - -H "Authorization: Bearer my-secret-token" \ - -d '{"model":"gemma:1b","messages":[{"role":"user","content":"расскажи про Go"}]}' - -# Список моделей -curl http://localhost:11435/api/tags \ - -H "Authorization: Bearer my-secret-token" -``` - ---- - -## Настройка Codex CLI - -- **Base URL**: `http://localhost:11435` -- **API Key**: значение из `AUTH_TOKEN` -- **Model**: `auto` (автомаршрутизация) или конкретная модель - ---- - -## Как добавить новую модель - -1. `ollama pull ` -2. Добавить переменную в `.env`: `ANALYSIS_MODEL=llama3.2:3b` -3. Добавить поле в `internal/config/config.go` -4. Расширить логику в `internal/service/router.go` - ---- - -## Структура кода - -| Файл | Ответственность | -|------|----------------| -| `cmd/server/main.go` | Точка входа, инициализация | -| `internal/config/config.go` | Env-переменные | -| `internal/model/ollama.go` | Go-типы Ollama API | -| `internal/handler/middleware.go` | Проверка токена | -| `internal/handler/proxy.go` | HTTP-хендлеры | -| `internal/service/ollama_client.go` | Клиент к Ollama (streaming + sync) | -| `internal/service/router.go` | LLM-маршрутизатор | -| `internal/router/router.go` | chi-роутер, маршруты | diff --git a/docs/architecture.docx b/docs/architecture.docx deleted file mode 100644 index b1b042cd30b0ce0c98a3f39197d5652e77dd037f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 35815 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 F{{hJCQ%(Q? diff --git a/docs/architecture.md b/docs/architecture.md deleted file mode 100644 index 693c8ec..0000000 --- a/docs/architecture.md +++ /dev/null @@ -1,277 +0,0 @@ -# Архитектура Ollama Proxy - -## 1. Общее описание - -**Ollama Proxy** — Go-сервис, прозрачно встающий между Codex CLI и локальной Ollama. Для Codex CLI прокси выглядит как обычная Ollama (те же эндпоинты, тот же формат). Внутри прокси: - -1. Проверяет авторизацию (токен из env) -2. Классифицирует запрос через Router LLM (gemma:1b) -3. Подменяет модель в запросе на подходящую -4. Проксирует запрос в реальную Ollama -5. Стримит ответ обратно в Codex CLI - -Сервис **stateless** — нет БД, нет сессий, нет хранения истории. История сообщений хранится на стороне Codex CLI. - ---- - -## 2. Архитектурная схема - -``` -┌─────────────────────────────────────────────────────────────────────┐ -│ CODEX CLI │ -│ (отправляет запросы как к обычной Ollama) │ -└────────────────────────────┬────────────────────────────────────────┘ - │ HTTP model: "auto" или "qwen2.5-coder:1.5b" - ▼ -┌─────────────────────────────────────────────────────────────────────┐ -│ OLLAMA PROXY (:11435) │ -│ │ -│ ┌──────────────────────────────────────────────────────────────┐ │ -│ │ Token Auth Middleware │ │ -│ │ Authorization: Bearer → 401 если неверный │ │ -│ └───────────────────────┬──────────────────────────────────────┘ │ -│ │ │ -│ ┌────────────────────────▼─────────────────────────────────────┐ │ -│ │ Router LLM │ │ -│ │ │ │ -│ │ Если model == "auto" или пустой: │ │ -│ │ → gemma:1b (синхронный вызов, stream=false) │ │ -│ │ → промпт: "Classify: code / document / general" │ │ -│ │ → ответ: одно слово │ │ -│ │ │ │ -│ │ Если model указана явно → пропустить, использовать как есть │ │ -│ └────────────────────────┬─────────────────────────────────────┘ │ -│ │ model подменён на целевую │ -│ ┌────────────────────────▼─────────────────────────────────────┐ │ -│ │ Proxy │ │ -│ │ POST /api/chat → Ollama /api/chat │ │ -│ │ Стриминг NDJSON построчно (bufio.Scanner + http.Flusher) │ │ -│ └────────────────────────┬─────────────────────────────────────┘ │ -└───────────────────────────┼─────────────────────────────────────────┘ - │ HTTP model: "qwen2.5-coder:1.5b" - ▼ -┌─────────────────────────────────────────────────────────────────────┐ -│ OLLAMA (:11434) │ -│ │ -│ gemma:1b qwen2.5-coder:1.5b (другие модели) │ -│ (router + docs) (код) │ -└─────────────────────────────────────────────────────────────────────┘ -``` - ---- - -## 3. Структура проекта - -``` -. -├── cmd/server/main.go # Точка входа: загрузка конфига, запуск HTTP-сервера -├── internal/ -│ ├── config/ -│ │ └── config.go # Парсинг env-переменных (caarlos0/env) -│ ├── model/ -│ │ └── ollama.go # Go-типы Ollama API: ChatRequest, ChatResponse, -│ │ # GenerateRequest, GenerateResponse, TagsResponse, Message -│ ├── handler/ -│ │ ├── middleware.go # Проверка Bearer-токена (простое сравнение строк) -│ │ └── proxy.go # HTTP-хендлеры: HandleChat, HandleGenerate, HandleTags -│ ├── service/ -│ │ ├── ollama_client.go # HTTP-клиент к Ollama: -│ │ │ # ProxyChat — стриминг /api/chat -│ │ │ # ProxyGenerate — стриминг /api/generate -│ │ │ # GetTags — список моделей -│ │ │ # Complete — синхронный вызов (для роутера) -│ │ └── router.go # LLM-маршрутизатор: -│ │ # Route(ctx, model, messages) → целевая модель -├── router/ -│ └── router.go # chi-роутер, подключение middleware и хендлеров -├── go.mod / go.sum # Зависимости -├── Makefile # build / run / test -├── .env # Локальные переменные (не в git) -├── .gitignore -├── .gitattributes # LF line endings -├── VERSION # Семантическая версия -└── docs/ - └── architecture.md # Этот файл -``` - ---- - -## 4. Поток обработки запроса - -### Случай 1: `model = "auto"` (первый запрос в сессии) - -``` -1. Codex CLI → POST /api/chat {"model":"auto", "messages":[...]} - -2. Middleware: проверить Authorization: Bearer - → если неверный: 401 Unauthorized - -3. Handler: декодировать ChatRequest из JSON - -4. Router.Route(ctx, "auto", messages): - a. Взять последнее user-сообщение - b. POST /api/chat к Ollama с model=gemma:1b, stream=false - Промпт: "Classify the following user request into exactly one category: - code, document, general. - Reply with ONLY the category name, nothing else. - User request: {текст}" - c. Получить ответ: "code" - d. Вернуть CODE_MODEL = "qwen2.5-coder:1.5b" - -5. Подменить req.Model = "qwen2.5-coder:1.5b" - -6. OllamaClient.ProxyChat(ctx, w, req): - a. POST /api/chat к Ollama с model=qwen2.5-coder:1.5b - b. Читать ответ построчно (bufio.Scanner) - c. Каждую строку NDJSON сразу писать в w + Flush() - -7. Codex CLI получает стриминг, видит model="qwen2.5-coder:1.5b" в ответе - → запоминает модель для последующих запросов -``` - -### Случай 2: `model = "qwen2.5-coder:1.5b"` (последующие запросы) - -``` -1. Codex CLI → POST /api/chat {"model":"qwen2.5-coder:1.5b", "messages":[...]} - -2. Middleware: проверить токен - -3. Router.Route(ctx, "qwen2.5-coder:1.5b", messages): - → модель указана явно, не "auto" → вернуть как есть - -4. OllamaClient.ProxyChat(ctx, w, req): - → прямой прокси к Ollama, Router LLM не вызывается -``` - ---- - -## 5. Ollama API — поддерживаемые эндпоинты - -| Метод | URL | Описание | -|-------|-----|----------| -| GET | `/health` | Проверка работоспособности (без авторизации) | -| POST | `/api/chat` | Чат. Streaming NDJSON | -| POST | `/api/generate` | Генерация текста. Streaming NDJSON | -| GET | `/api/tags` | Список моделей из реальной Ollama | - -### Формат запроса `/api/chat` - -```json -{ - "model": "auto", - "messages": [ - {"role": "user", "content": "напиши функцию на Go"} - ], - "stream": true -} -``` - -### Формат ответа `/api/chat` (NDJSON, одна строка = один чанк) - -```json -{"model":"qwen2.5-coder:1.5b","created_at":"2025-01-01T12:00:00Z","message":{"role":"assistant","content":"func"},"done":false} -{"model":"qwen2.5-coder:1.5b","created_at":"2025-01-01T12:00:01Z","message":{"role":"assistant","content":""},"done":true,"done_reason":"stop","total_duration":1234567890,"eval_count":42} -``` - ---- - -## 6. Авторизация - -Простая проверка Bearer-токена: - -``` -Authorization: Bearer -``` - -- Токен задаётся в `AUTH_TOKEN` (.env) -- Сравнение строк — никакого JWT, bcrypt, БД -- Неверный или отсутствующий токен → `401 Unauthorized` -- `/health` — без авторизации - -**Это временная заглушка.** В следующем этапе будет полноценная авторизация с JWT и БД. - ---- - -## 7. Router LLM — детали реализации - -**Промпт классификатора:** -``` -Classify the following user request into exactly one category: code, document, general. -Reply with ONLY the category name, nothing else. - -User request: {последнее user-сообщение} -``` - -**Вызов:** синхронный (`stream: false`), короткий timeout (5–10 сек). - -**Парсинг ответа:** -- Взять ответ, привести к нижнему регистру, убрать пробелы -- Если содержит "code" → CODE_MODEL -- Если содержит "document" → DOC_MODEL -- Иначе → GENERAL_MODEL (fallback) - -**Маппинг категорий → модели:** - -| Категория | Тестовая модель | Production модель | -|-----------|----------------|------------------| -| code | qwen2.5-coder:1.5b | qwen2.5-coder:32b | -| document | gemma:1b | deepseek-r1:70b | -| general | gemma:1b | llama3.3:70b | - ---- - -## 8. Streaming-прокси — детали реализации - -Ключевые требования: -- `http.Flusher` — сбрасывать буфер после каждой строки -- `bufio.Scanner` с буфером 1MB (для длинных строк с кодом) -- `http.Client` без глобального таймаута (используется контекст запроса) -- Отмена через `r.Context()` — если Codex CLI отключился, upstream-запрос к Ollama отменяется - -```go -// Псевдокод -scanner := bufio.NewScanner(resp.Body) -scanner.Buffer(make([]byte, 1024*1024), 1024*1024) - -flusher := w.(http.Flusher) -for scanner.Scan() { - w.Write(scanner.Bytes()) - w.Write([]byte("\n")) - flusher.Flush() -} -``` - ---- - -## 9. Переменные окружения - -| Переменная | Описание | По умолчанию | -|-----------|----------|-------------| -| `PROXY_PORT` | Порт прокси | 11435 | -| `AUTH_TOKEN` | Токен авторизации | — (обязательна) | -| `OLLAMA_URL` | URL реальной Ollama | http://localhost:11434 | -| `ROUTER_MODEL` | Модель-классификатор | gemma:1b | -| `CODE_MODEL` | Модель для кода | qwen2.5-coder:1.5b | -| `DOC_MODEL` | Модель для документов | gemma:1b | -| `GENERAL_MODEL` | Общая модель | gemma:1b | - ---- - -## 10. Технологический стек - -| Компонент | Технология | -|-----------|-----------| -| Язык | Go | -| HTTP-фреймворк | chi/v5 | -| Конфигурация | caarlos0/env/v11 | -| Streaming | bufio.Scanner + http.Flusher | -| LLM-инфраструктура | Ollama (локально) | - ---- - -## 11. Следующие этапы (после MVP) - -- **Авторизация**: полноценный JWT + PostgreSQL + регистрация/логин -- **История чатов**: хранение сообщений в БД (User → Chat → Message) -- **Production-модели**: qwen2.5-coder:32b, deepseek-r1:70b -- **Codex CLI**: тестирование сквозного потока с реальным клиентом diff --git a/docs/diagrams/mermaid.esm.min.mjs b/docs/diagrams/mermaid.esm.min.mjs deleted file mode 100644 index 7bd0f72..0000000 --- a/docs/diagrams/mermaid.esm.min.mjs +++ /dev/null @@ -1,14 +0,0 @@ -import{a as ht}from"./chunks/mermaid.esm.min/chunk-HQLFZTFY.mjs";import{a as Yt}from"./chunks/mermaid.esm.min/chunk-MEBTFSOL.mjs";import{a as Ut,b as qt}from"./chunks/mermaid.esm.min/chunk-7LIB5WBN.mjs";import{a as Bt}from"./chunks/mermaid.esm.min/chunk-L736DJ4U.mjs";import"./chunks/mermaid.esm.min/chunk-QTJCGBHB.mjs";import"./chunks/mermaid.esm.min/chunk-USR3SDWQ.mjs";import{b as St}from"./chunks/mermaid.esm.min/chunk-2VPXETT4.mjs";import"./chunks/mermaid.esm.min/chunk-S67DUUA5.mjs";import"./chunks/mermaid.esm.min/chunk-LM6QDVU5.mjs";import{a as Mt}from"./chunks/mermaid.esm.min/chunk-HESFG3RP.mjs";import{b as Vt,j as yt,l as $t,m as V,n as Nt,o as Ht}from"./chunks/mermaid.esm.min/chunk-YM3XIQPS.mjs";import"./chunks/mermaid.esm.min/chunk-TI4EEUUG.mjs";import{A as G,B as It,C as Y,D as Ft,G as _t,M as Gt,O as zt,aa as z,b as g,ba as X,c as gt,d as At,f as Tt,g as lt,ga as k,h as J,i as Z,j as Ct,k as Rt,r as tt,u as ut,v as kt,w as Ot,x as Pt,y as Dt,z as jt}from"./chunks/mermaid.esm.min/chunk-ZKYS2E5M.mjs";import{d as xt}from"./chunks/mermaid.esm.min/chunk-YPUTD6PB.mjs";import"./chunks/mermaid.esm.min/chunk-6BY5RJGC.mjs";import{a as r}from"./chunks/mermaid.esm.min/chunk-GTKDMUJJ.mjs";var Xt="c4",Ie=r(t=>/^\s*C4Context|C4Container|C4Component|C4Dynamic|C4Deployment/.test(t),"detector"),Fe=r(async()=>{let{diagram:t}=await import("./chunks/mermaid.esm.min/c4Diagram-GNV6VVOW.mjs");return{id:Xt,diagram:t}},"loader"),_e={id:Xt,detector:Ie,loader:Fe},Wt=_e;var Kt="flowchart",Ge=r((t,e)=>e?.flowchart?.defaultRenderer==="dagre-wrapper"||e?.flowchart?.defaultRenderer==="elk"?!1:/^\s*graph/.test(t),"detector"),ze=r(async()=>{let{diagram:t}=await import("./chunks/mermaid.esm.min/flowDiagram-RXJ4TZVH.mjs");return{id:Kt,diagram:t}},"loader"),Ve={id:Kt,detector:Ge,loader:ze},Qt=Ve;var Jt="flowchart-v2",$e=r((t,e)=>e?.flowchart?.defaultRenderer==="dagre-d3"?!1:(e?.flowchart?.defaultRenderer==="elk"&&(e.layout="elk"),/^\s*graph/.test(t)&&e?.flowchart?.defaultRenderer==="dagre-wrapper"?!0:/^\s*flowchart/.test(t)),"detector"),Ne=r(async()=>{let{diagram:t}=await import("./chunks/mermaid.esm.min/flowDiagram-RXJ4TZVH.mjs");return{id:Jt,diagram:t}},"loader"),He={id:Jt,detector:$e,loader:Ne},Zt=He;var tr="er",Ue=r(t=>/^\s*erDiagram/.test(t),"detector"),qe=r(async()=>{let{diagram:t}=await import("./chunks/mermaid.esm.min/erDiagram-K5RJBHCA.mjs");return{id:tr,diagram:t}},"loader"),Be={id:tr,detector:Ue,loader:qe},rr=Be;var er="gitGraph",Ye=r(t=>/^\s*gitGraph/.test(t),"detector"),Xe=r(async()=>{let{diagram:t}=await import("./chunks/mermaid.esm.min/gitGraphDiagram-WO7WVN2C.mjs");return{id:er,diagram:t}},"loader"),We={id:er,detector:Ye,loader:Xe},ar=We;var ir="gantt",Ke=r(t=>/^\s*gantt/.test(t),"detector"),Qe=r(async()=>{let{diagram:t}=await import("./chunks/mermaid.esm.min/ganttDiagram-5MLOKHXO.mjs");return{id:ir,diagram:t}},"loader"),Je={id:ir,detector:Ke,loader:Qe},or=Je;var nr="info",Ze=r(t=>/^\s*info/.test(t),"detector"),ta=r(async()=>{let{diagram:t}=await import("./chunks/mermaid.esm.min/infoDiagram-E3C2IIUA.mjs");return{id:nr,diagram:t}},"loader"),sr={id:nr,detector:Ze,loader:ta};var cr="pie",ra=r(t=>/^\s*pie/.test(t),"detector"),ea=r(async()=>{let{diagram:t}=await import("./chunks/mermaid.esm.min/pieDiagram-Q56JBFDI.mjs");return{id:cr,diagram:t}},"loader"),mr={id:cr,detector:ra,loader:ea};var pr="quadrantChart",aa=r(t=>/^\s*quadrantChart/.test(t),"detector"),ia=r(async()=>{let{diagram:t}=await import("./chunks/mermaid.esm.min/quadrantDiagram-KBTC774P.mjs");return{id:pr,diagram:t}},"loader"),oa={id:pr,detector:aa,loader:ia},dr=oa;var fr="xychart",na=r(t=>/^\s*xychart-beta/.test(t),"detector"),sa=r(async()=>{let{diagram:t}=await import("./chunks/mermaid.esm.min/xychartDiagram-4C6ER3FX.mjs");return{id:fr,diagram:t}},"loader"),ca={id:fr,detector:na,loader:sa},gr=ca;var lr="requirement",ma=r(t=>/^\s*requirement(Diagram)?/.test(t),"detector"),pa=r(async()=>{let{diagram:t}=await import("./chunks/mermaid.esm.min/requirementDiagram-OPD2HUS5.mjs");return{id:lr,diagram:t}},"loader"),da={id:lr,detector:ma,loader:pa},ur=da;var Dr="sequence",fa=r(t=>/^\s*sequenceDiagram/.test(t),"detector"),ga=r(async()=>{let{diagram:t}=await import("./chunks/mermaid.esm.min/sequenceDiagram-ODO66PDE.mjs");return{id:Dr,diagram:t}},"loader"),la={id:Dr,detector:fa,loader:ga},yr=la;var xr="class",ua=r((t,e)=>e?.class?.defaultRenderer==="dagre-wrapper"?!1:/^\s*classDiagram/.test(t),"detector"),Da=r(async()=>{let{diagram:t}=await import("./chunks/mermaid.esm.min/classDiagram-MKYM2BOE.mjs");return{id:xr,diagram:t}},"loader"),ya={id:xr,detector:ua,loader:Da},hr=ya;var Er="classDiagram",xa=r((t,e)=>/^\s*classDiagram/.test(t)&&e?.class?.defaultRenderer==="dagre-wrapper"?!0:/^\s*classDiagram-v2/.test(t),"detector"),ha=r(async()=>{let{diagram:t}=await import("./chunks/mermaid.esm.min/classDiagram-v2-PRA2ZCF7.mjs");return{id:Er,diagram:t}},"loader"),Ea={id:Er,detector:xa,loader:ha},wr=Ea;var br="state",wa=r((t,e)=>e?.state?.defaultRenderer==="dagre-wrapper"?!1:/^\s*stateDiagram/.test(t),"detector"),ba=r(async()=>{let{diagram:t}=await import("./chunks/mermaid.esm.min/stateDiagram-76M766UR.mjs");return{id:br,diagram:t}},"loader"),La={id:br,detector:wa,loader:ba},Lr=La;var vr="stateDiagram",va=r((t,e)=>!!(/^\s*stateDiagram-v2/.test(t)||/^\s*stateDiagram/.test(t)&&e?.state?.defaultRenderer==="dagre-wrapper"),"detector"),Sa=r(async()=>{let{diagram:t}=await import("./chunks/mermaid.esm.min/stateDiagram-v2-NOSC7VFN.mjs");return{id:vr,diagram:t}},"loader"),Ma={id:vr,detector:va,loader:Sa},Sr=Ma;var Mr="journey",Aa=r(t=>/^\s*journey/.test(t),"detector"),Ta=r(async()=>{let{diagram:t}=await import("./chunks/mermaid.esm.min/journeyDiagram-UZIDTGLP.mjs");return{id:Mr,diagram:t}},"loader"),Ca={id:Mr,detector:Aa,loader:Ta},Ar=Ca;var Ra=r((t,e,a)=>{g.debug(`rendering svg for syntax error -`);let i=Yt(e),o=i.append("g");i.attr("viewBox","0 0 2412 512"),Gt(i,100,512,!0),o.append("path").attr("class","error-icon").attr("d","m411.313,123.313c6.25-6.25 6.25-16.375 0-22.625s-16.375-6.25-22.625,0l-32,32-9.375,9.375-20.688-20.688c-12.484-12.5-32.766-12.5-45.25,0l-16,16c-1.261,1.261-2.304,2.648-3.31,4.051-21.739-8.561-45.324-13.426-70.065-13.426-105.867,0-192,86.133-192,192s86.133,192 192,192 192-86.133 192-192c0-24.741-4.864-48.327-13.426-70.065 1.402-1.007 2.79-2.049 4.051-3.31l16-16c12.5-12.492 12.5-32.758 0-45.25l-20.688-20.688 9.375-9.375 32.001-31.999zm-219.313,100.687c-52.938,0-96,43.063-96,96 0,8.836-7.164,16-16,16s-16-7.164-16-16c0-70.578 57.422-128 128-128 8.836,0 16,7.164 16,16s-7.164,16-16,16z"),o.append("path").attr("class","error-icon").attr("d","m459.02,148.98c-6.25-6.25-16.375-6.25-22.625,0s-6.25,16.375 0,22.625l16,16c3.125,3.125 7.219,4.688 11.313,4.688 4.094,0 8.188-1.563 11.313-4.688 6.25-6.25 6.25-16.375 0-22.625l-16.001-16z"),o.append("path").attr("class","error-icon").attr("d","m340.395,75.605c3.125,3.125 7.219,4.688 11.313,4.688 4.094,0 8.188-1.563 11.313-4.688 6.25-6.25 6.25-16.375 0-22.625l-16-16c-6.25-6.25-16.375-6.25-22.625,0s-6.25,16.375 0,22.625l15.999,16z"),o.append("path").attr("class","error-icon").attr("d","m400,64c8.844,0 16-7.164 16-16v-32c0-8.836-7.156-16-16-16-8.844,0-16,7.164-16,16v32c0,8.836 7.156,16 16,16z"),o.append("path").attr("class","error-icon").attr("d","m496,96.586h-32c-8.844,0-16,7.164-16,16 0,8.836 7.156,16 16,16h32c8.844,0 16-7.164 16-16 0-8.836-7.156-16-16-16z"),o.append("path").attr("class","error-icon").attr("d","m436.98,75.605c3.125,3.125 7.219,4.688 11.313,4.688 4.094,0 8.188-1.563 11.313-4.688l32-32c6.25-6.25 6.25-16.375 0-22.625s-16.375-6.25-22.625,0l-32,32c-6.251,6.25-6.251,16.375-0.001,22.625z"),o.append("text").attr("class","error-text").attr("x",1440).attr("y",250).attr("font-size","150px").style("text-anchor","middle").text("Syntax error in text"),o.append("text").attr("class","error-text").attr("x",1250).attr("y",400).attr("font-size","100px").style("text-anchor","middle").text(`mermaid version ${a}`)},"draw"),Et={draw:Ra},Tr=Et;var ka={db:{},renderer:Et,parser:{parse:r(()=>{},"parse")}},Cr=ka;var Rr="flowchart-elk",Oa=r((t,e={})=>/^\s*flowchart-elk/.test(t)||/^\s*flowchart|graph/.test(t)&&e?.flowchart?.defaultRenderer==="elk"?(e.layout="elk",!0):!1,"detector"),Pa=r(async()=>{let{diagram:t}=await import("./chunks/mermaid.esm.min/flowDiagram-RXJ4TZVH.mjs");return{id:Rr,diagram:t}},"loader"),ja={id:Rr,detector:Oa,loader:Pa},kr=ja;var Or="timeline",Ia=r(t=>/^\s*timeline/.test(t),"detector"),Fa=r(async()=>{let{diagram:t}=await import("./chunks/mermaid.esm.min/timeline-definition-VFFECQCT.mjs");return{id:Or,diagram:t}},"loader"),_a={id:Or,detector:Ia,loader:Fa},Pr=_a;var jr="mindmap",Ga=r(t=>/^\s*mindmap/.test(t),"detector"),za=r(async()=>{let{diagram:t}=await import("./chunks/mermaid.esm.min/mindmap-definition-RP2J3NYQ.mjs");return{id:jr,diagram:t}},"loader"),Va={id:jr,detector:Ga,loader:za},Ir=Va;var Fr="kanban",$a=r(t=>/^\s*kanban/.test(t),"detector"),Na=r(async()=>{let{diagram:t}=await import("./chunks/mermaid.esm.min/kanban-definition-4PSEFK7X.mjs");return{id:Fr,diagram:t}},"loader"),Ha={id:Fr,detector:$a,loader:Na},_r=Ha;var Gr="sankey",Ua=r(t=>/^\s*sankey-beta/.test(t),"detector"),qa=r(async()=>{let{diagram:t}=await import("./chunks/mermaid.esm.min/sankeyDiagram-2NKXCTV4.mjs");return{id:Gr,diagram:t}},"loader"),Ba={id:Gr,detector:Ua,loader:qa},zr=Ba;var Vr="packet",Ya=r(t=>/^\s*packet-beta/.test(t),"detector"),Xa=r(async()=>{let{diagram:t}=await import("./chunks/mermaid.esm.min/diagram-7BLTIMBB.mjs");return{id:Vr,diagram:t}},"loader"),$r={id:Vr,detector:Ya,loader:Xa};var Nr="radar",Wa=r(t=>/^\s*radar-beta/.test(t),"detector"),Ka=r(async()=>{let{diagram:t}=await import("./chunks/mermaid.esm.min/diagram-R7SGKMCD.mjs");return{id:Nr,diagram:t}},"loader"),Hr={id:Nr,detector:Wa,loader:Ka};var Ur="block",Qa=r(t=>/^\s*block-beta/.test(t),"detector"),Ja=r(async()=>{let{diagram:t}=await import("./chunks/mermaid.esm.min/blockDiagram-GQNB4GIR.mjs");return{id:Ur,diagram:t}},"loader"),Za={id:Ur,detector:Qa,loader:Ja},qr=Za;var Br="architecture",ti=r(t=>/^\s*architecture/.test(t),"detector"),ri=r(async()=>{let{diagram:t}=await import("./chunks/mermaid.esm.min/architectureDiagram-YZ6UH2CF.mjs");return{id:Br,diagram:t}},"loader"),ei={id:Br,detector:ti,loader:ri},Yr=ei;var Xr=!1,$=r(()=>{Xr||(Xr=!0,z("error",Cr,t=>t.toLowerCase().trim()==="error"),z("---",{db:{clear:r(()=>{},"clear")},styles:{},renderer:{draw:r(()=>{},"draw")},parser:{parse:r(()=>{throw new Error("Diagrams beginning with --- are not valid. If you were trying to use a YAML front-matter, please ensure that you've correctly opened and closed the YAML front-matter with un-indented `---` blocks")},"parse")},init:r(()=>null,"init")},t=>t.toLowerCase().trimStart().startsWith("---")),Z(Wt,_r,wr,hr,rr,or,sr,mr,ur,yr,kr,Zt,Qt,Ir,Pr,ar,Sr,Lr,Ar,dr,zr,$r,gr,qr,Yr,Hr))},"addDiagrams");var Wr=r(async()=>{g.debug("Loading registered diagrams");let e=(await Promise.allSettled(Object.entries(lt).map(async([a,{detector:i,loader:o}])=>{if(o)try{X(a)}catch{try{let{diagram:n,id:m}=await o();z(m,n,i)}catch(n){throw g.error(`Failed to load external diagram with key ${a}. Removing from detectors.`),delete lt[a],n}}}))).filter(a=>a.status==="rejected");if(e.length>0){g.error(`Failed to load ${e.length} external diagrams`);for(let a of e)g.error(a);throw new Error(`Failed to load ${e.length} external diagrams`)}},"loadRegisteredDiagrams");var rt="comm",et="rule",at="decl";var Kr="@import";var Qr="@namespace",Jr="@keyframes";var Zr="@layer";var wt=Math.abs,W=String.fromCharCode;function it(t){return t.trim()}r(it,"trim");function K(t,e,a){return t.replace(e,a)}r(K,"replace");function te(t,e,a){return t.indexOf(e,a)}r(te,"indexof");function j(t,e){return t.charCodeAt(e)|0}r(j,"charat");function I(t,e,a){return t.slice(e,a)}r(I,"substr");function h(t){return t.length}r(h,"strlen");function re(t){return t.length}r(re,"sizeof");function N(t,e){return e.push(t),t}r(N,"append");var ot=1,H=1,ee=0,w=0,D=0,q="";function nt(t,e,a,i,o,n,m,s){return{value:t,root:e,parent:a,type:i,props:o,children:n,line:ot,column:H,length:m,return:"",siblings:s}}r(nt,"node");function ae(){return D}r(ae,"char");function ie(){return D=w>0?j(q,--w):0,H--,D===10&&(H=1,ot--),D}r(ie,"prev");function b(){return D=w2||U(D)>3?"":" "}r(se,"whitespace");function ce(t,e){for(;--e&&b()&&!(D<48||D>102||D>57&&D<65||D>70&&D<97););return st(t,Q()+(e<6&&O()==32&&b()==32))}r(ce,"escaping");function bt(t){for(;b();)switch(D){case t:return w;case 34:case 39:t!==34&&t!==39&&bt(D);break;case 40:t===41&&bt(t);break;case 92:b();break}return w}r(bt,"delimiter");function me(t,e){for(;b()&&t+D!==57;)if(t+D===84&&O()===47)break;return"/*"+st(e,w-1)+"*"+W(t===47?t:b())}r(me,"commenter");function pe(t){for(;!U(O());)b();return st(t,w)}r(pe,"identifier");function ge(t){return ne(mt("",null,null,null,[""],t=oe(t),0,[0],t))}r(ge,"compile");function mt(t,e,a,i,o,n,m,s,c){for(var l=0,y=0,p=m,x=0,A=0,L=0,f=1,C=1,v=1,u=0,S="",R=o,T=n,E=i,d=S;C;)switch(L=u,u=b()){case 40:if(L!=108&&j(d,p-1)==58){te(d+=K(ct(u),"&","&\f"),"&\f",wt(l?s[l-1]:0))!=-1&&(v=-1);break}case 34:case 39:case 91:d+=ct(u);break;case 9:case 10:case 13:case 32:d+=se(L);break;case 92:d+=ce(Q()-1,7);continue;case 47:switch(O()){case 42:case 47:N(ai(me(b(),Q()),e,a,c),c),(U(L||1)==5||U(O()||1)==5)&&h(d)&&I(d,-1,void 0)!==" "&&(d+=" ");break;default:d+="/"}break;case 123*f:s[l++]=h(d)*v;case 125*f:case 59:case 0:switch(u){case 0:case 125:C=0;case 59+y:v==-1&&(d=K(d,/\f/g,"")),A>0&&(h(d)-p||f===0&&L===47)&&N(A>32?fe(d+";",i,a,p-1,c):fe(K(d," ","")+";",i,a,p-2,c),c);break;case 59:d+=";";default:if(N(E=de(d,e,a,l,y,o,s,S,R=[],T=[],p,n),n),u===123)if(y===0)mt(d,e,E,E,R,n,p,s,T);else{switch(x){case 99:if(j(d,3)===110)break;case 108:if(j(d,2)===97)break;default:y=0;case 100:case 109:case 115:}y?mt(t,E,E,i&&N(de(t,E,E,0,0,o,s,S,o,R=[],p,T),T),o,T,p,s,i?R:T):mt(d,E,E,E,[""],T,0,s,T)}}l=y=A=0,f=v=1,S=d="",p=m;break;case 58:p=1+h(d),A=L;default:if(f<1){if(u==123)--f;else if(u==125&&f++==0&&ie()==125)continue}switch(d+=W(u),u*f){case 38:v=y>0?1:(d+="\f",-1);break;case 44:s[l++]=(h(d)-1)*v,v=1;break;case 64:O()===45&&(d+=ct(b())),x=O(),y=p=h(S=d+=pe(Q())),u++;break;case 45:L===45&&h(d)==2&&(f=0)}}return n}r(mt,"parse");function de(t,e,a,i,o,n,m,s,c,l,y,p){for(var x=o-1,A=o===0?n:[""],L=re(A),f=0,C=0,v=0;f0?A[u]+" "+S:K(S,/&\f/g,A[u])))&&(c[v++]=R);return nt(t,e,a,o===0?et:s,c,l,y,p)}r(de,"ruleset");function ai(t,e,a,i){return nt(t,e,a,rt,W(ae()),I(t,2,-2),0,i)}r(ai,"comment");function fe(t,e,a,i,o){return nt(t,e,a,at,I(t,0,i),I(t,i+1,-1),i,o)}r(fe,"declaration");function pt(t,e){for(var a="",i=0;i{ye.forEach(t=>{t()}),ye=[]},"attachFunctions");var he=r(t=>t.replace(/^\s*%%(?!{)[^\n]+\n?/gm,"").trimStart(),"cleanupComments");function Ee(t){let e=t.match(At);if(!e)return{text:t,metadata:{}};let a=qt(e[1],{schema:Ut})??{};a=typeof a=="object"&&!Array.isArray(a)?a:{};let i={};return a.displayMode&&(i.displayMode=a.displayMode.toString()),a.title&&(i.title=a.title.toString()),a.config&&(i.config=a.config),{text:t.slice(e[0].length),metadata:i}}r(Ee,"extractFrontMatter");var ni=r(t=>t.replace(/\r\n?/g,` -`).replace(/<(\w+)([^>]*)>/g,(e,a,i)=>"<"+a+i.replace(/="([^"]*)"/g,"='$1'")+">"),"cleanupText"),si=r(t=>{let{text:e,metadata:a}=Ee(t),{displayMode:i,title:o,config:n={}}=a;return i&&(n.gantt||(n.gantt={}),n.gantt.displayMode=i),{title:o,config:n,text:e}},"processFrontmatter"),ci=r(t=>{let e=V.detectInit(t)??{},a=V.detectDirective(t,"wrap");return Array.isArray(a)?e.wrap=a.some(({type:i})=>i==="wrap"):a?.type==="wrap"&&(e.wrap=!0),{text:Vt(t),directive:e}},"processDirectives");function Lt(t){let e=ni(t),a=si(e),i=ci(a.text),o=$t(a.config,i.directive);return t=he(i.text),{code:t,title:a.title,config:o}}r(Lt,"preprocessDiagram");function we(t){let e=new TextEncoder().encode(t),a=Array.from(e,i=>String.fromCodePoint(i)).join("");return btoa(a)}r(we,"toBase64");var mi=5e4,pi="graph TB;a[Maximum text size in diagram exceeded];style a fill:#faa",di="sandbox",fi="loose",gi="http://www.w3.org/2000/svg",li="http://www.w3.org/1999/xlink",ui="http://www.w3.org/1999/xhtml",Di="100%",yi="100%",xi="border:0;margin:0;",hi="margin:0",Ei="allow-top-navigation-by-user-activation allow-popups",wi='The "iframe" tag is not supported by your browser.',bi=["foreignobject"],Li=["dominant-baseline"];function Se(t){let e=Lt(t);return Y(),It(e.config??{}),e}r(Se,"processAndSetConfigs");async function vi(t,e){$();try{let{code:a,config:i}=Se(t);return{diagramType:(await Me(a)).type,config:i}}catch(a){if(e?.suppressErrors)return!1;throw a}}r(vi,"parse");var be=r((t,e,a=[])=>` -.${t} ${e} { ${a.join(" !important; ")} !important; }`,"cssImportantStyles"),Si=r((t,e=new Map)=>{let a="";if(t.themeCSS!==void 0&&(a+=` -${t.themeCSS}`),t.fontFamily!==void 0&&(a+=` -:root { --mermaid-font-family: ${t.fontFamily}}`),t.altFontFamily!==void 0&&(a+=` -:root { --mermaid-alt-font-family: ${t.altFontFamily}}`),e instanceof Map){let m=t.htmlLabels??t.flowchart?.htmlLabels?["> *","span"]:["rect","polygon","ellipse","circle","path"];e.forEach(s=>{xt(s.styles)||m.forEach(c=>{a+=be(s.id,c,s.styles)}),xt(s.textStyles)||(a+=be(s.id,"tspan",(s?.textStyles||[]).map(c=>c.replace("color","fill"))))})}return a},"createCssStyles"),Mi=r((t,e,a,i)=>{let o=Si(t,a),n=zt(e,o,t.themeVariables);return pt(ge(`${i}{${n}}`),le)},"createUserStyles"),Ai=r((t="",e,a)=>{let i=t;return!a&&!e&&(i=i.replace(/marker-end="url\([\d+./:=?A-Za-z-]*?#/g,'marker-end="url(#')),i=Ht(i),i=i.replace(/
/g,"
"),i},"cleanUpSvgCode"),Ti=r((t="",e)=>{let a=e?.viewBox?.baseVal?.height?e.viewBox.baseVal.height+"px":yi,i=we(`${t}`);return``},"putIntoIFrame"),Le=r((t,e,a,i,o)=>{let n=t.append("div");n.attr("id",a),i&&n.attr("style",i);let m=n.append("svg").attr("id",e).attr("width","100%").attr("xmlns",gi);return o&&m.attr("xmlns:xlink",o),m.append("g"),t},"appendDivSvgG");function ve(t,e){return t.append("iframe").attr("id",e).attr("style","width: 100%; height: 100%;").attr("sandbox","")}r(ve,"sandboxedIframe");var Ci=r((t,e,a,i)=>{t.getElementById(e)?.remove(),t.getElementById(a)?.remove(),t.getElementById(i)?.remove()},"removeExistingElements"),Ri=r(async function(t,e,a){$();let i=Se(e);e=i.code;let o=G();g.debug(o),e.length>(o?.maxTextSize??mi)&&(e=pi);let n="#"+t,m="i"+t,s="#"+m,c="d"+t,l="#"+c,y=r(()=>{let ft=k(x?s:l).node();ft&&"remove"in ft&&ft.remove()},"removeTempElements"),p=k("body"),x=o.securityLevel===di,A=o.securityLevel===fi,L=o.fontFamily;if(a!==void 0){if(a&&(a.innerHTML=""),x){let M=ve(k(a),m);p=k(M.nodes()[0].contentDocument.body),p.node().style.margin=0}else p=k(a);Le(p,t,c,`font-family: ${L}`,li)}else{if(Ci(document,t,c,m),x){let M=ve(k("body"),m);p=k(M.nodes()[0].contentDocument.body),p.node().style.margin=0}else p=k("body");Le(p,t,c)}let f,C;try{f=await B.fromText(e,{title:i.title})}catch(M){if(o.suppressErrorRendering)throw y(),M;f=await B.fromText("error"),C=M}let v=p.select(l).node(),u=f.type,S=v.firstChild,R=S.firstChild,T=f.renderer.getClasses?.(e,f),E=Mi(o,u,T,n),d=document.createElement("style");d.innerHTML=E,S.insertBefore(d,R);try{await f.renderer.draw(e,t,ht.version,f)}catch(M){throw o.suppressErrorRendering?y():Tr.draw(e,t,ht.version),M}let Oe=p.select(`${l} svg`),Pe=f.db.getAccTitle?.(),je=f.db.getAccDescription?.();Oi(u,Oe,Pe,je),p.select(`[id="${t}"]`).selectAll("foreignobject > *").attr("xmlns",ui);let _=p.select(l).node().innerHTML;if(g.debug("config.arrowMarkerAbsolute",o.arrowMarkerAbsolute),_=Ai(_,x,_t(o.arrowMarkerAbsolute)),x){let M=p.select(l+" svg").node();_=Ti(_,M)}else A||(_=Ft.sanitize(_,{ADD_TAGS:bi,ADD_ATTR:Li,HTML_INTEGRATION_POINTS:{foreignobject:!0}}));if(xe(),C)throw C;return y(),{diagramType:u,svg:_,bindFunctions:f.db.bindFunctions}},"render");function ki(t={}){let e=Rt({},t);e?.fontFamily&&!e.themeVariables?.fontFamily&&(e.themeVariables||(e.themeVariables={}),e.themeVariables.fontFamily=e.fontFamily),Ot(e),e?.theme&&e.theme in tt?e.themeVariables=tt[e.theme].getThemeVariables(e.themeVariables):e&&(e.themeVariables=tt.default.getThemeVariables(e.themeVariables));let a=typeof e=="object"?kt(e):Dt();gt(a.logLevel),$()}r(ki,"initialize");var Me=r((t,e={})=>{let{code:a}=Lt(t);return B.fromText(a,e)},"getDiagramFromText");function Oi(t,e,a,i){ue(e,t),De(e,a,i,e.attr("id"))}r(Oi,"addA11yInfo");var F=Object.freeze({render:Ri,parse:vi,getDiagramFromText:Me,initialize:ki,getConfig:G,setConfig:jt,getSiteConfig:Dt,updateSiteConfig:Pt,reset:r(()=>{Y()},"reset"),globalReset:r(()=>{Y(ut)},"globalReset"),defaultConfig:ut});gt(G().logLevel);Y(G());var Pi=r((t,e,a)=>{g.warn(t),yt(t)?(a&&a(t.str,t.hash),e.push({...t,message:t.str,error:t})):(a&&a(t),t instanceof Error&&e.push({str:t.message,message:t.message,hash:t.name,error:t}))},"handleError"),Ae=r(async function(t={querySelector:".mermaid"}){try{await ji(t)}catch(e){if(yt(e)&&g.error(e.str),P.parseError&&P.parseError(e),!t.suppressErrors)throw g.error("Use the suppressErrors option to suppress these errors"),e}},"run"),ji=r(async function({postRenderCallback:t,querySelector:e,nodes:a}={querySelector:".mermaid"}){let i=F.getConfig();g.debug(`${t?"":"No "}Callback function found`);let o;if(a)o=a;else if(e)o=document.querySelectorAll(e);else throw new Error("Nodes and querySelector are both undefined");g.debug(`Found ${o.length} diagrams`),i?.startOnLoad!==void 0&&(g.debug("Start On Load: "+i?.startOnLoad),F.updateSiteConfig({startOnLoad:i?.startOnLoad}));let n=new V.InitIDGenerator(i.deterministicIds,i.deterministicIDSeed),m,s=[];for(let c of Array.from(o)){g.info("Rendering diagram: "+c.id);if(c.getAttribute("data-processed"))continue;c.setAttribute("data-processed","true");let l=`mermaid-${n.next()}`;m=c.innerHTML,m=Mt(V.entityDecode(m)).trim().replace(//gi,"
");let y=V.detectInit(m);y&&g.debug("Detected early reinit: ",y);try{let{svg:p,bindFunctions:x}=await ke(l,m,c);c.innerHTML=p,t&&await t(l),x&&x(c)}catch(p){Pi(p,s,P.parseError)}}if(s.length>0)throw s[0]},"runThrowsErrors"),Te=r(function(t){F.initialize(t)},"initialize"),Ii=r(async function(t,e,a){g.warn("mermaid.init is deprecated. Please use run instead."),t&&Te(t);let i={postRenderCallback:a,querySelector:".mermaid"};typeof e=="string"?i.querySelector=e:e&&(e instanceof HTMLElement?i.nodes=[e]:i.nodes=e),await Ae(i)},"init"),Fi=r(async(t,{lazyLoad:e=!0}={})=>{$(),Z(...t),e===!1&&await Wr()},"registerExternalDiagrams"),Ce=r(function(){if(P.startOnLoad){let{startOnLoad:t}=F.getConfig();t&&P.run().catch(e=>g.error("Mermaid failed to initialize",e))}},"contentLoaded");if(typeof document<"u"){window.addEventListener("load",Ce,!1)}var _i=r(function(t){P.parseError=t},"setParseErrorHandler"),dt=[],vt=!1,Re=r(async()=>{if(!vt){for(vt=!0;dt.length>0;){let t=dt.shift();if(t)try{await t()}catch(e){g.error("Error executing queue",e)}}vt=!1}},"executeQueue"),Gi=r(async(t,e)=>new Promise((a,i)=>{let o=r(()=>new Promise((n,m)=>{F.parse(t,e).then(s=>{n(s),a(s)},s=>{g.error("Error parsing",s),P.parseError?.(s),m(s),i(s)})}),"performCall");dt.push(o),Re().catch(i)}),"parse"),ke=r((t,e,a)=>new Promise((i,o)=>{let n=r(()=>new Promise((m,s)=>{F.render(t,e,a).then(c=>{m(c),i(c)},c=>{g.error("Error parsing",c),P.parseError?.(c),s(c),o(c)})}),"performCall");dt.push(n),Re().catch(o)}),"render"),P={startOnLoad:!0,mermaidAPI:F,parse:Gi,render:ke,init:Ii,run:Ae,registerExternalDiagrams:Fi,registerLayoutLoaders:Bt,initialize:Te,parseError:void 0,contentLoaded:Ce,setParseErrorHandler:_i,detectType:J,registerIconPacks:St},Hs=P;export{Hs as default}; -/*! Check if previously processed */ -/*! - * Wait for document loaded before starting the execution - */ diff --git a/docs/diagrams/preview.html b/docs/diagrams/preview.html deleted file mode 100644 index 67a00f8..0000000 --- a/docs/diagrams/preview.html +++ /dev/null @@ -1,36 +0,0 @@ - - - - - ORM Template — User ERD - - - -

ORM Template — ER-диаграмма

-
Единственная модель: User и ее атрибуты.
- -
-erDiagram
-    USER {
-        uuid id PK "первичный ключ пользователя"
-        text login "уникальный логин"
-        text password "хэш пароля"
-        text role "роль пользователя"
-        bool is_active "активность"
-        bool is_temporal "пароль временный, хранится в открытом виде"
-        timestamptz created_at "дата создания"
-    }
-
- - - - diff --git a/docs/generate_docx.py b/docs/generate_docx.py deleted file mode 100644 index 5d9e674..0000000 --- a/docs/generate_docx.py +++ /dev/null @@ -1,369 +0,0 @@ -"""Generate architecture.docx from architecture content.""" -from docx import Document -from docx.shared import Pt, Inches, RGBColor -from docx.enum.text import WD_ALIGN_PARAGRAPH -from docx.enum.table import WD_TABLE_ALIGNMENT -import os - -doc = Document() - -style = doc.styles['Normal'] -font = style.font -font.name = 'Calibri' -font.size = Pt(11) - -def add_heading(text, level=1): - h = doc.add_heading(text, level=level) - for run in h.runs: - run.font.color.rgb = RGBColor(0x1A, 0x1A, 0x2E) - -def add_para(text, bold=False): - p = doc.add_paragraph() - run = p.add_run(text) - run.bold = bold - return p - -def add_bullet(text, bold_prefix=None): - p = doc.add_paragraph(style='List Bullet') - if bold_prefix: - run = p.add_run(bold_prefix) - run.bold = True - p.add_run(text) - else: - p.add_run(text) - -def add_table(headers, rows): - table = doc.add_table(rows=1, cols=len(headers)) - table.style = 'Light Grid Accent 1' - table.alignment = WD_TABLE_ALIGNMENT.LEFT - hdr = table.rows[0] - for i, h in enumerate(headers): - hdr.cells[i].text = h - for p in hdr.cells[i].paragraphs: - for run in p.runs: - run.bold = True - for row_data in rows: - row = table.add_row() - for i, val in enumerate(row_data): - row.cells[i].text = val - doc.add_paragraph() - -def add_code(text): - p = doc.add_paragraph() - run = p.add_run(text) - run.font.name = 'Consolas' - run.font.size = Pt(9) - run.font.color.rgb = RGBColor(0x33, 0x33, 0x33) - pf = p.paragraph_format - pf.space_before = Pt(4) - pf.space_after = Pt(4) - -# === TITLE === -title = doc.add_heading('Архитектура AI-платформы', level=0) -title.alignment = WD_ALIGN_PARAGRAPH.CENTER -doc.add_paragraph() - -# === 1. ОБЩЕЕ ОПИСАНИЕ === -add_heading('1. Общее описание') -add_para('AI-платформа для интеллектуальной маршрутизации запросов пользователей по различным LLM-моделям. Система состоит из двух основных компонентов:') -add_bullet(' — авторизация, управление чатами и сообщениями, REST API', 'Go Backend') -add_bullet(' — классификация запросов и маршрутизация к нужной модели', 'Python LLM Orchestrator') - -# === 2. АРХИТЕКТУРНАЯ СХЕМА === -add_heading('2. Архитектурная схема') -add_para('Общий поток данных:', bold=True) -add_code( - 'Пользователь → Go Backend (:8080) → Python Orchestrator (:8000) → LLM (Groq API)\n' - ' ↕ ↕\n' - ' PostgreSQL Router LLM → Target LLM' -) -doc.add_paragraph() - -add_para('Компоненты Go Backend:', bold=True) -add_bullet(' — /api/auth/register, /api/auth/login', 'Auth Handler') -add_bullet(' — CRUD /api/chats', 'Chat Handler') -add_bullet(' — POST/GET /api/chats/{id}/messages', 'Message Handler') -add_bullet(' — bcrypt + JWT генерация/валидация', 'Auth Service') -add_bullet(' — CRUD операции с чатами', 'Chat Service') -add_bullet(' — sliding window контекст + вызов оркестратора', 'Message Service') -add_bullet(' — HTTP-клиент к Python FastAPI', 'Orchestrator Client') - -doc.add_paragraph() -add_para('Компоненты Python Orchestrator:', bold=True) -add_bullet(' — маленькая быстрая модель (llama-3.1-8b-instant) классифицирует запрос', 'Router LLM') -add_bullet(' — Qwen 2.5 Coder 32B для программирования', 'Code Model') -add_bullet(' — DeepSeek R1 70B для анализа документов', 'Document Model') -add_bullet(' — Llama 3.3 70B для общих вопросов', 'General Model') - -# === 3. СТРУКТУРА ПРОЕКТА === -add_heading('3. Структура проекта') -add_code( - '.\n' - '├── cmd/server/main.go # Точка входа Go backend\n' - '├── internal/\n' - '│ ├── config/config.go # Конфигурация (env-переменные)\n' - '│ ├── handler/\n' - '│ │ ├── auth.go # Хендлеры регистрации и логина\n' - '│ │ ├── chat.go # CRUD чатов\n' - '│ │ ├── message.go # Отправка/получение сообщений\n' - '│ │ └── middleware.go # JWT middleware\n' - '│ ├── service/\n' - '│ │ ├── auth.go # bcrypt, JWT генерация\n' - '│ │ ├── chat.go # Бизнес-логика чатов\n' - '│ │ ├── message.go # Контекст + вызов оркестратора\n' - '│ │ └── orchestrator.go # HTTP-клиент к Python\n' - '│ └── router/router.go # chi router, маршруты\n' - '├── ent/schema/\n' - '│ ├── common.go # Миксины (PkMixin, RegisteredMixin)\n' - '│ ├── user.go # Модель User\n' - '│ ├── chat.go # Модель Chat\n' - '│ └── message.go # Модель Message\n' - '├── python/\n' - '│ ├── pyproject.toml\n' - '│ ├── app/\n' - '│ │ ├── main.py # FastAPI приложение\n' - '│ │ ├── config.py # Настройки\n' - '│ │ ├── routers/completions.py # Эндпоинт чат-комплишенов\n' - '│ │ └── services/\n' - '│ │ ├── router_llm.py # Классификатор запросов\n' - '│ │ ├── groq_client.py # Клиент Groq API\n' - '│ │ └── vllm_client.py # Клиент vLLM (заглушка)\n' - '│ └── tests/\n' - '├── atlas/migrations/ # SQL-миграции\n' - '├── Makefile\n' - '└── go.mod / go.sum' -) - -# === 4. МОДЕЛЬ ДАННЫХ === -add_heading('4. Модель данных (PostgreSQL)') - -add_para('Таблица USERS:', bold=True) -add_table( - ['Поле', 'Тип', 'Ограничения'], - [ - ['id', 'UUID', 'PRIMARY KEY, DEFAULT uuid_generate_v4()'], - ['login', 'VARCHAR(128)', 'UNIQUE, NOT NULL'], - ['password_hash', 'VARCHAR(256)', 'NOT NULL (bcrypt)'], - ['role', 'VARCHAR(64)', "DEFAULT 'user'"], - ['is_active', 'BOOLEAN', 'DEFAULT true'], - ['created_at', 'TIMESTAMP', 'IMMUTABLE, DEFAULT now()'], - ] -) - -add_para('Таблица CHATS:', bold=True) -add_table( - ['Поле', 'Тип', 'Ограничения'], - [ - ['id', 'UUID', 'PRIMARY KEY'], - ['title', 'VARCHAR(256)', 'OPTIONAL'], - ['active_model', 'VARCHAR(128)', 'OPTIONAL'], - ['created_at', 'TIMESTAMP', 'IMMUTABLE, DEFAULT now()'], - ['user_chats', 'UUID', 'FK → users.id, NOT NULL'], - ] -) - -add_para('Таблица MESSAGES:', bold=True) -add_table( - ['Поле', 'Тип', 'Ограничения'], - [ - ['id', 'UUID', 'PRIMARY KEY'], - ['role', 'ENUM', 'user | assistant | system'], - ['content', 'TEXT', 'NOT NULL'], - ['model_used', 'VARCHAR(128)', 'OPTIONAL'], - ['token_count', 'INTEGER', 'OPTIONAL'], - ['created_at', 'TIMESTAMP', 'IMMUTABLE, DEFAULT now()'], - ['chat_messages', 'UUID', 'FK → chats.id, NOT NULL'], - ] -) - -add_para('Правило изоляции: каждый чат изолирован. История одного чата не передаётся в другой. Новый чат = чистый контекст.') - -# === 5. API ЭНДПОИНТЫ === -add_heading('5. API эндпоинты') - -add_para('Публичные (без авторизации):', bold=True) -add_table( - ['Метод', 'URL', 'Описание'], - [ - ['GET', '/health', 'Проверка работоспособности'], - ['POST', '/api/auth/register', 'Регистрация {login, password} → {token}'], - ['POST', '/api/auth/login', 'Вход {login, password} → {token}'], - ] -) - -add_para('Защищённые (Bearer JWT):', bold=True) -add_table( - ['Метод', 'URL', 'Описание'], - [ - ['POST', '/api/chats', 'Создать чат {title?, active_model?}'], - ['GET', '/api/chats', 'Список чатов пользователя'], - ['GET', '/api/chats/{id}', 'Получить чат'], - ['DELETE', '/api/chats/{id}', 'Удалить чат (каскадно с сообщениями)'], - ['POST', '/api/chats/{id}/messages', 'Отправить сообщение → получить ответ LLM'], - ['GET', '/api/chats/{id}/messages', 'История сообщений (пагинация)'], - ] -) - -add_para('Python Orchestrator (внутренний):', bold=True) -add_table( - ['Метод', 'URL', 'Описание'], - [ - ['POST', '/api/v1/chat/completions', 'Получить ответ от LLM'], - ] -) - -# === 6. ПОТОК ОБРАБОТКИ === -add_heading('6. Поток обработки сообщения') -add_para('1. Пользователь отправляет POST /api/chats/{id}/messages с содержимым сообщения.') -add_para('2. Go Backend:') -add_bullet('JWT middleware — проверка токена, извлечение user_id') -add_bullet('Проверка: чат принадлежит пользователю') -add_bullet('Сохранить user-сообщение в БД') -add_bullet('Собрать контекст: последние N сообщений из чата (sliding window)') -add_bullet('HTTP POST → Python Orchestrator') -add_para('3. Python Orchestrator:') -add_bullet('Router LLM классифицирует запрос (code / document / general)') -add_bullet('Выбирает целевую модель') -add_bullet('Отправляет контекст + запрос в модель (через Groq API)') -add_bullet('Возвращает: {content, model_used, tokens}') -add_para('4. Go Backend:') -add_bullet('Сохранить assistant-сообщение в БД (с model_used и token_count)') -add_bullet('Вернуть ответ пользователю') - -# === 7. МАРШРУТИЗАЦИЯ === -add_heading('7. Маршрутизация запросов (Router LLM)') -add_para('Router LLM — маленькая быстрая модель (llama-3.1-8b-instant), которая анализирует запрос и определяет категорию:') -add_table( - ['Категория', 'Целевая модель (Groq)', 'Когда'], - [ - ['code', 'qwen-2.5-coder-32b', 'Программирование, код, отладка'], - ['document', 'deepseek-r1-distill-llama-70b', 'Анализ документов, резюме, тексты'], - ['general', 'llama-3.3-70b-versatile', 'Всё остальное'], - ] -) -add_para('Если пользователь явно выбрал модель в чате (active_model), маршрутизация пропускается.') - -# === 8. КОНТЕКСТ === -add_heading('8. Управление контекстом') -add_bullet('Sliding Window: передаются последние N сообщений (по умолчанию 20)', '') -add_bullet('Лимит токенов: максимум MAX_TOKENS (по умолчанию 4096)', '') -add_bullet('Приблизительный подсчёт: len(content) / 4 ≈ 1 токен', '') -add_bullet('Новый чат: пользователь создаёт новый чат для сброса контекста', '') - -# === 9. АУТЕНТИФИКАЦИЯ === -add_heading('9. Аутентификация') -add_bullet('Пароли хешируются bcrypt (golang.org/x/crypto)') -add_bullet('JWT токен (HS256): sub (user_id), role, exp, iat') -add_bullet('Срок действия: 24 часа (настраивается через JWT_EXPIRE_HOURS)') -add_bullet('Заголовок: Authorization: Bearer ') - -# === 10. СТЕК === -add_heading('10. Технологический стек') - -add_para('Go Backend:', bold=True) -add_table( - ['Компонент', 'Технология'], - [ - ['HTTP-фреймворк', 'chi/v5'], - ['ORM', 'Ent (entgo.io)'], - ['Миграции', 'Atlas'], - ['JWT', 'golang-jwt/jwt/v5'], - ['Хеширование', 'golang.org/x/crypto (bcrypt)'], - ['БД-драйвер', 'pgx/v5'], - ['Конфигурация', 'caarlos0/env/v11'], - ] -) - -add_para('Python Orchestrator:', bold=True) -add_table( - ['Компонент', 'Технология'], - [ - ['Web-фреймворк', 'FastAPI'], - ['ASGI-сервер', 'Uvicorn'], - ['LLM API', 'Groq SDK'], - ['HTTP-клиент', 'httpx'], - ['Конфигурация', 'pydantic-settings'], - ] -) - -# === 11. ENV === -add_heading('11. Переменные окружения') - -add_para('Go Backend:', bold=True) -add_table( - ['Переменная', 'Описание', 'По умолчанию'], - [ - ['DB_URL', 'PostgreSQL connection string', '— (обязательна)'], - ['JWT_SECRET', 'Секрет для подписи JWT', '— (обязательна)'], - ['PORT', 'Порт сервера', '8080'], - ['JWT_EXPIRE_HOURS', 'Время жизни токена (часы)', '24'], - ['ORCHESTRATOR_URL', 'URL Python-оркестратора', 'http://localhost:8000'], - ['CONTEXT_WINDOW', 'Кол-во сообщений в контексте', '20'], - ['MAX_TOKENS', 'Максимум токенов контекста', '4096'], - ] -) - -add_para('Python Orchestrator:', bold=True) -add_table( - ['Переменная', 'Описание', 'По умолчанию'], - [ - ['GROQ_API_KEY', 'Ключ Groq API', '— (обязательна)'], - ['ROUTER_MODEL', 'Модель-классификатор', 'llama-3.1-8b-instant'], - ['PORT', 'Порт оркестратора', '8000'], - ] -) - -# === 12. JSON КОНТРАКТ === -add_heading('12. JSON-контракт Go ↔ Python') - -add_para('Запрос (Go → Python):', bold=True) -add_code( - '{\n' - ' "messages": [\n' - ' {"role": "user", "content": "Напиши функцию..."},\n' - ' {"role": "assistant", "content": "Вот функция..."},\n' - ' {"role": "user", "content": "А теперь добавь тесты"}\n' - ' ],\n' - ' "active_model": null\n' - '}' -) - -add_para('Ответ (Python → Go):', bold=True) -add_code( - '{\n' - ' "content": "Вот тесты для функции...",\n' - ' "model_used": "qwen-2.5-coder-32b",\n' - ' "tokens": 523\n' - '}' -) - -# === 13. ЭТАПЫ === -add_heading('13. Этапы разработки') - -add_para('Спринт 1: Go-скелет ✓', bold=True) -add_bullet('go.mod, зависимости, Ent-схемы (User, Chat, Message), main.go + /health') - -add_para('Спринт 2: Аутентификация', bold=True) -add_bullet('Сервис регистрации/логина (bcrypt + JWT), middleware, хендлеры') - -add_para('Спринт 3: CRUD чатов', bold=True) -add_bullet('Сервис и REST-хендлеры создания/получения/удаления чатов') - -add_para('Спринт 4: Python-оркестратор', bold=True) -add_bullet('FastAPI, Router LLM, Groq API клиент, эндпоинт /api/v1/chat/completions') - -add_para('Спринт 5: Сквозной поток сообщений', bold=True) -add_bullet('Go HTTP-клиент к Python, sliding window, хендлеры сообщений, полный цикл') - -# === 14. PRODUCTION === -add_heading('14. Production-сценарий (будущее)') -add_para('После проверки маршрутизации через Groq API система переводится на локальные модели:') -add_bullet('Каждая модель запускается через vLLM на отдельной GPU') -add_bullet('Каждая модель — отдельный сервис (отдельный порт)') -add_bullet('Python Orchestrator переключается с Groq API на локальные vLLM-эндпоинты') -add_bullet('Внешние API не используются') - -# Save -output_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'architecture.docx') -doc.save(output_path) -print(f"Saved to {output_path}")