Compare commits

..

26 Commits

Author SHA1 Message Date
d0b53607c4 Merged main into release while building v.2.0.17 2025-11-09 13:29:15 +06:00
487f3d484c Исправления в ветке main 2025-11-09 13:29:11 +06:00
814ca9ba7f Merged main into release while building v.2.0.16 2025-11-09 12:47:27 +06:00
c975e00c50 Исправления в ветке main 2025-11-09 12:47:23 +06:00
01488edbee Merge branch 'main' of github.com:Direct-Dev-Ru/go-lcg into main 2025-11-08 16:04:46 +06:00
63876a393c v2.0.15 2025-11-08 16:04:17 +06:00
eb9a7dcf32 v2.0.15 2025-11-08 16:02:35 +06:00
4779c4bca4 chore: temp goreleaser config [skip ci] 2025-10-29 10:25:47 +06:00
1e2ce929b2 release to github 2.0.14 2025-10-28 19:07:50 +06:00
9aa5aefdad docs synced 2025-10-28 18:57:28 +06:00
49a41c597a Merged main into release while building v2.0.14 2025-10-28 18:06:58 +06:00
7455987c0f Исправления в ветке main 2025-10-28 18:06:54 +06:00
93f60c4e36 Merged main into release while building v2.0.13 2025-10-28 17:56:06 +06:00
3e143ee7a1 Исправления в ветке main 2025-10-28 17:56:03 +06:00
8cdb31d96d Merged main into release while building v2.0.12 2025-10-28 17:37:25 +06:00
96a8060afb Исправления в ветке main 2025-10-28 17:37:22 +06:00
a20fb846f0 Merged main into release while building v2.0.11 2025-10-28 17:30:35 +06:00
99b1a74034 Исправления в ветке main 2025-10-28 17:30:30 +06:00
90cfc6fb0c Merged main into release while building v2.0.11 2025-10-28 16:58:58 +06:00
89d15bfdc9 Исправления в ветке main 2025-10-28 16:58:54 +06:00
114146f4d2 Merged main into release while building v2.0.10 2025-10-28 16:07:20 +06:00
5c672ecc39 Исправления в ветке main 2025-10-28 16:07:17 +06:00
b4b902cb4c Merged main into release while building v2.0.7 2025-10-28 15:12:13 +06:00
164f32dbaf Исправления в ветке main 2025-10-28 15:12:10 +06:00
7933abe62d Merged main into release while building v2.0.7 2025-10-28 14:52:58 +06:00
1545fe2508 Исправления в ветке main 2025-10-28 14:52:55 +06:00
33 changed files with 2053 additions and 141 deletions

View File

@@ -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)
=========================

View File

@@ -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

15
Dockerfiles/OllamaServer/.gitignore vendored Normal file
View File

@@ -0,0 +1,15 @@
# Временные файлы
*.log
*.tmp
*.temp
# IDE
.idea/
.vscode/
*.swp
*.swo
# OS files
.DS_Store
Thumbs.db

View File

@@ -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 []

View File

@@ -0,0 +1,124 @@
.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 8989:8080 \
-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)
echo "Запустить контейнер ${CONTAINER_NAME}"
echo "IMAGE_NAME: ${IMAGE_NAME}"
echo "IMAGE_TAG: ${IMAGE_TAG}"
echo "CONTAINER_NAME: ${CONTAINER_NAME}"
podman run -d \
--name ${CONTAINER_NAME} \
--restart always \
-p 8989:8080 \
-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

View File

@@ -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

View File

@@ -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 веб-интерфейс**: <http://localhost:8080>
- **Ollama API**: <http://localhost:11434>
## ⚙️ Переменные окружения
### Настройки 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

View File

@@ -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.

View File

@@ -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

View File

@@ -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:-0.0.0.0}"
export OLLAMA_PORT="${OLLAMA_PORT:-11434}"
export OLLAMA_ORIGINS="*"
# Настройка переменных окружения для LCG
export LCG_PROVIDER="${LCG_PROVIDER:-ollama}"
export LCG_HOST="${LCG_HOST:-http://0.0.0.0: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 "$@"

View File

@@ -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

View File

@@ -1 +1 @@
v2.0.5
v.2.0.17

View File

@@ -27,6 +27,7 @@ type Config struct {
ResultHistory string
NoHistoryEnv string
AllowExecution bool
Query string
MainFlags MainFlags
Server ServerConfig
Validation ValidationConfig

View File

@@ -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 }}"

View File

@@ -17,6 +17,7 @@ data:
LCG_NO_HISTORY: "false"
LCG_ALLOW_EXECUTION: "false"
LCG_DEBUG: "true"
LCG_PROVIDER: "proxy"
# Настройки аутентификации
LCG_SERVER_REQUIRE_AUTH: "true"

View File

@@ -198,14 +198,7 @@ log "🔍 Проверка образа:"
echo " docker run --rm $REPOSITORY:$VERSION /app/lcg --version"
echo ""
log "📝 Команды для использования:"
echo " kubectl apply -k kustomize"
echo " kubectl get pods"
echo " kubectl get services"
echo " kubectl get ingress"
echo " kubectl get hpa"
echo " kubectl get servicemonitor"
echo " kubectl get pods"
echo " kubectl get services"
echo " kubectl get ingress"
echo " kubectl get hpa"
echo " kubectl get servicemonitor"

View File

@@ -1 +1 @@
v2.0.5
v.2.0.17

View File

@@ -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 "Готово."

View File

@@ -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)
## 🔒 Безопасность

View File

@@ -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<BASE_PATH>/` 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:**

View File

@@ -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

View File

@@ -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
```
---

View File

@@ -5,7 +5,7 @@ metadata:
namespace: lcg
data:
# Основные настройки
LCG_VERSION: "v2.0.5"
LCG_VERSION: "v.2.0.17"
LCG_BASE_PATH: "/lcg"
LCG_SERVER_HOST: "0.0.0.0"
LCG_SERVER_PORT: "8080"
@@ -17,6 +17,7 @@ data:
LCG_NO_HISTORY: "false"
LCG_ALLOW_EXECUTION: "false"
LCG_DEBUG: "true"
LCG_PROVIDER: "proxy"
# Настройки аутентификации
LCG_SERVER_REQUIRE_AUTH: "true"

View File

@@ -5,7 +5,7 @@ metadata:
namespace: lcg
labels:
app: lcg
version: v2.0.5
version: v.2.0.17
spec:
replicas: 1
selector:
@@ -18,7 +18,7 @@ spec:
spec:
containers:
- name: lcg
image: kuznetcovay/lcg:v2.0.5
image: kuznetcovay/lcg:v.2.0.17
imagePullPolicy: Always
ports:
- containerPort: 8080

View File

@@ -15,11 +15,11 @@ resources:
# Common labels
# commonLabels:
# app: lcg
# version: v2.0.5
# version: v.2.0.17
# managed-by: kustomize
# Images
# images:
# - name: lcg
# newName: kuznetcovay/lcg
# newTag: v2.0.5
# newTag: v.2.0.17

147
main.go
View File

@@ -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 Файл истории результатов (по умолчанию: <result_folder>/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"),
@@ -156,12 +229,12 @@ lcg [опции] <описание команды>
Debug: c.Bool("debug"),
}
disableHistory = config.AppConfig.MainFlags.NoHistory || config.AppConfig.IsNoHistoryEnabled()
config.AppConfig.MainFlags.Debug = config.AppConfig.MainFlags.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
@@ -178,12 +251,18 @@ lcg [опции] <описание команды>
}
}
if CompileConditions.NoServe {
if CompileConditions.NoServe {
if len(args) > 1 && args[0] == "serve" {
printColored("❌ Error: serve command is disabled in this build\n", colorRed)
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
},
@@ -204,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{
{
@@ -343,6 +447,10 @@ func getCommands() []*cli.Command {
},
},
Action: func(c *cli.Context) error {
// Флаги приложения уже применены через глобальный Before hook
// Но применяем их еще раз на случай, если глобальный Before не сработал
applyAppFlagsToConfig(c)
if c.Bool("full") {
// Выводим полную конфигурацию в JSON формате
showFullConfig()
@@ -569,11 +677,9 @@ func getCommands() []*cli.Command {
host := c.String("host")
openBrowser := c.Bool("browser")
// Пробрасываем глобальный флаг debug для web-сервера
// Позволяет запускать: lcg -d serve -p ...
if c.Bool("debug") {
config.AppConfig.MainFlags.Debug = true
}
// Пробрасываем debug: флаг или переменная окружения LCG_DEBUG
// Позволяет запускать: LCG_DEBUG=1 lcg serve ... или lcg -d serve ...
config.AppConfig.MainFlags.Debug = c.Bool("debug") || config.GetEnvBool("LCG_DEBUG", false)
// Обновляем конфигурацию сервера с новыми параметрами
config.AppConfig.Server.Host = host
@@ -982,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 показывает краткую конфигурацию

View File

@@ -13,6 +13,13 @@ import (
"github.com/direct-dev-ru/linux-command-gpt/config"
)
const (
// CSRFTokenLifetimeHours минимальное время жизни CSRF токена в часах (не менее 12 часов)
CSRFTokenLifetimeHours = 12
// CSRFTokenLifetimeSeconds минимальное время жизни CSRF токена в секундах
CSRFTokenLifetimeSeconds = CSRFTokenLifetimeHours * 60 * 60
)
// CSRFManager управляет CSRF токенами
type CSRFManager struct {
secretKey []byte
@@ -113,13 +120,14 @@ func (c *CSRFManager) ValidateToken(token, userID string) bool {
return false
}
// Проверяем время жизни токена (24 часа)
// Проверяем время жизни токена (минимум 12 часов)
timestamp, err := parseInt64(timestampStr)
if err != nil {
return false
}
if time.Now().Unix()-timestamp > 24*60*60 {
// Минимальное время жизни токена: 12 часов (не менее 12 часов согласно требованиям)
if time.Now().Unix()-timestamp > CSRFTokenLifetimeSeconds {
return false
}
@@ -153,14 +161,15 @@ func GetCSRFTokenFromCookie(r *http.Request) string {
// setCSRFCookie устанавливает CSRF токен в cookie
func setCSRFCookie(w http.ResponseWriter, token string) {
// Минимальное время жизни токена: 12 часов (не менее 12 часов согласно требованиям)
cookie := &http.Cookie{
Name: "csrf_token",
Value: token,
Path: config.AppConfig.Server.CookiePath,
HttpOnly: true,
Secure: config.AppConfig.Server.CookieSecure,
SameSite: http.SameSiteLaxMode, // Более мягкий режим для reverse proxy
MaxAge: 1 * 60 * 60,
SameSite: http.SameSiteLaxMode, // Более мягкий режим для reverse proxy
MaxAge: CSRFTokenLifetimeSeconds, // Минимум 12 часов в секундах
}
// Добавляем домен если указан

View File

@@ -39,11 +39,12 @@ func generateAbbreviation(appName string) string {
// FileInfo содержит информацию о файле
type FileInfo struct {
Name string
Size string
ModTime string
Preview template.HTML
Content string // Полное содержимое для поиска
Name string
DisplayName string
Size string
ModTime string
Preview template.HTML
Content string // Полное содержимое для поиска
}
// handleResultsPage обрабатывает главную страницу со списком файлов
@@ -133,11 +134,12 @@ func getResultFiles() ([]FileInfo, error) {
}
files = append(files, FileInfo{
Name: entry.Name(),
Size: formatFileSize(info.Size()),
ModTime: info.ModTime().Format("02.01.2006 15:04"),
Preview: template.HTML(preview),
Content: fullContent,
Name: entry.Name(),
DisplayName: formatFileDisplayName(entry.Name()),
Size: formatFileSize(info.Size()),
ModTime: info.ModTime().Format("02.01.2006 15:04"),
Preview: template.HTML(preview),
Content: fullContent,
})
}
@@ -167,24 +169,91 @@ func formatFileSize(size int64) string {
return fmt.Sprintf("%.1f %cB", float64(size)/float64(div), "KMGTPE"[exp])
}
// formatFileDisplayName преобразует имя файла вида
// gpt_request_GigaChat-2-Max_2025-10-22_13-50-13.md
// в "Gpt Request GigaChat 2 Max — 2025-10-22 13:50:13"
func formatFileDisplayName(filename string) string {
name := strings.TrimSuffix(filename, ".md")
// Разделим на части по '_'
parts := strings.Split(name, "_")
if len(parts) == 0 {
return filename
}
// Первая часть может быть префиксом gpt/request — заменим '_' на пробел и приведем регистр
var words []string
for _, p := range parts {
if p == "" {
continue
}
// Заменяем '-' на пробел в словах модели/текста
p = strings.ReplaceAll(p, "-", " ")
// Разбиваем по пробелам и капитализуем каждое слово
for _, w := range strings.Fields(p) {
if w == "" {
continue
}
r := []rune(w)
r[0] = unicode.ToUpper(r[0])
words = append(words, string(r))
}
}
// Попробуем распознать хвост как дату и время
// Ищем шаблон YYYY-MM-DD_HH-MM-SS в исходном имени
var pretty string
// ожидаем последние две части — дата и время
if len(parts) >= 3 {
datePart := parts[len(parts)-2]
timePart := parts[len(parts)-1]
// заменить '-' в времени на ':'
timePretty := strings.ReplaceAll(timePart, "-", ":")
if len(datePart) == 10 && len(timePart) == 8 { // примитивная проверка
// Собираем текст до датных частей
text := strings.Join(words[:len(words)-2], " ")
pretty = strings.TrimSpace(text)
if pretty != "" {
pretty += " — " + datePart + " " + timePretty
} else {
pretty = datePart + " " + timePretty
}
return pretty
}
}
if len(words) > 0 {
pretty = strings.Join(words, " ")
return pretty
}
return filename
}
// handleFileView обрабатывает просмотр конкретного файла
func handleFileView(w http.ResponseWriter, r *http.Request) {
filename := strings.TrimPrefix(r.URL.Path, "/file/")
// Учитываем BasePath при извлечении имени файла
basePath := config.AppConfig.Server.BasePath
var filename string
if basePath != "" && basePath != "/" {
basePath = strings.TrimSuffix(basePath, "/")
filename = strings.TrimPrefix(r.URL.Path, basePath+"/file/")
} else {
filename = strings.TrimPrefix(r.URL.Path, "/file/")
}
if filename == "" {
http.NotFound(w, r)
renderNotFound(w, "Файл не указан", getBasePath())
return
}
// Проверяем, что файл существует и находится в папке результатов
filePath := filepath.Join(config.AppConfig.ResultFolder, filename)
if !strings.HasPrefix(filePath, config.AppConfig.ResultFolder) {
http.NotFound(w, r)
renderNotFound(w, "Запрошенный файл недоступен", getBasePath())
return
}
content, err := os.ReadFile(filePath)
if err != nil {
http.NotFound(w, r)
renderNotFound(w, "Файл не найден или был удален", getBasePath())
return
}
@@ -195,9 +264,11 @@ func handleFileView(w http.ResponseWriter, r *http.Request) {
data := struct {
Filename string
Content template.HTML
BasePath string
}{
Filename: filename,
Content: template.HTML(htmlContent),
BasePath: getBasePath(),
}
// Парсим и выполняем шаблон
@@ -221,22 +292,30 @@ func handleDeleteFile(w http.ResponseWriter, r *http.Request) {
return
}
filename := strings.TrimPrefix(r.URL.Path, "/delete/")
// Учитываем BasePath при извлечении имени файла
basePath := config.AppConfig.Server.BasePath
var filename string
if basePath != "" && basePath != "/" {
basePath = strings.TrimSuffix(basePath, "/")
filename = strings.TrimPrefix(r.URL.Path, basePath+"/delete/")
} else {
filename = strings.TrimPrefix(r.URL.Path, "/delete/")
}
if filename == "" {
http.NotFound(w, r)
renderNotFound(w, "Файл не указан", getBasePath())
return
}
// Проверяем, что файл существует и находится в папке результатов
filePath := filepath.Join(config.AppConfig.ResultFolder, filename)
if !strings.HasPrefix(filePath, config.AppConfig.ResultFolder) {
http.NotFound(w, r)
renderNotFound(w, "Запрошенный файл недоступен", getBasePath())
return
}
// Проверяем, что файл существует
if _, err := os.Stat(filePath); os.IsNotExist(err) {
http.NotFound(w, r)
renderNotFound(w, "Файл не найден или уже удален", getBasePath())
return
}

View File

@@ -3,11 +3,13 @@ package serve
import (
"crypto/tls"
"fmt"
"html/template"
"net/http"
"os"
"strings"
"github.com/direct-dev-ru/linux-command-gpt/config"
"github.com/direct-dev-ru/linux-command-gpt/serve/templates"
"github.com/direct-dev-ru/linux-command-gpt/ssl"
)
@@ -48,6 +50,18 @@ func StartResultServer(host, port string) error {
return fmt.Errorf("failed to initialize CSRF manager: %v", err)
}
// Гарантируем наличие папки результатов и файла истории
if _, err := os.Stat(config.AppConfig.ResultFolder); os.IsNotExist(err) {
if mkErr := os.MkdirAll(config.AppConfig.ResultFolder, 0755); mkErr != nil {
return fmt.Errorf("failed to create results folder: %v", mkErr)
}
}
if _, err := os.Stat(config.AppConfig.ResultHistory); os.IsNotExist(err) {
if writeErr := Write(config.AppConfig.ResultHistory, []HistoryEntry{}); writeErr != nil {
return fmt.Errorf("failed to create history file: %v", writeErr)
}
}
addr := fmt.Sprintf("%s:%s", host, port)
// Проверяем, нужно ли использовать HTTPS
@@ -138,15 +152,21 @@ func registerHTTPSRoutes() {
// Регистрируем все маршруты кроме главной страницы
registerRoutesExceptHome()
// Регистрируем главную страницу с проверкой HTTPS
// Регистрируем главную страницу (строго по BasePath) с проверкой HTTPS
http.HandleFunc(makePath("/"), func(w http.ResponseWriter, r *http.Request) {
// Проверяем, пришел ли запрос по HTTP (не HTTPS)
if r.TLS == nil {
handleHTTPSRedirect(w, r)
return
}
// Если уже HTTPS, обрабатываем как обычно
AuthMiddleware(handleResultsPage)(w, r)
// Обрабатываем только точные пути: BasePath или BasePath/
bp := getBasePath()
p := r.URL.Path
if (bp == "" && (p == "/" || p == "")) || (bp != "" && (p == bp || p == bp+"/")) {
AuthMiddleware(handleResultsPage)(w, r)
return
}
renderNotFound(w, "Страница не найдена", bp)
})
// Регистрируем главную страницу без слэша в конце для BasePath
@@ -163,6 +183,13 @@ func registerHTTPSRoutes() {
AuthMiddleware(handleResultsPage)(w, r)
})
}
// Catch-all 404 для любых незарегистрированных путей (только когда BasePath задан)
if getBasePath() != "" {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
renderNotFound(w, "Страница не найдена", getBasePath())
})
}
}
// registerRoutesExceptHome регистрирует все маршруты кроме главной страницы
@@ -203,6 +230,13 @@ func registerRoutesExceptHome() {
// API для сохранения результатов и истории
http.HandleFunc(makePath("/api/save-result"), AuthMiddleware(CSRFMiddleware(handleSaveResult)))
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())
// })
// }
}
// registerRoutes регистрирует все маршруты сервера
@@ -215,8 +249,17 @@ func registerRoutes() {
http.HandleFunc(makePath("/api/logout"), handleLogout)
http.HandleFunc(makePath("/api/validate-token"), handleValidateToken)
// Главная страница и файлы
http.HandleFunc(makePath("/"), AuthMiddleware(handleResultsPage))
// Главная страница (строго по BasePath) и файлы
http.HandleFunc(makePath("/"), func(w http.ResponseWriter, r *http.Request) {
// Обрабатываем только точные пути: BasePath или BasePath/
bp := getBasePath()
p := r.URL.Path
if (bp == "" && (p == "/" || p == "")) || (bp != "" && (p == bp || p == bp+"/")) {
AuthMiddleware(handleResultsPage)(w, r)
return
}
renderNotFound(w, "Страница не найдена", bp)
})
http.HandleFunc(makePath("/file/"), AuthMiddleware(handleFileView))
http.HandleFunc(makePath("/delete/"), AuthMiddleware(handleDeleteFile))
@@ -251,4 +294,30 @@ func registerRoutes() {
basePath = strings.TrimSuffix(basePath, "/")
http.HandleFunc(basePath, AuthMiddleware(handleResultsPage))
}
// Catch-all 404 для любых незарегистрированных путей (только когда BasePath задан)
if getBasePath() != "" {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
renderNotFound(w, "Страница не найдена", getBasePath())
})
}
}
// renderNotFound рендерит кастомную страницу 404
func renderNotFound(w http.ResponseWriter, message, basePath string) {
w.WriteHeader(http.StatusNotFound)
data := struct {
Message string
BasePath string
}{
Message: message,
BasePath: basePath,
}
tmpl, err := template.New("not_found").Parse(templates.NotFoundTemplate)
if err != nil {
http.Error(w, "404 Not Found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_ = tmpl.Execute(w, data)
}

View File

@@ -126,7 +126,7 @@ const FileViewTemplate = `
<div class="container">
<div class="header">
<h1>📄 {{.Filename}}</h1>
<a href="/" class="back-btn">← Назад к списку</a>
<a href="{{.BasePath}}/" class="back-btn">← Назад к списку</a>
</div>
<div class="content">
{{.Content}}

View File

@@ -111,19 +111,26 @@ const HistoryPageTemplate = `
font-size: 0.9em;
color: #2d5016;
border-left: 3px solid #2d5016;
max-height: 72px; /* ~4 строки */
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
}
.delete-btn {
background: #e74c3c;
color: white;
background: transparent;
color: #ef9a9a; /* бледно-красный */
border: none;
padding: 6px 12px;
padding: 2px 6px;
border-radius: 4px;
cursor: pointer;
font-size: 0.8em;
transition: background 0.3s ease;
font-size: 18px;
line-height: 1;
transition: color 0.2s ease, transform 0.2s ease;
}
.delete-btn:hover {
background: #c0392b;
color: rgb(171, 27, 24); /* ярче при ховере */
transform: translateY(-1px);
}
.empty-state {
text-align: center;
@@ -180,7 +187,7 @@ const HistoryPageTemplate = `
<span class="history-index">#{{.Index}}</span>
<span class="history-timestamp">{{.Timestamp}}</span>
</div>
<button class="delete-btn" onclick="event.stopPropagation(); deleteHistoryEntry({{.Index}})">🗑️ Удалить</button>
<button class="delete-btn" onclick="event.stopPropagation(); deleteHistoryEntry({{.Index}})"></button>
</div>
<div class="history-command">{{.Command}}</div>
<div class="history-response">{{.Response}}</div>

View File

@@ -0,0 +1,157 @@
package templates
// NotFoundTemplate современная страница 404
const NotFoundTemplate = `
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Страница не найдена — 404</title>
<style>
:root {
--bg: #1a0b0b; /* глубокий темно-красный фон */
--bg2: #2a0f0f; /* второй оттенок фона */
--fg: #ffeaea; /* светлый текст с красным оттенком */
--accent: #ff3b30; /* основной красный (iOS red) */
--accent2: #ff6f61; /* дополнительный коралловый */
--accentGlow: rgba(255,59,48,0.35);
--accentGlow2: rgba(255,111,97,0.30);
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
background:
radial-gradient(1200px 600px at 10% 10%, rgba(255,59,48,0.12), transparent),
radial-gradient(1200px 600px at 90% 90%, rgba(255,111,97,0.12), transparent),
linear-gradient(135deg, var(--bg), var(--bg2));
color: var(--fg);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Inter, sans-serif;
overflow: hidden;
}
.glow {
position: absolute;
inset: -20%;
background:
radial-gradient(700px 340px at 20% 30%, rgba(255,59,48,0.22), transparent 60%),
radial-gradient(700px 340px at 80% 70%, rgba(255,111,97,0.20), transparent 60%);
filter: blur(40px);
z-index: 0;
}
.card {
position: relative;
z-index: 1;
width: min(720px, 92vw);
padding: 32px;
border-radius: 20px;
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.08);
box-shadow: 0 10px 40px rgba(80,0,0,0.45), inset 0 0 0 1px rgba(255,255,255,0.03);
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;
font-weight: 800;
letter-spacing: -2px;
background: linear-gradient(90deg, var(--accent), var(--accent2));
-webkit-background-clip: text;
background-clip: text;
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);
font-weight: 600;
opacity: 0.95;
margin-bottom: 8px;
}
.desc {
font-size: 15px;
opacity: 0.75;
margin: 0 auto 20px auto;
max-width: 60ch;
}
.btns {
display: flex;
flex-wrap: wrap;
gap: 12px;
justify-content: center;
margin-top: 8px;
}
.btn {
appearance: none;
border: none;
cursor: pointer;
padding: 12px 18px;
border-radius: 12px;
font-weight: 600;
color: white;
background: linear-gradient(135deg, var(--accent), #c62828);
box-shadow: 0 6px 18px var(--accentGlow);
transition: transform .2s ease, box-shadow .2s ease, filter .2s ease;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 8px;
}
.btn:hover { transform: translateY(-2px); filter: brightness(1.05); }
.btn.secondary { background: linear-gradient(135deg, #e65100, var(--accent2)); box-shadow: 0 6px 18px var(--accentGlow2); }
.hint { margin-top: 16px; font-size: 13px; opacity: 0.6; }
</style>
<script>
function goHome() {
window.location.href = '{{.BasePath}}/';
}
function bindEsc() {
const handler = (e) => { if (e.key === 'Escape' || e.key === 'Esc') { e.preventDefault(); goHome(); } };
window.addEventListener('keydown', handler);
document.addEventListener('keydown', handler);
// фокус на body для гарантии получения клавиш
if (document && document.body) {
document.body.setAttribute('tabindex', '-1');
document.body.focus({ preventScroll: true });
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', bindEsc);
} else {
bindEsc();
}
</script>
<meta http-equiv="refresh" content="30">
<meta name="robots" content="noindex">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'><text y='50%' x='50%' dominant-baseline='middle' text-anchor='middle' font-size='42'>🚫</text></svg>">
</head>
<body>
<div class="glow"></div>
<div class="card">
<div class="code">404</div>
<div class="title">Страница не найдена</div>
<p class="desc">Такой страницы не существует. Вы можете вернуться на главную страницу или выполнить команду.</p>
<div class="btns">
<a class="btn" href="{{.BasePath}}/">🏠 На главную</a>
<a class="btn secondary" href="{{.BasePath}}/run">🚀 К выполнению</a>
</div>
<div class="hint">Нажмите Esc, чтобы вернуться на главную</div>
</div>
</body>
</html>
`

View File

@@ -73,8 +73,10 @@ const ResultsPageTemplate = `
}
.files-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
align-items: stretch;
grid-auto-rows: auto;
}
.file-card {
background: white;
@@ -91,32 +93,36 @@ const ResultsPageTemplate = `
}
.file-card-content {
cursor: pointer;
padding-left: 28px;
}
.file-actions {
position: absolute;
top: 10px;
right: 10px;
left: 10px;
display: flex;
gap: 8px;
}
.delete-btn {
background: #e74c3c;
color: white;
background: transparent;
color: #ef9a9a; /* бледно-красный */
border: none;
padding: 6px 12px;
padding: 4px 8px;
border-radius: 4px;
cursor: pointer;
font-size: 0.8em;
transition: background 0.3s ease;
font-size: 18px;
line-height: 1;
transition: color 0.2s ease, transform 0.2s ease;
}
.delete-btn:hover {
background: #c0392b;
color:rgb(171, 27, 24); /* чуть ярче при ховере */
transform: translateY(-1px);
}
.file-name {
font-weight: 600;
color: #333;
margin-bottom: 8px;
font-size: 1.1em;
padding-right: 10px;
}
.file-info {
color: #666;
@@ -165,16 +171,25 @@ const ResultsPageTemplate = `
body { padding: 10px; }
.container { margin: 0; border-radius: 8px; box-shadow: 0 10px 20px rgba(0,0,0,0.1); }
.header { padding: 20px; }
.header h1 { font-size: 2em; }
.header h1 { font-size: 1.9em; }
.content { padding: 20px; }
.files-grid { grid-template-columns: 1fr; }
.files-grid { dummy-attr: none; }
/* Стили карточек как в истории */
.file-card { background: #f0f8f0; border: 1px solid #a8e6cf; padding: 15px; }
.file-card:hover { border-color: #2d5016; box-shadow: 0 8px 25px rgba(45,80,22,0.2); transform: translateY(-2px); }
.file-name { color: #333; margin-bottom: 8px; }
.file-info { color: #666; font-size: 0.9em; }
.file-preview { background: #f8f9fa; border-left: 3px solid #2d5016; font-size: 0.85em; }
.file-actions { top: 8px; left: 8px; }
.delete-btn { padding: 2px 6px; font-size: 16px; }
.stats { grid-template-columns: 1fr 1fr; }
.nav-buttons { flex-direction: column; gap: 8px; }
.nav-btn, .nav-button { text-align: center; padding: 12px 16px; font-size: 14px; }
.search-container input { font-size: 16px; width: 96% !important; }
}
@media (max-width: 480px) {
.header h1 { font-size: 1.8em; }
.header h1 { font-size: 1.6em; }
.content { padding: 16px; }
.stats { grid-template-columns: 1fr; }
}
</style>
@@ -216,10 +231,10 @@ const ResultsPageTemplate = `
{{range .Files}}
<div class="file-card" data-content="{{.Content}}">
<div class="file-actions">
<button class="delete-btn" onclick="deleteFile('{{.Name}}')" title="Удалить файл">🗑️</button>
<button class="delete-btn" onclick="deleteFile('{{.Name}}')" title="Удалить файл"></button>
</div>
<div class="file-card-content" onclick="window.location.href='{{$.BasePath}}/file/{{.Name}}'">
<div class="file-name">{{.Name}}</div>
<div class="file-name">{{.DisplayName}}</div>
<div class="file-info">
📅 {{.ModTime}} | 📏 {{.Size}}
</div>