diff --git a/CHANGELOG.txt b/CHANGELOG.txt index f997f37..4a1dd39 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,36 @@ CHANGELOG ========= +Версия 2.0.6 (2025-10-28) +========================= + +## ✨ НОВОЕ И ИЗМЕНЕНО + +- 🌐 Поддержка BasePath для всех веб‑роутов и шаблонов + - Новый параметр: `LCG_BASE_URL` (пример: `/lcg`) — префикс для всех страниц и API + - Обновлены редиректы и middleware с учетом BasePath +- 🧭 Кастомная страница 404 (красная тема), показывается для любого неизвестного пути под BasePath +- 📱 Улучшена мобильная верстка результатов — стиль карточек как в истории +- 🗂️ Человекочитаемые заголовки результатов: преобразование имени файла в «заголовок — дата время» +- 🗑️ Иконки удаления: единый бледно‑красный крест ✖ в результатах и истории + +## 🐛 ИСПРАВЛЕНИЯ + +- 🛡️ Исправлен просмотр/удаление файла при включенном BasePath (правильный разбор URL) +- 🧰 На старте сервера гарантируется создание `ResultFolder` и пустого `ResultHistory` (без 500) +- 🚧 Главная страница обрабатывается только по точному пути BasePath, а не по произвольным под‑путям + +## ⚙️ КОНФИГУРАЦИЯ + +- 🔍 Debug режим теперь включается и флагом `--debug`, и переменной `LCG_DEBUG=1|true` +- 🍪 Уточнена работа с `CookiePath`/`BasePath` в middleware + +## 📚 ДОКУМЕНТАЦИЯ + +- Обновлены `README.md`, `USAGE_GUIDE.md`, `API_GUIDE.md`, `REVERSE_PROXY_GUIDE.md` — добавлены примеры с BasePath и примечания к 404 + +--- + Версия 2.0.1 (2025-10-22) ========================= diff --git a/Dockerfiles/OllamaServer/.dockerignore b/Dockerfiles/OllamaServer/.dockerignore new file mode 100644 index 0000000..c4e1bb4 --- /dev/null +++ b/Dockerfiles/OllamaServer/.dockerignore @@ -0,0 +1,52 @@ +# Git +.git +.gitignore +.gitattributes + +# Build artifacts +dist/ +*.exe +*.dll +*.so +*.dylib +lcg +go-lcg + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# Documentation +docs/ +*.md +!README.md + +# Tests +*_test.go +test_*.sh + +# Deployment scripts +deploy/ +shell-code/ +kustomize/ + +# Temporary files +*.log +*.tmp +*.temp + +# OS files +.DS_Store +Thumbs.db + +# Go +vendor/ + +# Docker +Dockerfile* +docker-compose*.yml +.dockerignore + diff --git a/Dockerfiles/OllamaServer/.gitignore b/Dockerfiles/OllamaServer/.gitignore new file mode 100644 index 0000000..77d13f0 --- /dev/null +++ b/Dockerfiles/OllamaServer/.gitignore @@ -0,0 +1,15 @@ +# Временные файлы +*.log +*.tmp +*.temp + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS files +.DS_Store +Thumbs.db + diff --git a/Dockerfiles/OllamaServer/Dockerfile b/Dockerfiles/OllamaServer/Dockerfile new file mode 100644 index 0000000..34c7949 --- /dev/null +++ b/Dockerfiles/OllamaServer/Dockerfile @@ -0,0 +1,83 @@ +# Используем готовый образ Ollama +FROM localhost/ollama_packed:latest + +# Устанавливаем bash если его нет (базовый образ ollama может быть на разных дистрибутивах) +RUN if ! command -v bash >/dev/null 2>&1; then \ + if command -v apk >/dev/null 2>&1; then \ + apk add --no-cache bash; \ + elif command -v apt-get >/dev/null 2>&1; then \ + apt-get update && apt-get install -y --no-install-recommends bash && rm -rf /var/lib/apt/lists/*; \ + fi; \ + fi + +# Определяем архитектуру для копирования правильного бинарника +ARG TARGETARCH +ARG TARGETOS=linux + +# Копируем папку dist с бинарниками +# Структура: dist/lcg_linux_amd64_v1/lcg_* или dist/lcg_linux_arm64_v8.0/lcg_* +COPY dist/ /tmp/dist/ + +# Выбираем правильный бинарник в зависимости от архитектуры +# Если TARGETARCH не установлен, определяем архитектуру через uname +RUN ARCH="${TARGETARCH:-$(uname -m)}" && \ + case "${ARCH}" in \ + amd64|x86_64) \ + BIN_FILE=$(find /tmp/dist/lcg_linux_amd64_v* -name "lcg_*" -type f 2>/dev/null | head -1) && \ + if [ -n "$BIN_FILE" ]; then \ + cp "$BIN_FILE" /usr/local/bin/lcg && \ + echo "Установлен бинарник для amd64: $BIN_FILE"; \ + else \ + echo "Бинарник для amd64 не найден в /tmp/dist/" && exit 1; \ + fi ;; \ + arm64|aarch64|arm) \ + BIN_FILE=$(find /tmp/dist/lcg_linux_arm64_v* -name "lcg_*" -type f 2>/dev/null | head -1) && \ + if [ -n "$BIN_FILE" ]; then \ + cp "$BIN_FILE" /usr/local/bin/lcg && \ + echo "Установлен бинарник для arm64: $BIN_FILE"; \ + else \ + echo "Бинарник для arm64 не найден в /tmp/dist/" && exit 1; \ + fi ;; \ + *) \ + echo "Unsupported architecture: ${ARCH}" && \ + echo "Доступные бинарники:" && \ + find /tmp/dist -name "lcg_*" -type f 2>/dev/null && \ + exit 1 ;; \ + esac && \ + chmod +x /usr/local/bin/lcg && \ + rm -rf /tmp/dist && \ + (lcg --version || echo "Бинарник lcg установлен") + +# Копируем entrypoint скрипт +COPY --chmod=755 Dockerfiles/OllamaServer/entrypoint.sh /entrypoint.sh + +# Создаем директории для данных LCG +# В базовом образе ollama уже есть пользователь ollama +RUN mkdir -p /app/data/results /app/data/prompts /app/data/config + +# Устанавливаем права доступа (пользователь ollama должен существовать в базовом образе) +RUN chown -R ollama:ollama /app/data 2>/dev/null || \ + (chown -R 1000:1000 /app/data 2>/dev/null || true) + +# Настройки по умолчанию +ENV LCG_PROVIDER=ollama +ENV LCG_HOST=http://127.0.0.1:11434/ +ENV LCG_MODEL=qwen2.5-coder:1.5b +ENV LCG_RESULT_FOLDER=/app/data/results +ENV LCG_PROMPT_FOLDER=/app/data/prompts +ENV LCG_CONFIG_FOLDER=/app/data/config +ENV LCG_SERVER_HOST=0.0.0.0 +ENV LCG_SERVER_PORT=8080 +# ENV LCG_SERVER_ALLOW_HTTP=true +# ENV OLLAMA_HOST=127.0.0.1 +# ENV OLLAMA_PORT=11434 + +# Expose порты +EXPOSE 8080 + +WORKDIR /home/ollama + +# Запускаем entrypoint +ENTRYPOINT ["/entrypoint.sh"] +CMD [] + diff --git a/Dockerfiles/OllamaServer/Makefile b/Dockerfiles/OllamaServer/Makefile new file mode 100644 index 0000000..b1f9d86 --- /dev/null +++ b/Dockerfiles/OllamaServer/Makefile @@ -0,0 +1,120 @@ +.PHONY: build build-podman run run-podman stop stop-podman logs logs-podman clean help + +# Переменные +IMAGE_NAME = lcg-ollama +IMAGE_TAG = latest +CONTAINER_NAME = lcg-ollama +DOCKERFILE = Dockerfile +CONTEXT = ../.. + +help: ## Показать справку + @echo "Доступные команды:" + @echo " make build - Собрать Docker образ" + @echo " make build-podman - Собрать Podman образ" + @echo " make run - Запустить контейнер (Docker)" + @echo " make run-podman - Запустить контейнер (Podman)" + @echo " make stop - Остановить контейнер (Docker)" + @echo " make stop-podman - Остановить контейнер (Podman)" + @echo " make logs - Показать логи (Docker)" + @echo " make logs-podman - Показать логи (Podman)" + @echo " make clean - Удалить контейнер и образ" + @echo " make compose-up - Запустить через docker-compose" + @echo " make compose-down - Остановить docker-compose" + @echo " make podman-compose-up - Запустить через podman-compose" + @echo " make podman-compose-down - Остановить podman-compose" + +build: ## Собрать Docker образ (требует собранных бинарников в dist/) + @echo "⚠️ Убедитесь, что бинарники собраны: goreleaser build --snapshot --clean" + docker build -f $(DOCKERFILE) -t $(IMAGE_NAME):$(IMAGE_TAG) $(CONTEXT) + @echo "Образ $(IMAGE_NAME):$(IMAGE_TAG) успешно собран" + +build-podman: ## Собрать Podman образ (требует собранных бинарников в dist/) + @echo "⚠️ Убедитесь, что бинарники собраны: goreleaser build --snapshot --clean" + podman build -f $(DOCKERFILE) -t $(IMAGE_NAME):$(IMAGE_TAG) $(CONTEXT) + @echo "Образ $(IMAGE_NAME):$(IMAGE_TAG) успешно собран" + +build-binaries: ## Собрать бинарники перед сборкой образа + @echo "Сборка бинарников..." + cd $(CONTEXT) && goreleaser build --snapshot --clean + @echo "Бинарники собраны в $(CONTEXT)/dist/" + +build-all: build-binaries build ## Собрать бинарники и Docker образ + @echo "✅ Все готово!" + +build-all-podman: build-binaries build-podman ## Собрать бинарники и Podman образ + @echo "✅ Все готово!" + +run: ## Запустить контейнер (Docker) + docker run -d \ + --name $(CONTAINER_NAME) \ + -p 8080:8080 \ + -p 11434:11434 \ + -v ollama-data:/home/ollama/.ollama \ + -v lcg-results:/app/data/results \ + -v lcg-prompts:/app/data/prompts \ + -v lcg-config:/app/data/config \ + $(IMAGE_NAME):$(IMAGE_TAG) + @echo "Контейнер $(CONTAINER_NAME) запущен" + +run-podman: ## Запустить контейнер (Podman) + podman run -d \ + --name $(CONTAINER_NAME) \ + -p 8080:8080 \ + -p 11434:11434 \ + -v ollama-data:/home/ollama/.ollama \ + -v lcg-results:/app/data/results \ + -v lcg-prompts:/app/data/prompts \ + -v lcg-config:/app/data/config \ + $(IMAGE_NAME):$(IMAGE_TAG) + @echo "Контейнер $(CONTAINER_NAME) запущен" + +stop: ## Остановить контейнер (Docker) + docker stop $(CONTAINER_NAME) || true + docker rm $(CONTAINER_NAME) || true + @echo "Контейнер $(CONTAINER_NAME) остановлен и удален" + +stop-podman: ## Остановить контейнер (Podman) + podman stop $(CONTAINER_NAME) || true + podman rm $(CONTAINER_NAME) || true + @echo "Контейнер $(CONTAINER_NAME) остановлен и удален" + +logs: ## Показать логи (Docker) + docker logs -f $(CONTAINER_NAME) + +logs-podman: ## Показать логи (Podman) + podman logs -f $(CONTAINER_NAME) + +clean: ## Удалить контейнер и образ + docker stop $(CONTAINER_NAME) || true + docker rm $(CONTAINER_NAME) || true + docker rmi $(IMAGE_NAME):$(IMAGE_TAG) || true + @echo "Контейнер и образ удалены" + +compose-up: ## Запустить через docker-compose + docker-compose up -d + @echo "Сервисы запущены через docker-compose" + +compose-down: ## Остановить docker-compose + docker-compose down + @echo "Сервисы остановлены" + +podman-compose-up: ## Запустить через podman-compose + podman-compose -f podman-compose.yml up -d + @echo "Сервисы запущены через podman-compose" + +podman-compose-down: ## Остановить podman-compose + podman-compose -f podman-compose.yml down + @echo "Сервисы остановлены" + +shell: ## Подключиться к контейнеру (Docker) + docker exec -it $(CONTAINER_NAME) sh + +shell-podman: ## Подключиться к контейнеру (Podman) + podman exec -it $(CONTAINER_NAME) sh + +pull-model: ## Загрузить модель codegeex4 (Docker) + docker exec $(CONTAINER_NAME) ollama pull codegeex4 + +pull-model-podman: ## Загрузить модель codegeex4 (Podman) + podman exec $(CONTAINER_NAME) ollama pull codegeex4 + diff --git a/Dockerfiles/OllamaServer/QUICKSTART.md b/Dockerfiles/OllamaServer/QUICKSTART.md new file mode 100644 index 0000000..fa1e19f --- /dev/null +++ b/Dockerfiles/OllamaServer/QUICKSTART.md @@ -0,0 +1,185 @@ +# 🚀 Быстрый старт - LCG с Ollama + +## Подготовка + +1. Убедитесь, что у вас установлен Docker или Podman +2. Клонируйте репозиторий (если еще не сделали) +3. Соберите бинарники (требуется перед сборкой образа) + +```bash +# Из корня проекта +goreleaser build --snapshot --clean + +# Или используйте скрипт +./deploy/4.build-binaries.sh v2.0.15 +``` + +4. Перейдите в папку с Dockerfile + +```bash +cd Dockerfiles/OllamaServer +``` + +## Запуск с Docker + +### Вариант 1: Docker Compose (рекомендуется) + +```bash +# Важно: убедитесь, что бинарники собраны в ../../dist/ +docker-compose up -d +``` + +### Вариант 2: Ручная сборка и запуск + +```bash +# Сборка образа (контекст должен быть корень проекта) +cd ../.. # Переходим в корень проекта +docker build -f Dockerfiles/OllamaServer/Dockerfile -t lcg-ollama:latest . + +# Запуск контейнера +docker run -d \ + --name lcg-ollama \ + -p 8080:8080 \ + -p 11434:11434 \ + -v ollama-data:/home/ollama/.ollama \ + -v lcg-results:/app/data/results \ + lcg-ollama:latest +``` + +## Запуск с Podman + +### Вариант 1: Podman Compose + +```bash +podman-compose -f podman-compose.yml up -d +``` + +### Вариант 2: Ручная сборка и запуск + +```bash +# Сборка образа (контекст должен быть корень проекта) +cd ../.. # Переходим в корень проекта +podman build -f Dockerfiles/OllamaServer/Dockerfile -t lcg-ollama:latest . + +# Запуск контейнера +podman run -d \ + --name lcg-ollama \ + -p 8080:8080 \ + -p 11434:11434 \ + -v ollama-data:/home/ollama/.ollama \ + -v lcg-results:/app/data/results \ + lcg-ollama:latest +``` + +## Проверка запуска + +### Проверка логов + +```bash +# Docker +docker logs -f lcg-ollama + +# Podman +podman logs -f lcg-ollama +``` + +Дождитесь сообщений: +- `Ollama сервер готов!` +- `LCG сервер запущен на http://0.0.0.0:8080` + +### Проверка доступности + +```bash +# Проверка Ollama +curl http://localhost:11434/api/tags + +# Проверка LCG +curl http://localhost:8080/ +``` + +## Загрузка модели + +После запуска контейнера загрузите модель: + +```bash +# Docker +docker exec lcg-ollama ollama pull codegeex4 + +# Podman +podman exec lcg-ollama ollama pull codegeex4 +``` + +Или используйте модель по умолчанию, указанную в переменных окружения. + +## Доступ к веб-интерфейсу + +Откройте в браузере: http://localhost:8080 + +## Остановка + +```bash +# Docker +docker-compose down + +# Podman +podman-compose -f podman-compose.yml down +``` + +Или для ручного запуска: + +```bash +# Docker +docker stop lcg-ollama +docker rm lcg-ollama + +# Podman +podman stop lcg-ollama +podman rm lcg-ollama +``` + +## Решение проблем + +### Порт занят + +Измените порты в docker-compose.yml или команде run: + +```bash +-p 9000:8080 # LCG на порту 9000 +-p 11435:11434 # Ollama на порту 11435 +``` + +### Контейнер не запускается + +Проверьте логи: + +```bash +docker logs lcg-ollama +# или +podman logs lcg-ollama +``` + +### Модель не загружена + +Убедитесь, что модель существует: + +```bash +docker exec lcg-ollama ollama list +# или +podman exec lcg-ollama ollama list +``` + +Если модели нет, загрузите её: + +```bash +docker exec lcg-ollama ollama pull codegeex4 +# или +podman exec lcg-ollama ollama pull codegeex4 +``` + +## Следующие шаги + +- Прочитайте полную документацию в [README.md](README.md) +- Настройте аутентификацию для продакшена +- Настройте reverse proxy для HTTPS +- Загрузите нужные модели Ollama + diff --git a/Dockerfiles/OllamaServer/README.md b/Dockerfiles/OllamaServer/README.md new file mode 100644 index 0000000..a8f0dd1 --- /dev/null +++ b/Dockerfiles/OllamaServer/README.md @@ -0,0 +1,454 @@ +# 🐳 LCG с Ollama Server - Docker/Podman контейнер + +Этот образ содержит Linux Command GPT (LCG) и Ollama сервер, работающие вместе в одном контейнере. + +Поддерживается запуск через Docker и Podman. + +## 📋 Описание + +Контейнер автоматически запускает: + +1 **Ollama сервер** (v0.9.5) на порту 11434 + +2 **LCG веб-сервер** на порту 8080 + +Ollama используется как провайдер LLM для генерации Linux команд. + +## 🚀 Быстрый старт + +### Предварительные требования + +Перед сборкой Docker образа необходимо собрать бинарники: + +```bash +# Из корня проекта +# Используйте goreleaser для сборки бинарников +goreleaser build --snapshot --clean + +# Или используйте скрипт сборки +./deploy/4.build-binaries.sh v2.0.15 +``` + +Убедитесь, что в папке `dist/` есть бинарники: + +- `dist/lcg_linux_amd64_v1/lcg_*` для amd64 +- `dist/lcg_linux_arm64_v8.0/lcg_*` для arm64 + +### Сборка образа + +#### Docker + +```bash +# Из корня проекта (важно: контекст должен быть корень проекта) +docker build -f Dockerfiles/OllamaServer/Dockerfile -t lcg-ollama:latest . + +# Или с указанием архитектуры +docker buildx build \ + --platform linux/amd64,linux/arm64 \ + -f Dockerfiles/OllamaServer/Dockerfile \ + -t lcg-ollama:latest . +``` + +#### Podman + +```bash +# Из корня проекта +podman build -f Dockerfiles/OllamaServer/Dockerfile -t lcg-ollama:latest . + +# Или с указанием архитектуры +podman build \ + --platform linux/amd64,linux/arm64 \ + -f Dockerfiles/OllamaServer/Dockerfile \ + -t lcg-ollama:latest . +``` + +### Запуск контейнера + +#### Docker run + +```bash +docker run -d \ + --name lcg-ollama \ + -p 8080:8080 \ + lcg-ollama:latest + ollama serve +``` + +#### Podman run + +```bash +podman run -d \ + --name lcg-ollama \ + -p 8989:8080 \ + --restart always \ + lcg-ollama:latest \ + ollama serve +``` + +когда контейнер запущен на удаленном хосте - можете воспользоваться консольными возможностями утилиты lcg следующим образом + +``` bash +ssh user@[host_where_contaier_running] 'podman exec -it $(podman ps -q --filter \"ancestor=localhost/lcg-ollama:latest\") lcg [your query] +``` + +``` bash +ssh user@[host_where_contaier_running] 'podman exec -it $(podman ps -q --filter "ancestor=localhost/lcg-ollama:latest") /bin/sh -c "export LCG_MODEL=qwen3:0.6b && lcg config --full"' + +### Использование docker-compose / podman-compose + +#### Docker Compose + +```bash +cd Dockerfiles/OllamaServer +docker-compose up -d +``` + +#### Podman Compose + +```bash +cd Dockerfiles/OllamaServer +podman-compose -f podman-compose.yml up -d +``` + +Или используйте встроенную поддержку Podman: + +```bash +cd Dockerfiles/OllamaServer +podman play kube podman-compose.yml +``` + +## 🌐 Доступ к сервисам + +После запуска контейнера доступны: + +- **LCG веб-интерфейс**: +- **Ollama API**: + +## ⚙️ Переменные окружения + +### Настройки LCG + +| Переменная | Значение по умолчанию | Описание | +|------------|----------------------|----------| +| `LCG_PROVIDER` | `ollama` | Тип провайдера | +| `LCG_HOST` | `http://127.0.0.1:11434/` | URL Ollama API | +| `LCG_MODEL` | `codegeex4` | Модель для использования | +| `LCG_SERVER_HOST` | `0.0.0.0` | Хост LCG сервера | +| `LCG_SERVER_PORT` | `8080` | Порт LCG сервера | +| `LCG_SERVER_ALLOW_HTTP` | `true` | Разрешить HTTP | +| `LCG_RESULT_FOLDER` | `/app/data/results` | Папка для результатов | +| `LCG_PROMPT_FOLDER` | `/app/data/prompts` | Папка для промптов | +| `LCG_CONFIG_FOLDER` | `/app/data/config` | Папка для конфигурации | + +### Настройки Ollama + +| Переменная | Значение по умолчанию | Описание | +|------------|----------------------|----------| +| `OLLAMA_HOST` | `127.0.0.1` | Хост Ollama сервера | +| `OLLAMA_PORT` | `11434` | Порт Ollama сервера | + +### Безопасность + +| Переменная | Значение по умолчанию | Описание | +|------------|----------------------|----------| +| `LCG_SERVER_REQUIRE_AUTH` | `false` | Требовать аутентификацию | +| `LCG_SERVER_PASSWORD` | `admin#123456` | Пароль для аутентификации | + +## 📦 Volumes + +Рекомендуется монтировать volumes для персистентного хранения данных: + +```bash +docker run -d \ + --name lcg-ollama \ + -p 8080:8080 \ + -p 11434:11434 \ + -v ollama-data:/home/ollama/.ollama \ + -v lcg-results:/app/data/results \ + -v lcg-prompts:/app/data/prompts \ + -v lcg-config:/app/data/config \ + lcg-ollama:latest +``` + +### Volumes описание + +- `ollama-data`: Модели и данные Ollama +- `lcg-results`: Результаты генерации команд +- `lcg-prompts`: Системные промпты +- `lcg-config`: Конфигурация LCG + +## 🔧 Примеры использования + +### Запуск с кастомной моделью + +```bash +docker run -d \ + --name lcg-ollama \ + -p 8080:8080 \ + -p 11434:11434 \ + -e LCG_MODEL=llama3:8b \ + lcg-ollama:latest +``` + +### Запуск с аутентификацией + +```bash +docker run -d \ + --name lcg-ollama \ + -p 8080:8080 \ + -p 11434:11434 \ + -e LCG_SERVER_REQUIRE_AUTH=true \ + -e LCG_SERVER_PASSWORD=my_secure_password \ + lcg-ollama:latest +``` + +### Запуск с кастомным портом + +```bash +docker run -d \ + --name lcg-ollama \ + -p 9000:9000 \ + -p 11434:11434 \ + -e LCG_SERVER_PORT=9000 \ + lcg-ollama:latest +``` + +## 📥 Загрузка моделей Ollama + +После запуска контейнера можно загрузить модели: + +```bash +# Подключиться к контейнеру +docker exec -it lcg-ollama sh + +# Загрузить модель +ollama pull codegeex4 +ollama pull llama3:8b +ollama pull qwen2.5:7b +``` + +Или извне контейнера: + +```bash +# Убедитесь, что Ollama доступен извне (OLLAMA_HOST=0.0.0.0) +docker exec lcg-ollama ollama pull codegeex4 +``` + +## 🔍 Проверка работоспособности + +### Проверка Ollama + +```bash +# Проверка health +curl http://localhost:11434/api/tags + +# Список моделей +curl http://localhost:11434/api/tags | jq '.models' +``` + +### Проверка LCG + +```bash +# Проверка веб-интерфейса +curl http://localhost:8080/ + +# Проверка через API +curl -X POST http://localhost:8080/api/execute \ + -H "Content-Type: application/json" \ + -d '{"prompt": "создать директорию test"}' +``` + +## 🐧 Podman специфичные инструкции + +### Запуск с Podman + +Podman работает аналогично Docker, но есть несколько отличий: + +#### Создание сетей (если нужно) + +```bash +podman network create lcg-network +``` + +#### Запуск с сетью + +```bash +podman run -d \ + --name lcg-ollama \ + --network lcg-network \ + -p 8080:8080 \ + -p 11434:11434 \ + lcg-ollama:latest +``` + +#### Запуск в rootless режиме + +Podman по умолчанию работает в rootless режиме, что повышает безопасность: + +```bash +# Не требует sudo +podman run -d \ + --name lcg-ollama \ + -p 8080:8080 \ + -p 11434:11434 \ + lcg-ollama:latest +``` + +#### Использование systemd для автозапуска + +Создайте systemd unit файл: + +```bash +# Генерируем unit файл +podman generate systemd --name lcg-ollama --files + +# Копируем в systemd +sudo cp container-lcg-ollama.service /etc/systemd/system/ + +# Включаем автозапуск +sudo systemctl enable container-lcg-ollama.service +sudo systemctl start container-lcg-ollama.service +``` + +#### Проверка статуса + +```bash +# Статус контейнера +podman ps + +# Логи +podman logs lcg-ollama + +# Логи в реальном времени +podman logs -f lcg-ollama +``` + +## 🐛 Отладка + +### Просмотр логов + +#### Docker log + +```bash +# Логи контейнера +docker logs lcg-ollama + +# Логи в реальном времени +docker logs -f lcg-ollama +``` + +#### Podman log + +```bash +# Логи контейнера +podman logs lcg-ollama + +# Логи в реальном времени +podman logs -f lcg-ollama +``` + +### Подключение к контейнеру + +#### Docker exec + +```bash +docker exec -it lcg-ollama sh +``` + +#### Podman exec + +```bash +podman exec -it lcg-ollama sh +``` + +### Проверка процессов + +#### Docker check ps + +```bash +docker exec lcg-ollama ps aux +``` + +#### Podman check ps + +```bash +podman exec lcg-ollama ps aux +``` + +## 🔒 Безопасность + +### Рекомендации для продакшена + +1. **Используйте аутентификацию**: + + ```bash + -e LCG_SERVER_REQUIRE_AUTH=true + -e LCG_SERVER_PASSWORD=strong_password + ``` + +2. **Ограничьте доступ к портам**: + - Используйте firewall правила + - Не экспортируйте порты на публичный интерфейс + +3. **Используйте HTTPS**: + - Настройте reverse proxy (nginx, traefik) + - Используйте SSL сертификаты + +4. **Ограничьте ресурсы**: + + ```bash + docker run -d \ + --name lcg-ollama \ + --memory="4g" \ + --cpus="2" \ + lcg-ollama:latest + ``` + +## 📊 Мониторинг + +### Healthcheck + +Контейнер включает healthcheck, который проверяет доступность LCG сервера: + +```bash +docker inspect lcg-ollama | jq '.[0].State.Health' +``` + +### Метрики + +LCG предоставляет Prometheus метрики на `/metrics` endpoint (если включено). + +## 🚀 Production Deployment + +### С docker-compose + +```bash +cd Dockerfiles/OllamaServer +docker-compose up -d +``` + +### С Kubernetes + +Используйте манифесты из папки `deploy/` или `kustomize/`. + +## 📝 Примечания + +- Ollama версия: 0.9.5 +- LCG версия: см. VERSION.txt +- Минимальные требования: 2GB RAM, 2 CPU cores +- Рекомендуется: 4GB+ RAM для больших моделей + +## 🔗 Полезные ссылки + +- [Ollama документация](https://github.com/ollama/ollama) +- [LCG документация](../../docs/README.md) +- [LCG API Guide](../../docs/API_GUIDE.md) + +## ❓ Поддержка + +При возникновении проблем: + +1. Проверьте логи: `docker logs lcg-ollama` +2. Проверьте переменные окружения +3. Убедитесь, что порты не заняты +4. Проверьте, что модели загружены в Ollama diff --git a/Dockerfiles/OllamaServer/STRUCTURE.md b/Dockerfiles/OllamaServer/STRUCTURE.md new file mode 100644 index 0000000..6d95700 --- /dev/null +++ b/Dockerfiles/OllamaServer/STRUCTURE.md @@ -0,0 +1,143 @@ +# 📁 Структура проекта OllamaServer + +## Файлы + +``` +Dockerfiles/OllamaServer/ +├── Dockerfile # Multi-stage Dockerfile для сборки образа +├── entrypoint.sh # Скрипт запуска LCG и Ollama серверов +├── docker-compose.yml # Docker Compose конфигурация +├── podman-compose.yml # Podman Compose конфигурация +├── Makefile # Команды для сборки и запуска +├── README.md # Полная документация +├── QUICKSTART.md # Быстрый старт +├── STRUCTURE.md # Этот файл +├── .dockerignore # Исключения для Docker build +└── .gitignore # Исключения для Git +``` + +## Описание файлов + +### Dockerfile + +Dockerfile, который: + +1. Использует готовый образ `ollama/ollama:0.9.5` как базовый +2. Копирует предварительно собранный бинарник LCG из папки `dist/` +3. Выбирает правильный бинарник в зависимости от архитектуры (amd64/arm64) +4. Устанавливает entrypoint.sh для запуска обоих сервисов +5. Настраивает рабочее окружение и переменные окружения + +**Важно**: Перед сборкой образа необходимо собрать бинарники с помощью `goreleaser build --snapshot --clean` + +### entrypoint.sh + +Скрипт запуска, который: + +1. Запускает Ollama сервер в фоне +2. Ожидает готовности Ollama API +3. Запускает LCG сервер в фоне +4. Мониторит состояние процессов +5. Корректно обрабатывает сигналы завершения + +### docker-compose.yml / podman-compose.yml + +Конфигурация для запуска через compose: + +- Настройки портов +- Переменные окружения +- Volumes для персистентного хранения +- Healthcheck + +### Makefile + +Удобные команды для: + +- Сборки образа +- Запуска/остановки контейнера +- Просмотра логов +- Работы с compose + +### README.md + +Полная документация с: + +- Описанием функциональности +- Инструкциями по установке +- Настройками переменных окружения +- Примерами использования +- Решением проблем + +### QUICKSTART.md + +Краткое руководство для быстрого старта. + +## Порты + +- **8080**: LCG веб-сервер +- **11434**: Ollama API + +## Volumes + +- `ollama-data`: Данные Ollama (модели) +- `lcg-results`: Результаты генерации команд +- `lcg-prompts`: Системные промпты +- `lcg-config`: Конфигурация LCG + +## Переменные окружения + +Основные переменные (см. README.md для полного списка): + +- `LCG_PROVIDER=ollama` +- `LCG_HOST=http://127.0.0.1:11434/` +- `LCG_MODEL=codegeex4` +- `OLLAMA_HOST=0.0.0.0` +- `OLLAMA_PORT=11434` + +## Запуск + +### Предварительная подготовка + +Перед сборкой образа необходимо собрать бинарники: + +```bash +# Из корня проекта +goreleaser build --snapshot --clean +``` + +Убедитесь, что в папке `dist/` есть бинарники для нужных архитектур. + +### Docker + +```bash +cd Dockerfiles/OllamaServer +docker-compose up -d +``` + +### Podman + +```bash +cd Dockerfiles/OllamaServer +podman-compose -f podman-compose.yml up -d +``` + +### Make + +```bash +cd Dockerfiles/OllamaServer +make build-all # Собрать бинарники и Docker образ +make compose-up # Запустить через docker-compose + +# Или для Podman +make build-all-podman +make podman-compose-up +``` + +## Архитектура + +Контейнер запускает два сервиса: + +1. **Ollama** (порт 11434) - LLM сервер +2. **LCG** (порт 8080) - Веб-интерфейс и API + +Оба сервиса работают в одном контейнере и общаются через localhost. diff --git a/Dockerfiles/OllamaServer/docker-compose.yml b/Dockerfiles/OllamaServer/docker-compose.yml new file mode 100644 index 0000000..e50790a --- /dev/null +++ b/Dockerfiles/OllamaServer/docker-compose.yml @@ -0,0 +1,55 @@ +version: '3.8' + +services: + lcg-ollama: + build: + context: ../.. # Контекст сборки - корень проекта (для доступа к dist/) + dockerfile: Dockerfiles/OllamaServer/Dockerfile + # TARGETARCH определяется автоматически Docker на основе платформы хоста + container_name: lcg-ollama + ports: + - "8080:8080" # LCG веб-сервер + - "11434:11434" # Ollama API + environment: + # Настройки LCG + - LCG_PROVIDER=ollama + - LCG_HOST=http://127.0.0.1:11434/ + - LCG_MODEL=codegeex4 + - LCG_RESULT_FOLDER=/app/data/results + - LCG_PROMPT_FOLDER=/app/data/prompts + - LCG_CONFIG_FOLDER=/app/data/config + - LCG_SERVER_HOST=0.0.0.0 + - LCG_SERVER_PORT=8080 + - LCG_SERVER_ALLOW_HTTP=true + # Настройки Ollama + - OLLAMA_HOST=0.0.0.0 + - OLLAMA_PORT=11434 + - OLLAMA_ORIGINS=* + # Опционально: настройки безопасности + - LCG_SERVER_REQUIRE_AUTH=false + - LCG_SERVER_PASSWORD=admin#123456 + volumes: + # Персистентное хранилище для данных Ollama + - ollama-data:/home/ollama/.ollama + # Персистентное хранилище для результатов LCG + - lcg-results:/app/data/results + - lcg-prompts:/app/data/prompts + - lcg-config:/app/data/config + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + +volumes: + ollama-data: + driver: local + lcg-results: + driver: local + lcg-prompts: + driver: local + lcg-config: + driver: local + diff --git a/Dockerfiles/OllamaServer/entrypoint.sh b/Dockerfiles/OllamaServer/entrypoint.sh new file mode 100755 index 0000000..6bd9180 --- /dev/null +++ b/Dockerfiles/OllamaServer/entrypoint.sh @@ -0,0 +1,99 @@ +#!/bin/bash +set -e + +# Цвета для вывода +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Функция для логирования +log() { + echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1" +} + +error() { + echo -e "${RED}[ERROR]${NC} $1" >&2 +} + +warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +# Обработка сигналов для корректного завершения +cleanup() { + log "Получен сигнал завершения, останавливаем сервисы..." + if [ ! -z "$LCG_PID" ]; then + kill $LCG_PID 2>/dev/null || true + wait $LCG_PID 2>/dev/null || true + fi + log "Сервисы остановлены" + exit 0 +} + +trap cleanup SIGTERM SIGINT + +# Проверка наличия бинарника lcg +if [ ! -f /usr/local/bin/lcg ]; then + error "Бинарник lcg не найден в /usr/local/bin/lcg" + exit 1 +fi + + +# Создаем необходимые директории +mkdir -p "${LCG_RESULT_FOLDER:-/app/data/results}" +mkdir -p "${LCG_PROMPT_FOLDER:-/app/data/prompts}" +mkdir -p "${LCG_CONFIG_FOLDER:-/app/data/config}" + +# Настройка переменных окружения для Ollama +export OLLAMA_HOST="${OLLAMA_HOST:-127.0.0.1}" +export OLLAMA_PORT="${OLLAMA_PORT:-11434}" +export OLLAMA_ORIGINS="*" + +# Настройка переменных окружения для LCG +export LCG_PROVIDER="${LCG_PROVIDER:-ollama}" +export LCG_HOST="${LCG_HOST:-http://127.0.0.1:11434/}" +export LCG_MODEL="${LCG_MODEL:-qwen2.5-coder:1.5b}" +export LCG_RESULT_FOLDER="${LCG_RESULT_FOLDER:-/app/data/results}" +export LCG_PROMPT_FOLDER="${LCG_PROMPT_FOLDER:-/app/data/prompts}" +export LCG_CONFIG_FOLDER="${LCG_CONFIG_FOLDER:-/app/data/config}" +export LCG_SERVER_HOST="${LCG_SERVER_HOST:-0.0.0.0}" +export LCG_SERVER_PORT="${LCG_SERVER_PORT:-8080}" +export LCG_SERVER_ALLOW_HTTP="${LCG_SERVER_ALLOW_HTTP:-true}" + +log "==========================================" +log "Запуск LCG с Ollama сервером" +log "==========================================" +info "LCG Provider: $LCG_PROVIDER" +info "LCG Host: $LCG_HOST" +info "LCG Model: $LCG_MODEL" +info "LCG Server: http://${LCG_SERVER_HOST}:${LCG_SERVER_PORT}" +info "Ollama Host: $OLLAMA_HOST:$OLLAMA_PORT" +log "==========================================" + + +log "Запуск LCG сервера..." +/usr/local/bin/lcg serve \ + --host "${LCG_SERVER_HOST}" \ + --port "${LCG_SERVER_PORT}" & +LCG_PID=$! + +# Ждем, пока LCG запустится +sleep 3 + +# Проверяем, что LCG запущен +if ! kill -0 $LCG_PID 2>/dev/null; then + error "LCG сервер не запустился" + kill $OLLAMA_PID 2>/dev/null || true + exit 1 +fi + +log "LCG сервер запущен на http://${LCG_SERVER_HOST}:${LCG_SERVER_PORT}" + +# Запускаем переданные аргументы +exec "$@" diff --git a/Dockerfiles/OllamaServer/podman-compose.yml b/Dockerfiles/OllamaServer/podman-compose.yml new file mode 100644 index 0000000..ca76825 --- /dev/null +++ b/Dockerfiles/OllamaServer/podman-compose.yml @@ -0,0 +1,54 @@ +version: '3.8' + +services: + lcg-ollama: + build: + context: ../.. + dockerfile: Dockerfiles/OllamaServer/Dockerfile + container_name: lcg-ollama + ports: + - "8080:8080" # LCG веб-сервер + - "11434:11434" # Ollama API + environment: + # Настройки LCG + - LCG_PROVIDER=ollama + - LCG_HOST=http://127.0.0.1:11434/ + - LCG_MODEL=codegeex4 + - LCG_RESULT_FOLDER=/app/data/results + - LCG_PROMPT_FOLDER=/app/data/prompts + - LCG_CONFIG_FOLDER=/app/data/config + - LCG_SERVER_HOST=0.0.0.0 + - LCG_SERVER_PORT=8080 + - LCG_SERVER_ALLOW_HTTP=true + # Настройки Ollama + - OLLAMA_HOST=0.0.0.0 + - OLLAMA_PORT=11434 + - OLLAMA_ORIGINS=* + # Опционально: настройки безопасности + - LCG_SERVER_REQUIRE_AUTH=false + - LCG_SERVER_PASSWORD=admin#123456 + volumes: + # Персистентное хранилище для данных Ollama + - ollama-data:/home/ollama/.ollama + # Персистентное хранилище для результатов LCG + - lcg-results:/app/data/results + - lcg-prompts:/app/data/prompts + - lcg-config:/app/data/config + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + +volumes: + ollama-data: + driver: local + lcg-results: + driver: local + lcg-prompts: + driver: local + lcg-config: + driver: local + diff --git a/VERSION.txt b/VERSION.txt index 68496c0..d640b26 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -v2.0.14 +v.2.0.16 diff --git a/config/config.go b/config/config.go index d2237f3..6d7070f 100644 --- a/config/config.go +++ b/config/config.go @@ -27,6 +27,7 @@ type Config struct { ResultHistory string NoHistoryEnv string AllowExecution bool + Query string MainFlags MainFlags Server ServerConfig Validation ValidationConfig diff --git a/deploy/.goreleaser.yaml b/deploy/.goreleaser.yaml index 3fce9f5..1a11f75 100644 --- a/deploy/.goreleaser.yaml +++ b/deploy/.goreleaser.yaml @@ -6,6 +6,8 @@ builds: binary: "lcg_{{ .Version }}" goos: - linux + - windows + - darwin goarch: - amd64 - arm64 @@ -21,9 +23,10 @@ builds: archives: - id: lcg - builds: + ids: - lcg - format: binary + formats: + - binary name_template: "{{ .Binary }}_{{ .Os }}_{{ .Arch }}" files: - "lcg_{{ .Version }}" diff --git a/deploy/VERSION.txt b/deploy/VERSION.txt index 68496c0..d640b26 100644 --- a/deploy/VERSION.txt +++ b/deploy/VERSION.txt @@ -1 +1 @@ -v2.0.14 +v.2.0.16 diff --git a/deploy/release-goreleaser.sh b/deploy/release-goreleaser.sh new file mode 100644 index 0000000..55c6066 --- /dev/null +++ b/deploy/release-goreleaser.sh @@ -0,0 +1,170 @@ +#!/usr/bin/env bash +set -euo pipefail + +# release-goreleaser.sh +# Копирует deploy/.goreleaser.yaml в корень, запускает релиз и удаляет файл. +# +# Использование: +# deploy/release-goreleaser.sh # обычный релиз на GitHub (нужен GITHUB_TOKEN) +# deploy/release-goreleaser.sh --snapshot # локальный снепшот без публикации + +ROOT_DIR="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" +SRC_CFG="$ROOT_DIR/deploy/.goreleaser.yaml" +DST_CFG="$ROOT_DIR/.goreleaser.yaml" + +log() { echo -e "\033[36m[release]\033[0m $*"; } +err() { echo -e "\033[31m[error]\033[0m $*" >&2; } + +if ! command -v goreleaser >/dev/null 2>&1; then + err "goreleaser не найден. Установите: https://goreleaser.com/install/" + exit 1 +fi + +if [[ ! -f "$SRC_CFG" ]]; then + err "Не найден файл конфигурации: $SRC_CFG" + exit 1 +fi + +MODE="release" +if [[ "${1:-}" == "--snapshot" ]]; then + MODE="snapshot" + shift || true +fi + +if [[ -f "$DST_CFG" ]]; then + err "В корне уже существует .goreleaser.yaml. Удалите/переименуйте перед запуском." + exit 1 +fi + +cleanup() { + if [[ -f "$DST_CFG" ]]; then + rm -f "$DST_CFG" || true + log "Удалил временный $DST_CFG" + fi +} +trap cleanup EXIT + +log "Копирую конфиг: $SRC_CFG -> $DST_CFG" +cp "$SRC_CFG" "$DST_CFG" + +pushd "$ROOT_DIR" >/dev/null + +EXTRA_FLAGS=() +PREV_HEAD="$(git rev-parse HEAD 2>/dev/null || echo "")" + +git add . +git commit --amend --no-edit || true + +## Версию берём из deploy/VERSION.txt или VERSION.txt в корне +VERSION_FILE="$ROOT_DIR/deploy/VERSION.txt" +[[ -f "$VERSION_FILE" ]] || VERSION_FILE="$ROOT_DIR/VERSION.txt" +if [[ -f "$VERSION_FILE" ]]; then + VERSION_RAW="$(head -n1 "$VERSION_FILE" | tr -d ' \t\r\n')" + if [[ -n "$VERSION_RAW" ]]; then + TAG="$VERSION_RAW" + [[ "$TAG" == v* ]] || TAG="v$TAG" + export GORELEASER_CURRENT_TAG="$TAG" + log "Версия релиза: $TAG (из $(realpath --relative-to="$ROOT_DIR" "$VERSION_FILE" 2>/dev/null || echo "$VERSION_FILE"))" + fi +fi + +create_and_push_tag() { + local tag="$1" + if git rev-parse "$tag" >/dev/null 2>&1; then + log "Git tag уже существует: $tag" + else + log "Создаю git tag: $tag" + git tag -a "$tag" -m "Release $tag" + if [[ "${NO_GIT_PUSH:-false}" != "true" ]]; then + log "Пушу тег $tag на origin" + git push origin "$tag" + else + log "Пропущен пуш тега (NO_GIT_PUSH=true)" + fi + fi +} + +move_tag_to_head() { + local tag="$1" + if [[ -z "$tag" ]]; then + return 0 + fi + if git rev-parse "$tag" >/dev/null 2>&1; then + log "Переношу тег $tag на текущий коммит (HEAD)" + git tag -f "$tag" HEAD + if [[ "${NO_GIT_PUSH:-false}" != "true" ]]; then + log "Форс‑пуш тега $tag на origin" + git push -f origin "$tag" + else + log "Пропущен пуш тега (NO_GIT_PUSH=true)" + fi + else + log "Тега $tag нет — пропускаю перенос" + fi +} + +fetch_token_from_k8s() { + export KUBECONFIG=/home/su/.kube/config_hlab + local ns="${K8S_NAMESPACE:-flux-system}" + local name="${K8S_SECRET_NAME:-git-secrets}" + # Предпочитаем jq (как в примере), при отсутствии используем jsonpath + base64 -d + if command -v jq >/dev/null 2>&1; then + kubectl get secret "$name" -n "$ns" -o json \ + | jq -r '.data.password | @base64d' + else + kubectl get secret "$name" -n "$ns" -o jsonpath='{.data.password}' \ + | base64 -d 2>/dev/null || true + fi +} + +if [[ "$MODE" == "snapshot" ]]; then + log "Запуск goreleaser (snapshot, без публикации)" + goreleaser release --snapshot --clean --config "$DST_CFG" "${EXTRA_FLAGS[@]}" +else + # Если версия определена и тега нет — создадим (goreleaser ориентируется на теги) + if [[ -n "${GORELEASER_CURRENT_TAG:-}" ]]; then + create_and_push_tag "$GORELEASER_CURRENT_TAG" + # Перемещаем тег на текущий HEAD (если существовал ранее, закрепим на последнем коммите) + move_tag_to_head "$GORELEASER_CURRENT_TAG" + else + # Если версия не задана, попробуем взять последний существующий тег и перенести его на HEAD + LAST_TAG="$(git describe --tags --abbrev=0 2>/dev/null || true)" + if [[ -n "$LAST_TAG" ]]; then + move_tag_to_head "$LAST_TAG" + export GORELEASER_CURRENT_TAG="$LAST_TAG" + log "Использую последний тег: $LAST_TAG" + fi + fi + if [[ -z "${GITHUB_TOKEN:-}" ]]; then + log "GITHUB_TOKEN не задан — пробую получить из k8s секрета (${K8S_NAMESPACE:-flux-system}/${K8S_SECRET_NAME:-git-secrets}, ключ: password)" + if ! command -v kubectl >/dev/null 2>&1; then + err "kubectl не найден, а GITHUB_TOKEN не задан. Установите kubectl или экспортируйте GITHUB_TOKEN." + exit 1 + fi + TOKEN_FROM_K8S="$(fetch_token_from_k8s || true)" + if [[ -n "$TOKEN_FROM_K8S" && "$TOKEN_FROM_K8S" != "null" ]]; then + export GITHUB_TOKEN="$TOKEN_FROM_K8S" + log "GITHUB_TOKEN получен из секрета Kubernetes." + else + err "Не удалось получить GITHUB_TOKEN из секрета Kubernetes. Экспортируйте GITHUB_TOKEN и повторите." + exit 1 + fi + fi + log "Запуск goreleaser (публикация на GitHub)" + goreleaser release --clean --config "$DST_CFG" "${EXTRA_FLAGS[@]}" +fi + +popd >/dev/null + +# Откатываем временный коммит, если он был +if [[ "${TEMP_COMMIT_DONE:-false}" == "true" && -n "$PREV_HEAD" ]]; then + if git reset --soft "$PREV_HEAD" >/dev/null 2>&1; then + log "Откатил временный коммит" + else + log "Не удалось откатить временный коммит — проверьте историю вручную" + fi +fi + +log "Готово." + + diff --git a/docs/CONFIG_COMMAND.md b/docs/CONFIG_COMMAND.md index 1b35f76..70ef9af 100644 --- a/docs/CONFIG_COMMAND.md +++ b/docs/CONFIG_COMMAND.md @@ -69,8 +69,8 @@ lcg co -f "host": "localhost" }, "validation": { - "max_system_prompt_length": 1000, - "max_user_message_length": 2000, + "max_system_prompt_length": 2000, + "max_user_message_length": 4000, "max_prompt_name_length": 2000, "max_prompt_desc_length": 5000, "max_command_length": 8000, @@ -116,12 +116,12 @@ lcg co -f ### Настройки валидации (validation) -- **max_system_prompt_length** - максимальная длина системного промпта -- **max_user_message_length** - максимальная длина пользовательского сообщения -- **max_prompt_name_length** - максимальная длина названия промпта -- **max_prompt_desc_length** - максимальная длина описания промпта -- **max_command_length** - максимальная длина команды/ответа -- **max_explanation_length** - максимальная длина объяснения +- **max_system_prompt_length** - максимальная длина системного промпта (по умолчанию: 2000) +- **max_user_message_length** - максимальная длина пользовательского сообщения (по умолчанию: 4000) +- **max_prompt_name_length** - максимальная длина названия промпта (по умолчанию: 2000) +- **max_prompt_desc_length** - максимальная длина описания промпта (по умолчанию: 5000) +- **max_command_length** - максимальная длина команды/ответа (по умолчанию: 8000) +- **max_explanation_length** - максимальная длина объяснения (по умолчанию: 20000) ## 🔒 Безопасность diff --git a/docs/README.md b/docs/README.md index 29a51a0..d5bcf78 100644 --- a/docs/README.md +++ b/docs/README.md @@ -35,10 +35,17 @@ Explanations: Clipboard support requires `xclip` or `xsel`. -## What's new in 2.0.1 +## What's new in 2.0.14 -- Mobile UI improvements: better responsiveness (buttons, fonts, spacing) and reduced motion support -- Public REST endpoint: `POST /execute` (curl-only) for programmatic access — see `API_GUIDE.md` +- Authentication: JWT-based authentication with HTTP-only cookies +- CSRF protection: Full CSRF protection with tokens and middleware +- Security: Enhanced security with token validation and sessions +- Kubernetes deployment: Full set of manifests for Kubernetes deployment with Traefik +- Reverse Proxy: Support for working behind reverse proxy with cookie configuration +- Web interface: Improved web interface with modern design +- Monitoring: Prometheus metrics and ServiceMonitor +- Scaling: HPA for automatic scaling +- Testing: CSRF protection testing tools ## Environment @@ -133,10 +140,15 @@ The `serve` command provides both a web interface and REST API: **Web Interface:** -- Browse results at `http://localhost:8080/` -- Execute requests at `http://localhost:8080/run` -- Manage prompts at `http://localhost:8080/prompts` -- View history at `http://localhost:8080/history` +- Browse results at `http://localhost:8080/` (or `http://localhost:8080/` if `LCG_BASE_URL` set) +- Execute requests at `.../run` +- Manage prompts at `.../prompts` +- View history at `.../history` + +Notes: +- Base path: set `LCG_BASE_URL` (e.g. `/lcg`) to prefix all routes and API. +- Custom 404: unknown paths under base path render a modern 404 page. +- Debug: enable via flag `--debug` or env `LCG_DEBUG=1|true`. **REST API:** diff --git a/docs/USAGE_GUIDE.md b/docs/USAGE_GUIDE.md index 4c74dc8..c408c51 100644 --- a/docs/USAGE_GUIDE.md +++ b/docs/USAGE_GUIDE.md @@ -22,7 +22,7 @@ sudo apt-get install xsel ```bash -git clone --depth 1 https://github.com/Direct-Dev-Ru/go-lcg.git ~/.linux-command-gpt +git clone --depth 1 https://github.com/Direct-Dev-Ru/linux-command-gpt.git ~/.linux-command-gpt cd ~/.linux-command-gpt go build -o lcg @@ -60,7 +60,7 @@ lcg --file /path/to/context.txt "хочу вывести список дирек Действия: (c)копировать, (s)сохранить, (r)перегенерировать, (e)выполнить, (v|vv|vvv)подробно, (n)ничего: ``` -### Что нового в 3.0.0 +### Что нового в 2.0.14 - **Аутентификация**: Добавлена система аутентификации с JWT токенами и HTTP-only cookies - **CSRF защита**: Полная защита от CSRF атак с токенами и middleware diff --git a/docs/VALIDATION_CONFIG.md b/docs/VALIDATION_CONFIG.md index e1f2d9d..de88b96 100644 --- a/docs/VALIDATION_CONFIG.md +++ b/docs/VALIDATION_CONFIG.md @@ -8,26 +8,26 @@ | Переменная | Описание | По умолчанию | |------------|----------|--------------| -| `LCG_MAX_SYSTEM_PROMPT_LENGTH` | Максимальная длина системного промпта | 1000 | -| `LCG_MAX_USER_MESSAGE_LENGTH` | Максимальная длина пользовательского сообщения | 2000 | -| `LCG_MAX_PROMPT_NAME_LENGTH` | Максимальная длина названия промпта | 200 | -| `LCG_MAX_PROMPT_DESC_LENGTH` | Максимальная длина описания промпта | 500 | -| `LCG_MAX_COMMAND_LENGTH` | Максимальная длина команды/ответа | 2000 | -| `LCG_MAX_EXPLANATION_LENGTH` | Максимальная длина объяснения | 2000 | +| `LCG_MAX_SYSTEM_PROMPT_LENGTH` | Максимальная длина системного промпта | 2000 | +| `LCG_MAX_USER_MESSAGE_LENGTH` | Максимальная длина пользовательского сообщения | 4000 | +| `LCG_MAX_PROMPT_NAME_LENGTH` | Максимальная длина названия промпта | 2000 | +| `LCG_MAX_PROMPT_DESC_LENGTH` | Максимальная длина описания промпта | 5000 | +| `LCG_MAX_COMMAND_LENGTH` | Максимальная длина команды/ответа | 8000 | +| `LCG_MAX_EXPLANATION_LENGTH` | Максимальная длина объяснения | 20000 | ## 🚀 Примеры использования ### Установка через переменные окружения ```bash -# Увеличить лимит системного промпта до 2к символов -export LCG_MAX_SYSTEM_PROMPT_LENGTH=2000 +# Увеличить лимит системного промпта до 3к символов +export LCG_MAX_SYSTEM_PROMPT_LENGTH=3000 -# Уменьшить лимит пользовательского сообщения до 1к символов -export LCG_MAX_USER_MESSAGE_LENGTH=1000 +# Уменьшить лимит пользовательского сообщения до 2к символов +export LCG_MAX_USER_MESSAGE_LENGTH=2000 -# Увеличить лимит названия промпта до 500 символов -export LCG_MAX_PROMPT_NAME_LENGTH=500 +# Увеличить лимит названия промпта до 3000 символов +export LCG_MAX_PROMPT_NAME_LENGTH=3000 ``` ### Установка в .env файле @@ -35,11 +35,11 @@ export LCG_MAX_PROMPT_NAME_LENGTH=500 ```bash # .env файл LCG_MAX_SYSTEM_PROMPT_LENGTH=2000 -LCG_MAX_USER_MESSAGE_LENGTH=1500 -LCG_MAX_PROMPT_NAME_LENGTH=300 -LCG_MAX_PROMPT_DESC_LENGTH=1000 -LCG_MAX_COMMAND_LENGTH=3000 -LCG_MAX_EXPLANATION_LENGTH=5000 +LCG_MAX_USER_MESSAGE_LENGTH=4000 +LCG_MAX_PROMPT_NAME_LENGTH=2000 +LCG_MAX_PROMPT_DESC_LENGTH=5000 +LCG_MAX_COMMAND_LENGTH=8000 +LCG_MAX_EXPLANATION_LENGTH=20000 ``` ### Установка в systemd сервисе @@ -55,8 +55,8 @@ User=lcg WorkingDirectory=/opt/lcg ExecStart=/opt/lcg/lcg serve Environment=LCG_MAX_SYSTEM_PROMPT_LENGTH=2000 -Environment=LCG_MAX_USER_MESSAGE_LENGTH=1500 -Environment=LCG_MAX_PROMPT_NAME_LENGTH=300 +Environment=LCG_MAX_USER_MESSAGE_LENGTH=4000 +Environment=LCG_MAX_PROMPT_NAME_LENGTH=2000 Restart=always [Install] @@ -72,7 +72,7 @@ FROM golang:1.21-alpine AS builder FROM alpine:latest COPY --from=builder /app/lcg /usr/local/bin/ ENV LCG_MAX_SYSTEM_PROMPT_LENGTH=2000 -ENV LCG_MAX_USER_MESSAGE_LENGTH=1500 +ENV LCG_MAX_USER_MESSAGE_LENGTH=4000 CMD ["lcg", "serve"] ``` @@ -84,8 +84,8 @@ services: image: lcg:latest environment: - LCG_MAX_SYSTEM_PROMPT_LENGTH=2000 - - LCG_MAX_USER_MESSAGE_LENGTH=1500 - - LCG_MAX_PROMPT_NAME_LENGTH=300 + - LCG_MAX_USER_MESSAGE_LENGTH=4000 + - LCG_MAX_PROMPT_NAME_LENGTH=2000 ports: - "8080:8080" ``` @@ -153,9 +153,9 @@ validation.FormatLengthInfo(systemPrompt, userMessage) ## 📝 Примеры сообщений об ошибках ``` -❌ Ошибка: system_prompt: системный промпт слишком длинный: 1500 символов (максимум 1000) -❌ Ошибка: user_message: пользовательское сообщение слишком длинное: 2500 символов (максимум 2000) -❌ Ошибка: prompt_name: название промпта слишком длинное: 300 символов (максимум 200) +❌ Ошибка: system_prompt: системный промпт слишком длинный: 2500 символов (максимум 2000) +❌ Ошибка: user_message: пользовательское сообщение слишком длинное: 4500 символов (максимум 4000) +❌ Ошибка: prompt_name: название промпта слишком длинное: 2500 символов (максимум 2000) ``` ## 🔄 Миграция с жестко заданных значений @@ -179,25 +179,25 @@ if err := validation.ValidateSystemPrompt(prompt); err != nil { ### Для разработки ```bash export LCG_MAX_SYSTEM_PROMPT_LENGTH=2000 -export LCG_MAX_USER_MESSAGE_LENGTH=2000 -export LCG_MAX_PROMPT_NAME_LENGTH=200 -export LCG_MAX_PROMPT_DESC_LENGTH=500 +export LCG_MAX_USER_MESSAGE_LENGTH=4000 +export LCG_MAX_PROMPT_NAME_LENGTH=2000 +export LCG_MAX_PROMPT_DESC_LENGTH=5000 ``` ### Для продакшена ```bash -export LCG_MAX_SYSTEM_PROMPT_LENGTH=1000 -export LCG_MAX_USER_MESSAGE_LENGTH=1500 -export LCG_MAX_PROMPT_NAME_LENGTH=100 -export LCG_MAX_PROMPT_DESC_LENGTH=300 +export LCG_MAX_SYSTEM_PROMPT_LENGTH=2000 +export LCG_MAX_USER_MESSAGE_LENGTH=4000 +export LCG_MAX_PROMPT_NAME_LENGTH=2000 +export LCG_MAX_PROMPT_DESC_LENGTH=5000 ``` ### Для высоконагруженных систем ```bash -export LCG_MAX_SYSTEM_PROMPT_LENGTH=500 -export LCG_MAX_USER_MESSAGE_LENGTH=1000 -export LCG_MAX_PROMPT_NAME_LENGTH=50 -export LCG_MAX_PROMPT_DESC_LENGTH=200 +export LCG_MAX_SYSTEM_PROMPT_LENGTH=1000 +export LCG_MAX_USER_MESSAGE_LENGTH=2000 +export LCG_MAX_PROMPT_NAME_LENGTH=1000 +export LCG_MAX_PROMPT_DESC_LENGTH=2500 ``` --- diff --git a/kustomize/configmap.yaml b/kustomize/configmap.yaml index c3fbbe8..d9fb147 100644 --- a/kustomize/configmap.yaml +++ b/kustomize/configmap.yaml @@ -5,7 +5,7 @@ metadata: namespace: lcg data: # Основные настройки - LCG_VERSION: "v2.0.14" + LCG_VERSION: "v.2.0.16" LCG_BASE_PATH: "/lcg" LCG_SERVER_HOST: "0.0.0.0" LCG_SERVER_PORT: "8080" diff --git a/kustomize/deployment.yaml b/kustomize/deployment.yaml index 480c4ae..94d2f22 100644 --- a/kustomize/deployment.yaml +++ b/kustomize/deployment.yaml @@ -5,7 +5,7 @@ metadata: namespace: lcg labels: app: lcg - version: v2.0.14 + version: v.2.0.16 spec: replicas: 1 selector: @@ -18,7 +18,7 @@ spec: spec: containers: - name: lcg - image: kuznetcovay/lcg:v2.0.14 + image: kuznetcovay/lcg:v.2.0.16 imagePullPolicy: Always ports: - containerPort: 8080 diff --git a/kustomize/kustomization.yaml b/kustomize/kustomization.yaml index 5824368..916f555 100644 --- a/kustomize/kustomization.yaml +++ b/kustomize/kustomization.yaml @@ -15,11 +15,11 @@ resources: # Common labels # commonLabels: # app: lcg -# version: v2.0.14 +# version: v.2.0.16 # managed-by: kustomize # Images # images: # - name: lcg # newName: kuznetcovay/lcg -# newTag: v2.0.14 +# newTag: v.2.0.16 diff --git a/main.go b/main.go index 114314f..326dee9 100644 --- a/main.go +++ b/main.go @@ -9,6 +9,7 @@ import ( "os/exec" "os/user" "path/filepath" + "slices" "strconv" "strings" "time" @@ -60,7 +61,7 @@ func main() { CompileConditions.NoServe = false } - fmt.Println("Build conditions:", CompileConditions) + // fmt.Println("Build conditions:", CompileConditions) _ = colorBlue @@ -76,6 +77,12 @@ func main() { Usage: config.AppConfig.AppName + " - Генерация Linux команд из описаний", Version: Version, Commands: getCommands(), + Before: func(c *cli.Context) error { + // Применяем флаги приложения к конфигурации перед выполнением любой команды + // Это гарантирует, что флаги будут применены даже для команд, которые не используют основной Action + applyAppFlagsToConfig(c) + return nil + }, UsageText: ` lcg [опции] <описание команды> @@ -87,12 +94,56 @@ lcg [опции] <описание команды> {{.AppName}} - инструмент для генерации Linux команд из описаний на естественном языке. Поддерживает чтение частей промпта из файлов и позволяет сохранять, копировать или перегенерировать результаты. может задавать системный промпт или выбирать из предустановленных промптов. + Переменные окружения: - LCG_HOST Endpoint для LLM API (по умолчанию: http://192.168.87.108:11434/) - LCG_MODEL Название модели (по умолчанию: codegeex4) - LCG_PROMPT Текст промпта по умолчанию - LCG_PROVIDER Тип провайдера: "ollama" или "proxy" (по умолчанию: ollama) - LCG_JWT_TOKEN JWT токен для proxy провайдера + +Основные настройки: + LCG_HOST Endpoint для LLM API (по умолчанию: http://192.168.87.108:11434/) + LCG_MODEL Название модели (по умолчанию: hf.co/yandex/YandexGPT-5-Lite-8B-instruct-GGUF:Q4_K_M) + LCG_PROMPT Текст промпта по умолчанию + LCG_PROVIDER Тип провайдера: "ollama" или "proxy" (по умолчанию: ollama) + LCG_JWT_TOKEN JWT токен для proxy провайдера + LCG_PROMPT_ID ID промпта по умолчанию (по умолчанию: 1) + LCG_TIMEOUT Таймаут запроса в секундах (по умолчанию: 300) + LCG_COMPLETIONS_PATH Путь к API для завершений (по умолчанию: api/chat) + LCG_PROXY_URL URL прокси для proxy провайдера (по умолчанию: /api/v1/protected/sberchat/chat) + LCG_API_KEY_FILE Файл с API ключом (по умолчанию: .openai_api_key) + LCG_APP_NAME Название приложения (по умолчанию: Linux Command GPT) + +Настройки истории и выполнения: + LCG_NO_HISTORY Отключить запись истории ("1" или "true" = отключено, пусто = включено) + LCG_ALLOW_EXECUTION Разрешить выполнение команд ("1" или "true" = разрешено, пусто = запрещено) + LCG_RESULT_FOLDER Папка для сохранения результатов (по умолчанию: ~/.config/lcg/gpt_results) + LCG_RESULT_HISTORY Файл истории результатов (по умолчанию: /lcg_history.json) + LCG_PROMPT_FOLDER Папка для системных промптов (по умолчанию: ~/.config/lcg/gpt_sys_prompts) + LCG_CONFIG_FOLDER Папка для конфигурации (по умолчанию: ~/.config/lcg/config) + +Настройки сервера (команда serve): + LCG_SERVER_PORT Порт сервера (по умолчанию: 8080) + LCG_SERVER_HOST Хост сервера (по умолчанию: localhost) + LCG_SERVER_ALLOW_HTTP Разрешить HTTP соединения ("true" для localhost, "false" для других хостов) + LCG_SERVER_REQUIRE_AUTH Требовать аутентификацию ("1" или "true" = требуется, пусто = не требуется) + LCG_SERVER_PASSWORD Пароль администратора (по умолчанию: admin#123456) + LCG_SERVER_SSL_CERT_FILE Путь к SSL сертификату + LCG_SERVER_SSL_KEY_FILE Путь к приватному ключу SSL + LCG_DOMAIN Домен для сервера (по умолчанию: значение LCG_SERVER_HOST) + LCG_COOKIE_SECURE Безопасные cookie ("1" или "true" = включено, пусто = выключено) + LCG_COOKIE_PATH Путь для cookie (по умолчанию: /lcg) + LCG_COOKIE_TTL_HOURS Время жизни cookie в часах (по умолчанию: 168) + LCG_BASE_URL Базовый URL приложения (по умолчанию: /lcg) + LCG_HEALTH_URL URL для проверки здоровья API (по умолчанию: /api/v1/protected/sberchat/health) + +Настройки валидации: + LCG_MAX_SYSTEM_PROMPT_LENGTH Максимальная длина системного промпта (по умолчанию: 2000) + LCG_MAX_USER_MESSAGE_LENGTH Максимальная длина пользовательского сообщения (по умолчанию: 4000) + LCG_MAX_PROMPT_NAME_LENGTH Максимальная длина названия промпта (по умолчанию: 2000) + LCG_MAX_PROMPT_DESC_LENGTH Максимальная длина описания промпта (по умолчанию: 5000) + LCG_MAX_COMMAND_LENGTH Максимальная длина команды (по умолчанию: 8000) + LCG_MAX_EXPLANATION_LENGTH Максимальная длина объяснения (по умолчанию: 20000) + +Отладка и браузер: + LCG_DEBUG Включить режим отладки ("1" или "true" = включено, пусто = выключено) + LCG_BROWSER_PATH Путь к браузеру для автоматического открытия (команда serve --browser) `, Flags: []cli.Flag{ &cli.StringFlag{ @@ -100,12 +151,25 @@ lcg [опции] <описание команды> Aliases: []string{"f"}, Usage: "Read part of the command from a file", }, + &cli.StringFlag{ + Name: "model", + Aliases: []string{"M"}, + DefaultText: "Use model from LCG_MODEL or default model", + Usage: "Model to use", + }, &cli.BoolFlag{ Name: "no-history", Aliases: []string{"nh"}, Usage: "Disable writing/updating command history (overrides LCG_NO_HISTORY)", Value: false, }, + &cli.StringFlag{ + Name: "query", + Aliases: []string{"Q"}, + Usage: "Query to send to the model", + DefaultText: "Hello? what day is it today?", + Value: "Hello? what day is it today?", + }, &cli.StringFlag{ Name: "sys", Aliases: []string{"s"}, @@ -137,16 +201,25 @@ lcg [опции] <описание команды> Action: func(c *cli.Context) error { file := c.String("file") system := c.String("sys") + model := c.String("model") + query := c.String("query") // обновляем конфиг на основе флагов - if system != "" { + if c.IsSet("sys") && system != "" { config.AppConfig.Prompt = system } + if c.IsSet("query") && query != "" { + config.AppConfig.Query = query + } if c.IsSet("timeout") { config.AppConfig.Timeout = fmt.Sprintf("%d", c.Int("timeout")) } + if c.IsSet("model") { + config.AppConfig.Model = model + } + promptID := c.Int("prompt-id") timeout := c.Int("timeout") - // сохраняем конкретные значения флагов + config.AppConfig.MainFlags = config.MainFlags{ File: file, NoHistory: c.Bool("no-history"), @@ -159,12 +232,9 @@ lcg [опции] <описание команды> config.AppConfig.MainFlags.Debug = config.AppConfig.MainFlags.Debug || config.GetEnvBool("LCG_DEBUG", false) - fmt.Println("Debug:", config.AppConfig.MainFlags.Debug) - fmt.Println("LCG_DEBUG:", config.GetEnvBool("LCG_DEBUG", false)) - args := c.Args().Slice() - if len(args) == 0 { + if len(args) == 0 && config.AppConfig.Query == "" { cli.ShowAppHelp(c) showTips() return nil @@ -187,6 +257,12 @@ lcg [опции] <описание команды> os.Exit(1) } } + + if config.AppConfig.Query != "" { + executeMain(file, system, config.AppConfig.Query, timeout) + return nil + } + executeMain(file, system, strings.Join(args, " "), timeout) return nil }, @@ -207,6 +283,31 @@ lcg [опции] <описание команды> } } +// applyAppFlagsToConfig применяет флаги приложения к конфигурации +// Работает как для основного Action, так и для команд +func applyAppFlagsToConfig(c *cli.Context) { + // Применяем флаг model - проверяем и через IsSet, и значение напрямую + // так как IsSet может не работать для флагов без значения по умолчанию + if model := c.String("model"); model != "" { + config.AppConfig.Model = model + } + + // Применяем флаг sys + if sys := c.String("sys"); sys != "" { + config.AppConfig.Prompt = sys + } + + // Применяем флаг timeout (только если явно установлен) + if c.IsSet("timeout") { + config.AppConfig.Timeout = fmt.Sprintf("%d", c.Int("timeout")) + } + + // Применяем флаг query (игнорируем значение по умолчанию) + if query := c.String("query"); query != "" && query != "Hello? what day is it today?" { + config.AppConfig.Query = query + } +} + func getCommands() []*cli.Command { commands := []*cli.Command{ { @@ -346,6 +447,10 @@ func getCommands() []*cli.Command { }, }, Action: func(c *cli.Context) error { + // Флаги приложения уже применены через глобальный Before hook + // Но применяем их еще раз на случай, если глобальный Before не сработал + applyAppFlagsToConfig(c) + if c.Bool("full") { // Выводим полную конфигурацию в JSON формате showFullConfig() @@ -983,12 +1088,7 @@ func getServerAllowHTTPForHost(host string) bool { // isSecureHost проверяет, является ли хост безопасным для HTTP func isSecureHost(host string) bool { secureHosts := []string{"localhost", "127.0.0.1", "::1"} - for _, secureHost := range secureHosts { - if host == secureHost { - return true - } - } - return false + return slices.Contains(secureHosts, host) } // showShortConfig показывает краткую конфигурацию diff --git a/serve/serve.go b/serve/serve.go index f05dd78..d0d8244 100644 --- a/serve/serve.go +++ b/serve/serve.go @@ -232,11 +232,11 @@ func registerRoutesExceptHome() { http.HandleFunc(makePath("/api/add-to-history"), AuthMiddleware(CSRFMiddleware(handleAddToHistory))) // Catch-all 404 для любых незарегистрированных путей (только когда BasePath задан) - if getBasePath() != "" { - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - renderNotFound(w, "Страница не найдена", getBasePath()) - }) - } + // if getBasePath() != "" { + // http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + // renderNotFound(w, "Страница не найдена", getBasePath()) + // }) + // } } // registerRoutes регистрирует все маршруты сервера diff --git a/serve/templates/not_found.go b/serve/templates/not_found.go index 09296d0..99c90f4 100644 --- a/serve/templates/not_found.go +++ b/serve/templates/not_found.go @@ -53,6 +53,16 @@ const NotFoundTemplate = ` backdrop-filter: blur(10px); text-align: center; } + @keyframes pulse { + 0%, 100% { + transform: scale(1); + text-shadow: 0 8px 40px var(--accentGlow); + } + 50% { + transform: scale(1.15); + text-shadow: 0 12px 60px var(--accentGlow), 0 0 30px var(--accentGlow2); + } + } .code { font-size: clamp(48px, 12vw, 120px); line-height: 0.9; @@ -64,6 +74,8 @@ const NotFoundTemplate = ` color: transparent; margin: 8px 0 12px 0; text-shadow: 0 8px 40px var(--accentGlow); + animation: pulse 2.5s ease-in-out infinite; + transform-origin: center; } .title { font-size: clamp(18px, 3.2vw, 28px); @@ -133,7 +145,7 @@ const NotFoundTemplate = `
404
Страница не найдена
-

{{.Message}}

+

Такой страницы не существует. Вы можете вернуться на главную страницу или выполнить команду.

🏠 На главную 🚀 К выполнению @@ -143,5 +155,3 @@ const NotFoundTemplate = ` ` - -