mirror of
https://github.com/Direct-Dev-Ru/go-lcg.git
synced 2025-11-16 09:39:56 +00:00
Compare commits
53 Commits
lcg.v2.0.1
...
v.2.0.19
| Author | SHA1 | Date | |
|---|---|---|---|
| 9365e0833c | |||
| 731784b420 | |||
| 57f51c5d0e | |||
| a78e1d24bf | |||
| d0b53607c4 | |||
| 487f3d484c | |||
| 814ca9ba7f | |||
| c975e00c50 | |||
| 01488edbee | |||
| 63876a393c | |||
| eb9a7dcf32 | |||
| 4779c4bca4 | |||
| 1e2ce929b2 | |||
| 9aa5aefdad | |||
| 49a41c597a | |||
| 7455987c0f | |||
| 93f60c4e36 | |||
| 3e143ee7a1 | |||
| 8cdb31d96d | |||
| 96a8060afb | |||
| a20fb846f0 | |||
| 99b1a74034 | |||
| 90cfc6fb0c | |||
| 89d15bfdc9 | |||
| 114146f4d2 | |||
| 5c672ecc39 | |||
| b4b902cb4c | |||
| 164f32dbaf | |||
| 7933abe62d | |||
| 1545fe2508 | |||
| d213de7a95 | |||
| deb80f2b37 | |||
| 81b01d74ae | |||
| 3c95eb85db | |||
| 1fbdd237a3 | |||
| 5b78e775c1 | |||
| 2d82b91090 | |||
| 9044b02d27 | |||
| 5d3829d1fe | |||
| e7c11879a1 | |||
| edadedcf80 | |||
| 6444c35bbb | |||
| 5ff6d4e072 | |||
| 6ec41355d3 | |||
| ffc2d6ba0a | |||
| 7a0d0746d4 | |||
| dab94df7d2 | |||
| 0da366cad5 | |||
| 281f7f877a | |||
| 3be2880dd2 | |||
| c70effda73 | |||
| 611bd17ac1 | |||
| e1bd79db8c |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -17,3 +17,6 @@ gpt_results
|
|||||||
shell-code/jwt.admin.token
|
shell-code/jwt.admin.token
|
||||||
run.sh
|
run.sh
|
||||||
lcg_history.json
|
lcg_history.json
|
||||||
|
deploy/0.create_sealed_secrets.sh
|
||||||
|
deploy/0.create_git_secrets.sh
|
||||||
|
deploy/0.create_app_secrets.sh
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
# This is an example .goreleaser.yml file with some sensible defaults.
|
|
||||||
# Make sure to check the documentation at https://goreleaser.com
|
|
||||||
|
|
||||||
# The lines below are called `modelines`. See `:help modeline`
|
|
||||||
# Feel free to remove those if you don't want/need to use them.
|
|
||||||
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
|
|
||||||
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
|
|
||||||
|
|
||||||
version: 2
|
|
||||||
|
|
||||||
before:
|
|
||||||
hooks:
|
|
||||||
# You may remove this if you don't use go modules.
|
|
||||||
- go mod tidy
|
|
||||||
# you may remove this if you don't need go generate
|
|
||||||
- go generate ./...
|
|
||||||
|
|
||||||
builds:
|
|
||||||
- env:
|
|
||||||
- CGO_ENABLED=0
|
|
||||||
goos:
|
|
||||||
- linux
|
|
||||||
- windows
|
|
||||||
- darwin
|
|
||||||
|
|
||||||
archives:
|
|
||||||
- formats: [tar.gz]
|
|
||||||
# this name template makes the OS and Arch compatible with the results of `uname`.
|
|
||||||
name_template: >-
|
|
||||||
{{ .ProjectName }}_
|
|
||||||
{{- title .Os }}_
|
|
||||||
{{- if eq .Arch "amd64" }}x86_64
|
|
||||||
{{- else if eq .Arch "386" }}i386
|
|
||||||
{{- else }}{{ .Arch }}{{ end }}
|
|
||||||
{{- if .Arm }}v{{ .Arm }}{{ end }}
|
|
||||||
# use zip for windows archives
|
|
||||||
format_overrides:
|
|
||||||
- goos: windows
|
|
||||||
formats: [zip]
|
|
||||||
|
|
||||||
changelog:
|
|
||||||
sort: asc
|
|
||||||
filters:
|
|
||||||
exclude:
|
|
||||||
- "^docs:"
|
|
||||||
- "^test:"
|
|
||||||
|
|
||||||
release:
|
|
||||||
footer: >-
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Released by [GoReleaser](https://github.com/goreleaser/goreleaser).
|
|
||||||
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"djlint.showInstallError": false
|
||||||
|
}
|
||||||
@@ -1,6 +1,36 @@
|
|||||||
CHANGELOG
|
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)
|
Версия 2.0.1 (2025-10-22)
|
||||||
=========================
|
=========================
|
||||||
|
|
||||||
|
|||||||
52
Dockerfiles/OllamaServer/.dockerignore
Normal file
52
Dockerfiles/OllamaServer/.dockerignore
Normal 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
15
Dockerfiles/OllamaServer/.gitignore
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Временные файлы
|
||||||
|
*.log
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
87
Dockerfiles/OllamaServer/Dockerfile
Normal file
87
Dockerfiles/OllamaServer/Dockerfile
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
# Используем готовый образ 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 TZ='Asia/Omsk'
|
||||||
|
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||||
|
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_DOMAIN="remote.ollama-server.ru"
|
||||||
|
ENV LCG_COOKIE_PATH="/lcg"
|
||||||
|
# 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 []
|
||||||
|
|
||||||
124
Dockerfiles/OllamaServer/Makefile
Normal file
124
Dockerfiles/OllamaServer/Makefile
Normal 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
|
||||||
|
|
||||||
185
Dockerfiles/OllamaServer/QUICKSTART.md
Normal file
185
Dockerfiles/OllamaServer/QUICKSTART.md
Normal 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
|
||||||
|
|
||||||
454
Dockerfiles/OllamaServer/README.md
Normal file
454
Dockerfiles/OllamaServer/README.md
Normal 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
|
||||||
143
Dockerfiles/OllamaServer/STRUCTURE.md
Normal file
143
Dockerfiles/OllamaServer/STRUCTURE.md
Normal 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.
|
||||||
55
Dockerfiles/OllamaServer/docker-compose.yml
Normal file
55
Dockerfiles/OllamaServer/docker-compose.yml
Normal 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
|
||||||
|
|
||||||
99
Dockerfiles/OllamaServer/entrypoint.sh
Executable file
99
Dockerfiles/OllamaServer/entrypoint.sh
Executable 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 "$@"
|
||||||
54
Dockerfiles/OllamaServer/podman-compose.yml
Normal file
54
Dockerfiles/OllamaServer/podman-compose.yml
Normal 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
|
||||||
|
|
||||||
@@ -1 +1 @@
|
|||||||
v2.0.1
|
v.2.0.19
|
||||||
|
|||||||
1
build-conditions.yaml
Normal file
1
build-conditions.yaml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
no-serve: false
|
||||||
58
build.sh
Executable file
58
build.sh
Executable file
@@ -0,0 +1,58 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 🚀 LCG Build Script (Root)
|
||||||
|
# Скрипт для сборки из корневой директории проекта
|
||||||
|
|
||||||
|
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 "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
success() {
|
||||||
|
echo -e "${GREEN}✅ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
warning() {
|
||||||
|
echo -e "${YELLOW}⚠️ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
error() {
|
||||||
|
echo -e "${RED}❌ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Параметры
|
||||||
|
REPOSITORY=${1:-"your-registry.com/lcg"}
|
||||||
|
VERSION=${2:-"latest"}
|
||||||
|
PLATFORMS=${3:-"linux/amd64,linux/arm64"}
|
||||||
|
|
||||||
|
log "🚀 Сборка LCG из корневой директории..."
|
||||||
|
|
||||||
|
# Проверяем, что мы в корневой директории
|
||||||
|
if [ ! -f "go.mod" ]; then
|
||||||
|
error "Этот скрипт должен запускаться из корневой директории проекта (где находится go.mod)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Записываем версию в файл VERSION.txt
|
||||||
|
echo "$VERSION" > VERSION.txt
|
||||||
|
log "📝 Версия записана в VERSION.txt: $VERSION"
|
||||||
|
|
||||||
|
# Запускаем полную сборку
|
||||||
|
log "🚀 Запуск полной сборки..."
|
||||||
|
./deploy/full-build.sh "$REPOSITORY" "$VERSION" "$PLATFORMS"
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
success "🎉 Сборка завершена успешно!"
|
||||||
|
else
|
||||||
|
error "Ошибка при сборке"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -20,9 +21,20 @@ type HistoryEntry struct {
|
|||||||
|
|
||||||
func read(historyPath string) ([]HistoryEntry, error) {
|
func read(historyPath string) ([]HistoryEntry, error) {
|
||||||
data, err := os.ReadFile(historyPath)
|
data, err := os.ReadFile(historyPath)
|
||||||
if err != nil || len(data) == 0 {
|
if err != nil {
|
||||||
|
// Если файл не существует, создаем пустой файл истории
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
emptyHistory := []HistoryEntry{}
|
||||||
|
if writeErr := write(historyPath, emptyHistory); writeErr != nil {
|
||||||
|
return nil, fmt.Errorf("не удалось создать файл истории: %v", writeErr)
|
||||||
|
}
|
||||||
|
return emptyHistory, nil
|
||||||
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if len(data) == 0 {
|
||||||
|
return []HistoryEntry{}, nil
|
||||||
|
}
|
||||||
var items []HistoryEntry
|
var items []HistoryEntry
|
||||||
if err := json.Unmarshal(data, &items); err != nil {
|
if err := json.Unmarshal(data, &items); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -50,6 +62,12 @@ func ShowHistory(historyPath string, printColored func(string, string), colorYel
|
|||||||
printColored("📝 История пуста\n", colorYellow)
|
printColored("📝 История пуста\n", colorYellow)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Сортируем записи по времени в убывающем порядке (новые сначала)
|
||||||
|
sort.Slice(items, func(i, j int) bool {
|
||||||
|
return items[i].Timestamp.After(items[j].Timestamp)
|
||||||
|
})
|
||||||
|
|
||||||
printColored("📝 История (из файла):\n", colorYellow)
|
printColored("📝 История (из файла):\n", colorYellow)
|
||||||
for _, h := range items {
|
for _, h := range items {
|
||||||
ts := h.Timestamp.Format("2006-01-02 15:04:05")
|
ts := h.Timestamp.Format("2006-01-02 15:04:05")
|
||||||
|
|||||||
127
config/config.go
127
config/config.go
@@ -3,6 +3,8 @@ package config
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -10,10 +12,12 @@ type Config struct {
|
|||||||
Cwd string
|
Cwd string
|
||||||
Host string
|
Host string
|
||||||
ProxyUrl string
|
ProxyUrl string
|
||||||
|
AppName string
|
||||||
Completions string
|
Completions string
|
||||||
Model string
|
Model string
|
||||||
Prompt string
|
Prompt string
|
||||||
ApiKeyFile string
|
ApiKeyFile string
|
||||||
|
ConfigFolder string
|
||||||
ResultFolder string
|
ResultFolder string
|
||||||
PromptFolder string
|
PromptFolder string
|
||||||
ProviderType string
|
ProviderType string
|
||||||
@@ -23,8 +27,10 @@ type Config struct {
|
|||||||
ResultHistory string
|
ResultHistory string
|
||||||
NoHistoryEnv string
|
NoHistoryEnv string
|
||||||
AllowExecution bool
|
AllowExecution bool
|
||||||
|
Query string
|
||||||
MainFlags MainFlags
|
MainFlags MainFlags
|
||||||
Server ServerConfig
|
Server ServerConfig
|
||||||
|
Validation ValidationConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
type MainFlags struct {
|
type MainFlags struct {
|
||||||
@@ -37,8 +43,39 @@ type MainFlags struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ServerConfig struct {
|
type ServerConfig struct {
|
||||||
Port string
|
Port string
|
||||||
Host string
|
Host string
|
||||||
|
HealthUrl string
|
||||||
|
ProxyUrl string
|
||||||
|
BasePath string
|
||||||
|
ConfigFolder string
|
||||||
|
AllowHTTP bool
|
||||||
|
SSLCertFile string
|
||||||
|
SSLKeyFile string
|
||||||
|
RequireAuth bool
|
||||||
|
Password string
|
||||||
|
Domain string
|
||||||
|
CookieSecure bool
|
||||||
|
CookiePath string
|
||||||
|
CookieTTLHours int
|
||||||
|
}
|
||||||
|
|
||||||
|
type ValidationConfig struct {
|
||||||
|
MaxSystemPromptLength int
|
||||||
|
MaxUserMessageLength int
|
||||||
|
MaxPromptNameLength int
|
||||||
|
MaxPromptDescLength int
|
||||||
|
MaxCommandLength int
|
||||||
|
MaxExplanationLength int
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetEnvBool(key string, defaultValue bool) bool {
|
||||||
|
if value, exists := os.LookupEnv(key); exists {
|
||||||
|
if boolValue, err := strconv.ParseBool(value); err == nil {
|
||||||
|
return boolValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
}
|
}
|
||||||
|
|
||||||
func getEnv(key, defaultValue string) string {
|
func getEnv(key, defaultValue string) string {
|
||||||
@@ -48,6 +85,33 @@ func getEnv(key, defaultValue string) string {
|
|||||||
return defaultValue
|
return defaultValue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getEnvInt(key string, defaultValue int) int {
|
||||||
|
if value, exists := os.LookupEnv(key); exists {
|
||||||
|
if intValue, err := strconv.Atoi(value); err == nil {
|
||||||
|
return intValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func getServerAllowHTTP() bool {
|
||||||
|
// Если переменная явно установлена, используем её
|
||||||
|
if value, exists := os.LookupEnv("LCG_SERVER_ALLOW_HTTP"); exists {
|
||||||
|
if boolValue, err := strconv.ParseBool(value); err == nil {
|
||||||
|
return boolValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если переменная не установлена, определяем по умолчанию на основе хоста
|
||||||
|
host := getEnv("LCG_SERVER_HOST", "localhost")
|
||||||
|
return isSecureHost(host)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSecureHost(host string) bool {
|
||||||
|
secureHosts := []string{"localhost", "127.0.0.1", "::1"}
|
||||||
|
return slices.Contains(secureHosts, host)
|
||||||
|
}
|
||||||
|
|
||||||
func Load() Config {
|
func Load() Config {
|
||||||
cwd, _ := os.Getwd()
|
cwd, _ := os.Getwd()
|
||||||
|
|
||||||
@@ -55,14 +119,21 @@ func Load() Config {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
homedir = cwd
|
homedir = cwd
|
||||||
}
|
}
|
||||||
os.MkdirAll(path.Join(homedir, ".config", "lcg", "gpt_results"), 0755)
|
privateResultsDir := path.Join(homedir, ".config", "lcg", "gpt_results")
|
||||||
resultFolder := getEnv("LCG_RESULT_FOLDER", path.Join(homedir, ".config", "lcg", "gpt_results"))
|
os.MkdirAll(privateResultsDir, 0700)
|
||||||
|
resultFolder := getEnv("LCG_RESULT_FOLDER", privateResultsDir)
|
||||||
|
|
||||||
os.MkdirAll(path.Join(homedir, ".config", "lcg", "gpt_sys_prompts"), 0755)
|
privatePromptsDir := path.Join(homedir, ".config", "lcg", "gpt_sys_prompts")
|
||||||
promptFolder := getEnv("LCG_PROMPT_FOLDER", path.Join(homedir, ".config", "lcg", "gpt_sys_prompts"))
|
os.MkdirAll(privatePromptsDir, 0700)
|
||||||
|
promptFolder := getEnv("LCG_PROMPT_FOLDER", privatePromptsDir)
|
||||||
|
|
||||||
|
privateConfigDir := path.Join(homedir, ".config", "lcg", "config")
|
||||||
|
os.MkdirAll(privateConfigDir, 0700)
|
||||||
|
configFolder := getEnv("LCG_CONFIG_FOLDER", privateConfigDir)
|
||||||
|
|
||||||
return Config{
|
return Config{
|
||||||
Cwd: cwd,
|
Cwd: cwd,
|
||||||
|
AppName: getEnv("LCG_APP_NAME", "Linux Command GPT"),
|
||||||
Host: getEnv("LCG_HOST", "http://192.168.87.108:11434/"),
|
Host: getEnv("LCG_HOST", "http://192.168.87.108:11434/"),
|
||||||
ProxyUrl: getEnv("LCG_PROXY_URL", "/api/v1/protected/sberchat/chat"),
|
ProxyUrl: getEnv("LCG_PROXY_URL", "/api/v1/protected/sberchat/chat"),
|
||||||
Completions: getEnv("LCG_COMPLETIONS_PATH", "api/chat"),
|
Completions: getEnv("LCG_COMPLETIONS_PATH", "api/chat"),
|
||||||
@@ -71,6 +142,7 @@ func Load() Config {
|
|||||||
ApiKeyFile: getEnv("LCG_API_KEY_FILE", ".openai_api_key"),
|
ApiKeyFile: getEnv("LCG_API_KEY_FILE", ".openai_api_key"),
|
||||||
ResultFolder: resultFolder,
|
ResultFolder: resultFolder,
|
||||||
PromptFolder: promptFolder,
|
PromptFolder: promptFolder,
|
||||||
|
ConfigFolder: configFolder,
|
||||||
ProviderType: getEnv("LCG_PROVIDER", "ollama"),
|
ProviderType: getEnv("LCG_PROVIDER", "ollama"),
|
||||||
JwtToken: getEnv("LCG_JWT_TOKEN", ""),
|
JwtToken: getEnv("LCG_JWT_TOKEN", ""),
|
||||||
PromptID: getEnv("LCG_PROMPT_ID", "1"),
|
PromptID: getEnv("LCG_PROMPT_ID", "1"),
|
||||||
@@ -79,8 +151,29 @@ func Load() Config {
|
|||||||
NoHistoryEnv: getEnv("LCG_NO_HISTORY", ""),
|
NoHistoryEnv: getEnv("LCG_NO_HISTORY", ""),
|
||||||
AllowExecution: isAllowExecutionEnabled(),
|
AllowExecution: isAllowExecutionEnabled(),
|
||||||
Server: ServerConfig{
|
Server: ServerConfig{
|
||||||
Port: getEnv("LCG_SERVER_PORT", "8080"),
|
Port: getEnv("LCG_SERVER_PORT", "8080"),
|
||||||
Host: getEnv("LCG_SERVER_HOST", "localhost"),
|
Host: getEnv("LCG_SERVER_HOST", "localhost"),
|
||||||
|
ConfigFolder: getEnv("LCG_CONFIG_FOLDER", path.Join(homedir, ".config", "lcg", "config")),
|
||||||
|
AllowHTTP: getServerAllowHTTP(),
|
||||||
|
SSLCertFile: getEnv("LCG_SERVER_SSL_CERT_FILE", ""),
|
||||||
|
SSLKeyFile: getEnv("LCG_SERVER_SSL_KEY_FILE", ""),
|
||||||
|
RequireAuth: isServerRequireAuth(),
|
||||||
|
Password: getEnv("LCG_SERVER_PASSWORD", "admin#123456"),
|
||||||
|
Domain: getEnv("LCG_DOMAIN", getEnv("LCG_SERVER_HOST", "localhost")),
|
||||||
|
CookieSecure: isCookieSecure(),
|
||||||
|
CookiePath: getEnv("LCG_COOKIE_PATH", "/lcg"),
|
||||||
|
CookieTTLHours: getEnvInt("LCG_COOKIE_TTL_HOURS", 168), // 7 дней по умолчанию
|
||||||
|
BasePath: getEnv("LCG_BASE_URL", "/lcg"),
|
||||||
|
HealthUrl: getEnv("LCG_HEALTH_URL", "/api/v1/protected/sberchat/health"),
|
||||||
|
ProxyUrl: getEnv("LCG_PROXY_URL", "/api/v1/protected/sberchat/chat"),
|
||||||
|
},
|
||||||
|
Validation: ValidationConfig{
|
||||||
|
MaxSystemPromptLength: getEnvInt("LCG_MAX_SYSTEM_PROMPT_LENGTH", 2000),
|
||||||
|
MaxUserMessageLength: getEnvInt("LCG_MAX_USER_MESSAGE_LENGTH", 4000),
|
||||||
|
MaxPromptNameLength: getEnvInt("LCG_MAX_PROMPT_NAME_LENGTH", 2000),
|
||||||
|
MaxPromptDescLength: getEnvInt("LCG_MAX_PROMPT_DESC_LENGTH", 5000),
|
||||||
|
MaxCommandLength: getEnvInt("LCG_MAX_COMMAND_LENGTH", 8000),
|
||||||
|
MaxExplanationLength: getEnvInt("LCG_MAX_EXPLANATION_LENGTH", 20000),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -103,6 +196,24 @@ func isAllowExecutionEnabled() bool {
|
|||||||
return vLower == "1" || vLower == "true"
|
return vLower == "1" || vLower == "true"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isServerRequireAuth() bool {
|
||||||
|
v := strings.TrimSpace(getEnv("LCG_SERVER_REQUIRE_AUTH", ""))
|
||||||
|
if v == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
vLower := strings.ToLower(v)
|
||||||
|
return vLower == "1" || vLower == "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
func isCookieSecure() bool {
|
||||||
|
v := strings.TrimSpace(getEnv("LCG_COOKIE_SECURE", ""))
|
||||||
|
if v == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
vLower := strings.ToLower(v)
|
||||||
|
return vLower == "1" || vLower == "true"
|
||||||
|
}
|
||||||
|
|
||||||
var AppConfig Config
|
var AppConfig Config
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
32
deploy/.goreleaser.yaml
Normal file
32
deploy/.goreleaser.yaml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Goreleaser configuration version 2
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
builds:
|
||||||
|
- id: lcg
|
||||||
|
binary: "lcg_{{ .Version }}"
|
||||||
|
goos:
|
||||||
|
- linux
|
||||||
|
- windows
|
||||||
|
- darwin
|
||||||
|
goarch:
|
||||||
|
- amd64
|
||||||
|
- arm64
|
||||||
|
env:
|
||||||
|
- CGO_ENABLED=0
|
||||||
|
ldflags:
|
||||||
|
- -s -w
|
||||||
|
- -X main.version={{.Version}}
|
||||||
|
- -X main.commit={{.Commit}}
|
||||||
|
- -X main.date={{.Date}}
|
||||||
|
main: .
|
||||||
|
dir: .
|
||||||
|
|
||||||
|
archives:
|
||||||
|
- id: lcg
|
||||||
|
ids:
|
||||||
|
- lcg
|
||||||
|
formats:
|
||||||
|
- binary
|
||||||
|
name_template: "{{ .Binary }}_{{ .Os }}_{{ .Arch }}"
|
||||||
|
files:
|
||||||
|
- "lcg_{{ .Version }}"
|
||||||
21
deploy/0.create_secrets.example.sh
Normal file
21
deploy/0.create_secrets.example.sh
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# https://dev.to/ashokan/sealed-secrets-the-secret-sauce-for-managing-secrets-2hg6
|
||||||
|
# head -c 64 /dev/urandom | base64 -w 0
|
||||||
|
export KUBECONFIG=/home/su/.kube/config_hlab
|
||||||
|
|
||||||
|
kubectl create secret generic lcg-secrets -n lcg \
|
||||||
|
--from-literal=LCG_SERVER_PASSWORDL= \
|
||||||
|
--from-literal=LCG_CSRF_SECRET=\
|
||||||
|
--from-literal=LCG_JWT_SECRET=\
|
||||||
|
--from-literal=LCG_JWT_TOKEN=\
|
||||||
|
--dry-run=client -o yaml | tee secret-cfg.yaml
|
||||||
|
|
||||||
|
kubeseal --controller-name=sealed-secrets-controller --controller-namespace=kube-system -o yaml <secret-cfg.yaml | tee sealed-cfg.yaml
|
||||||
|
|
||||||
|
rm -f secret-cfg.yaml
|
||||||
|
|
||||||
|
kubectl apply -f sealed-cfg.yaml
|
||||||
|
cp sealed-cfg.yaml ../kustomize/secret.yaml
|
||||||
|
|
||||||
|
kubectl get secret lcg-secrets -n lcg -o json | jq ".data | map_values(@base64d)"
|
||||||
7
deploy/0.namespace.yaml
Normal file
7
deploy/0.namespace.yaml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: lcg
|
||||||
|
labels:
|
||||||
|
name: lcg
|
||||||
|
app: linux-command-gpt
|
||||||
45
deploy/1.configmap.tmpl.yaml
Normal file
45
deploy/1.configmap.tmpl.yaml
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: lcg-config
|
||||||
|
namespace: lcg
|
||||||
|
data:
|
||||||
|
# Основные настройки
|
||||||
|
LCG_VERSION: "${VERSION}"
|
||||||
|
LCG_BASE_PATH: "/lcg"
|
||||||
|
LCG_SERVER_HOST: "0.0.0.0"
|
||||||
|
LCG_SERVER_PORT: "8080"
|
||||||
|
LCG_SERVER_ALLOW_HTTP: "true"
|
||||||
|
LCG_APP_NAME: "Linux Command GPT"
|
||||||
|
LCG_RESULT_FOLDER: "/app/data/results"
|
||||||
|
LCG_PROMPT_FOLDER: "/app/data/prompts"
|
||||||
|
LCG_CONFIG_FOLDER: "/app/data/config"
|
||||||
|
LCG_NO_HISTORY: "false"
|
||||||
|
LCG_ALLOW_EXECUTION: "false"
|
||||||
|
LCG_DEBUG: "true"
|
||||||
|
LCG_PROVIDER: "proxy"
|
||||||
|
|
||||||
|
# Настройки аутентификации
|
||||||
|
LCG_SERVER_REQUIRE_AUTH: "true"
|
||||||
|
|
||||||
|
LCG_COOKIE_SECURE: "true"
|
||||||
|
LCG_COOKIE_TTL_HOURS: "168"
|
||||||
|
LCG_DOMAIN: "direct-dev.ru"
|
||||||
|
LCG_COOKIE_PATH: "/lcg"
|
||||||
|
|
||||||
|
# Настройки провайдера (по умолчанию)
|
||||||
|
LCG_PROVIDER_TYPE: "proxy"
|
||||||
|
LCG_HOST: "https://direct-dev.ru"
|
||||||
|
LCG_HEALTH_URL: "/api/v1/protected/sberchat/health"
|
||||||
|
LCG_PROXY_URL: "/api/v1/protected/sberchat/chat"
|
||||||
|
LCG_MODEL: "GigaChat-2-Max"
|
||||||
|
|
||||||
|
# Настройки валидации
|
||||||
|
LCG_MAX_SYSTEM_PROMPT_LENGTH: "2000"
|
||||||
|
LCG_MAX_USER_MESSAGE_LENGTH: "4000"
|
||||||
|
LCG_MAX_PROMPT_NAME_LENGTH: "2000"
|
||||||
|
LCG_MAX_PROMPT_DESC_LENGTH: "50000"
|
||||||
|
|
||||||
|
# Настройки таймаутов
|
||||||
|
LCG_TIMEOUT: "300"
|
||||||
|
|
||||||
77
deploy/2.lcg-flux.yaml
Normal file
77
deploy/2.lcg-flux.yaml
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
apiVersion: source.toolkit.fluxcd.io/v1
|
||||||
|
kind: GitRepository
|
||||||
|
metadata:
|
||||||
|
name: go-lcg
|
||||||
|
namespace: flux-system
|
||||||
|
spec:
|
||||||
|
interval: 3m
|
||||||
|
url: https://github.com/Direct-Dev-Ru/go-lcg.git
|
||||||
|
ref:
|
||||||
|
branch: release
|
||||||
|
secretRef:
|
||||||
|
name: git-secrets
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# apiVersion: source.toolkit.fluxcd.io/v1
|
||||||
|
# kind: GitRepository
|
||||||
|
# metadata:
|
||||||
|
# name: linux-command-gpt
|
||||||
|
# namespace: flux-system
|
||||||
|
# spec:
|
||||||
|
# interval: 3m
|
||||||
|
# url: https://direct-dev.ru/gitea/GiteaAdmin/go-lcg.git
|
||||||
|
# ref:
|
||||||
|
# branch: release
|
||||||
|
# secretRef:
|
||||||
|
# name: gitea-token
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: kustomize.toolkit.fluxcd.io/v1
|
||||||
|
kind: Kustomization
|
||||||
|
metadata:
|
||||||
|
name: lcg
|
||||||
|
namespace: flux-system
|
||||||
|
spec:
|
||||||
|
healthChecks:
|
||||||
|
- kind: Deployment
|
||||||
|
name: lcg
|
||||||
|
namespace: lcg
|
||||||
|
interval: 3m15s
|
||||||
|
path: ./kustomize
|
||||||
|
prune: true
|
||||||
|
sourceRef:
|
||||||
|
kind: GitRepository
|
||||||
|
name: go-lcg
|
||||||
|
targetNamespace: lcg
|
||||||
|
timeout: 2m0s
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
apiVersion: image.toolkit.fluxcd.io/v1beta2
|
||||||
|
kind: ImageRepository
|
||||||
|
metadata:
|
||||||
|
name: lcg
|
||||||
|
namespace: flux-system
|
||||||
|
spec:
|
||||||
|
image: kuznetcovay/lcg
|
||||||
|
interval: 3m
|
||||||
|
secretRef:
|
||||||
|
name: regcred
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
apiVersion: image.toolkit.fluxcd.io/v1beta2
|
||||||
|
kind: ImagePolicy
|
||||||
|
metadata:
|
||||||
|
name: lcg
|
||||||
|
namespace: flux-system
|
||||||
|
spec:
|
||||||
|
imageRepositoryRef:
|
||||||
|
name: lcg
|
||||||
|
policy:
|
||||||
|
semver:
|
||||||
|
range: '>=1.0.0'
|
||||||
|
|
||||||
|
---
|
||||||
91
deploy/3.deployment.tmpl.yaml
Normal file
91
deploy/3.deployment.tmpl.yaml
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: lcg
|
||||||
|
namespace: lcg
|
||||||
|
labels:
|
||||||
|
app: lcg
|
||||||
|
version: ${VERSION}
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: lcg
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: lcg
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: lcg
|
||||||
|
image: ${REPOSITORY}:${VERSION}
|
||||||
|
imagePullPolicy: Always
|
||||||
|
ports:
|
||||||
|
- containerPort: 8080
|
||||||
|
protocol: TCP
|
||||||
|
envFrom:
|
||||||
|
- configMapRef:
|
||||||
|
name: lcg-config
|
||||||
|
- secretRef:
|
||||||
|
name: lcg-secrets
|
||||||
|
env:
|
||||||
|
# Pod information
|
||||||
|
- name: POD_NAME
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: metadata.name
|
||||||
|
- name: POD_NAMESPACE
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: metadata.namespace
|
||||||
|
- name: NODE_NAME
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: spec.nodeName
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpu: 1000m
|
||||||
|
memory: 1Gi
|
||||||
|
requests:
|
||||||
|
cpu: 250m
|
||||||
|
memory: 512Mi
|
||||||
|
volumeMounts:
|
||||||
|
- name: lcg-data
|
||||||
|
mountPath: /app/data
|
||||||
|
- name: lcg-config
|
||||||
|
mountPath: /app/config
|
||||||
|
readOnly: true
|
||||||
|
# Health checks
|
||||||
|
startupProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: 8080
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 5
|
||||||
|
failureThreshold: 30
|
||||||
|
readinessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: 8080
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 10
|
||||||
|
livenessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: 8080
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 60
|
||||||
|
volumes:
|
||||||
|
- name: lcg-data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: lcg-data
|
||||||
|
- name: lcg-config
|
||||||
|
configMap:
|
||||||
|
name: lcg-config
|
||||||
|
# Security context
|
||||||
|
securityContext:
|
||||||
|
runAsNonRoot: true
|
||||||
|
runAsUser: 1001
|
||||||
|
readOnlyRootFilesystem: false
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
capabilities:
|
||||||
|
drop:
|
||||||
|
- ALL
|
||||||
|
restartPolicy: Always
|
||||||
114
deploy/4.build-binaries.sh
Executable file
114
deploy/4.build-binaries.sh
Executable file
@@ -0,0 +1,114 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 🚀 LCG Binary Build Script
|
||||||
|
# Скрипт для сборки бинарных файлов с помощью goreleaser на хосте
|
||||||
|
|
||||||
|
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 "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
success() {
|
||||||
|
echo -e "${GREEN}✅ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
warning() {
|
||||||
|
echo -e "${YELLOW}⚠️ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
error() {
|
||||||
|
echo -e "${RED}❌ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Параметры
|
||||||
|
VERSION=${1:-"dev"}
|
||||||
|
# CLEAN=${2:-"true"}
|
||||||
|
|
||||||
|
# Записываем версию в файл VERSION.txt (в корневой директории проекта)
|
||||||
|
echo "$VERSION" > VERSION.txt
|
||||||
|
log "📝 Версия записана в VERSION.txt: $VERSION"
|
||||||
|
|
||||||
|
log "🚀 Сборка бинарных файлов LCG с goreleaser..."
|
||||||
|
|
||||||
|
# Проверяем наличие goreleaser
|
||||||
|
if ! command -v goreleaser &> /dev/null; then
|
||||||
|
error "goreleaser не найден. Установите goreleaser:"
|
||||||
|
echo " curl -sL https://github.com/goreleaser/goreleaser/releases/latest/download/goreleaser_Linux_x86_64.tar.gz | tar -xz -C /usr/local/bin goreleaser"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Проверяем наличие Go
|
||||||
|
if ! command -v go &> /dev/null; then
|
||||||
|
error "Go не найден. Установите Go для сборки."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Переходим в корневую директорию проекта
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
log "📁 Рабочая директория: $(pwd)"
|
||||||
|
log "📁 Папка dist будет создана в: $(pwd)/dist"
|
||||||
|
|
||||||
|
# Очищаем предыдущие сборки если нужно
|
||||||
|
# if [ "$CLEAN" = "true" ]; then
|
||||||
|
# log "🧹 Очистка предыдущих сборок..."
|
||||||
|
# rm -rf dist/
|
||||||
|
# goreleaser clean
|
||||||
|
# fi
|
||||||
|
|
||||||
|
# Проверяем наличие .goreleaser.yaml
|
||||||
|
if [ ! -f "deploy/.goreleaser.yaml" ]; then
|
||||||
|
error "Файл .goreleaser.yaml не найден в папке deploy/"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Копируем конфигурацию goreleaser в корень проекта
|
||||||
|
log "📋 Копирование конфигурации goreleaser..."
|
||||||
|
cp deploy/.goreleaser.yaml .goreleaser.yaml
|
||||||
|
|
||||||
|
# Устанавливаем переменные окружения для версии
|
||||||
|
export GORELEASER_CURRENT_TAG="$VERSION"
|
||||||
|
|
||||||
|
# Собираем бинарные файлы
|
||||||
|
log "🏗️ Сборка бинарных файлов для всех платформ..."
|
||||||
|
goreleaser build --snapshot --clean
|
||||||
|
|
||||||
|
# Проверяем результат
|
||||||
|
if [ -d "dist" ]; then
|
||||||
|
log "📊 Собранные бинарные файлы:"
|
||||||
|
find dist -name "lcg_*" -type f | while read -r binary; do
|
||||||
|
echo " $binary ($(stat -c%s "$binary") bytes, $(file "$binary" | cut -d: -f2))"
|
||||||
|
done
|
||||||
|
|
||||||
|
success "🎉 Бинарные файлы успешно собраны!"
|
||||||
|
|
||||||
|
# Показываем структуру dist/
|
||||||
|
log "📁 Структура папки dist/:"
|
||||||
|
tree -h dist/ 2>/dev/null || find dist -type f | sort
|
||||||
|
|
||||||
|
else
|
||||||
|
error "Папка dist/ не создана. Проверьте конфигурацию goreleaser."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Очищаем временный файл конфигурации
|
||||||
|
rm -f .goreleaser.yaml
|
||||||
|
|
||||||
|
success "🎉 Сборка бинарных файлов завершена!"
|
||||||
|
|
||||||
|
# Показываем команды для Docker сборки
|
||||||
|
echo ""
|
||||||
|
log "📝 Следующие шаги:"
|
||||||
|
echo " cd deploy"
|
||||||
|
echo " docker buildx build --platform linux/amd64,linux/arm64 --tag your-registry.com/lcg:$VERSION --push ."
|
||||||
|
echo " # или используйте скрипт:"
|
||||||
|
echo " ./5.build-docker.sh your-registry.com/lcg $VERSION"
|
||||||
12
deploy/4.pvc.yaml
Normal file
12
deploy/4.pvc.yaml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: lcg-data
|
||||||
|
namespace: lcg
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 5Gi
|
||||||
|
storageClassName: nfs
|
||||||
183
deploy/5.build-docker.sh
Executable file
183
deploy/5.build-docker.sh
Executable file
@@ -0,0 +1,183 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 🐳 LCG Docker Build Script
|
||||||
|
# Скрипт для сборки Docker образа с предварительно собранными бинарными файлами
|
||||||
|
|
||||||
|
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 "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
success() {
|
||||||
|
echo -e "${GREEN}✅ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
warning() {
|
||||||
|
echo -e "${YELLOW}⚠️ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
error() {
|
||||||
|
echo -e "${RED}❌ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Параметры
|
||||||
|
REPOSITORY=${1:-"your-registry.com/lcg"}
|
||||||
|
VERSION=${2:-""}
|
||||||
|
PLATFORMS=${3:-"linux/amd64,linux/arm64"}
|
||||||
|
|
||||||
|
if [ -z "$VERSION" ]; then
|
||||||
|
error "Версия не указана! Использование: $0 <repository> <version>"
|
||||||
|
echo "Пример: $0 your-registry.com/lcg v1.0.0 <platforms>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "🐳 Сборка Docker образа LCG..."
|
||||||
|
|
||||||
|
# Проверяем наличие docker
|
||||||
|
if ! command -v docker &> /dev/null; then
|
||||||
|
error "Docker не найден. Установите Docker для сборки образов."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Проверяем наличие docker buildx
|
||||||
|
if ! docker buildx version &> /dev/null; then
|
||||||
|
error "Docker Buildx не найден. Установите Docker Buildx для мультиплатформенной сборки."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Проверяем наличие бинарных файлов в текущей директории (если запускаем из корня)
|
||||||
|
if [ ! -d "dist" ]; then
|
||||||
|
error "Папка dist/ не найдена. Сначала соберите бинарные файлы:"
|
||||||
|
echo " ./deploy/4.build-binaries.sh $VERSION"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Проверяем наличие бинарных файлов для всех платформ
|
||||||
|
MISSING_BINARIES=()
|
||||||
|
|
||||||
|
# Ищем бинарные файлы с версией в имени
|
||||||
|
AMD64_BINARY=$(find dist -name "*linux_amd64*" -type d | head -1)
|
||||||
|
echo "AMD64_BINARY: $AMD64_BINARY"
|
||||||
|
ARM64_BINARY=$(find dist -name "*linux_arm64*" -type d | head -1)
|
||||||
|
echo "ARM64_BINARY: $ARM64_BINARY"
|
||||||
|
|
||||||
|
# Проверяем наличие бинарных файлов в найденных папках и соответствие версии
|
||||||
|
if [ -n "$AMD64_BINARY" ]; then
|
||||||
|
AMD64_FILE=$(find "$AMD64_BINARY" -name "lcg_*" -type f | head -1)
|
||||||
|
if [ -z "$AMD64_FILE" ]; then
|
||||||
|
AMD64_BINARY=""
|
||||||
|
else
|
||||||
|
# Извлекаем версию из имени файла
|
||||||
|
FILE_VERSION=$(basename "$AMD64_FILE" | sed 's/lcg_//' | sed 's/-SNAPSHOT.*//')
|
||||||
|
# Нормализуем версии для сравнения (убираем префикс 'v' если есть)
|
||||||
|
NORMALIZED_FILE_VERSION=$(echo "$FILE_VERSION" | sed 's/^v//')
|
||||||
|
NORMALIZED_VERSION=$(echo "$VERSION" | sed 's/^v//')
|
||||||
|
if [ "$NORMALIZED_FILE_VERSION" != "$NORMALIZED_VERSION" ]; then
|
||||||
|
error "Версия в имени бинарного файла ($FILE_VERSION) не совпадает с переданной версией ($VERSION)"
|
||||||
|
echo "Файл: $AMD64_FILE"
|
||||||
|
echo "Ожидаемая версия: $VERSION"
|
||||||
|
echo "Версия в файле: $FILE_VERSION"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$ARM64_BINARY" ]; then
|
||||||
|
ARM64_FILE=$(find "$ARM64_BINARY" -name "lcg_*" -type f | head -1)
|
||||||
|
if [ -z "$ARM64_FILE" ]; then
|
||||||
|
ARM64_BINARY=""
|
||||||
|
else
|
||||||
|
# Извлекаем версию из имени файла
|
||||||
|
FILE_VERSION=$(basename "$ARM64_FILE" | sed 's/lcg_//' | sed 's/-SNAPSHOT.*//')
|
||||||
|
# Нормализуем версии для сравнения (убираем префикс 'v' если есть)
|
||||||
|
NORMALIZED_FILE_VERSION=$(echo "$FILE_VERSION" | sed 's/^v//')
|
||||||
|
NORMALIZED_VERSION=$(echo "$VERSION" | sed 's/^v//')
|
||||||
|
if [ "$NORMALIZED_FILE_VERSION" != "$NORMALIZED_VERSION" ]; then
|
||||||
|
error "Версия в имени бинарного файла ($FILE_VERSION) не совпадает с переданной версией ($VERSION)"
|
||||||
|
echo "Файл: $ARM64_FILE"
|
||||||
|
echo "Ожидаемая версия: $VERSION"
|
||||||
|
echo "Версия в файле: $FILE_VERSION"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$AMD64_BINARY" ]; then
|
||||||
|
MISSING_BINARIES+=("linux/amd64")
|
||||||
|
fi
|
||||||
|
if [ -z "$ARM64_BINARY" ]; then
|
||||||
|
MISSING_BINARIES+=("linux/arm64")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ${#MISSING_BINARIES[@]} -gt 0 ]; then
|
||||||
|
error "Отсутствуют бинарные файлы для платформ: ${MISSING_BINARIES[*]}"
|
||||||
|
echo "Сначала соберите бинарные файлы:"
|
||||||
|
echo " ./4.build-binaries.sh $VERSION"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Показываем найденные файлы и их версии
|
||||||
|
log "📊 Найденные бинарные файлы:"
|
||||||
|
if [ -n "$AMD64_FILE" ]; then
|
||||||
|
echo " AMD64: $AMD64_FILE"
|
||||||
|
fi
|
||||||
|
if [ -n "$ARM64_FILE" ]; then
|
||||||
|
echo " ARM64: $ARM64_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Создаем builder если не существует
|
||||||
|
log "🔧 Настройка Docker Buildx..."
|
||||||
|
docker buildx create --name lcg-builder --use 2>/dev/null || docker buildx use lcg-builder
|
||||||
|
|
||||||
|
# Копируем бинарные файлы и файл версии в папку deploy
|
||||||
|
log "📋 Копирование бинарных файлов и файла версии..."
|
||||||
|
cp -r dist ./deploy/dist
|
||||||
|
cp VERSION.txt ./deploy/VERSION.txt 2>/dev/null || echo "dev" > ./deploy/VERSION.txt
|
||||||
|
|
||||||
|
# Сборка для всех платформ
|
||||||
|
log "🏗️ Сборка образа для платформ: $PLATFORMS"
|
||||||
|
log "📦 Репозиторий: $REPOSITORY"
|
||||||
|
log "🏷️ Версия: $VERSION"
|
||||||
|
|
||||||
|
# Сборка и push
|
||||||
|
docker buildx build \
|
||||||
|
--platform "$PLATFORMS" \
|
||||||
|
--tag "$REPOSITORY:$VERSION" \
|
||||||
|
--tag "$REPOSITORY:latest" \
|
||||||
|
--push \
|
||||||
|
--file deploy/Dockerfile \
|
||||||
|
deploy/
|
||||||
|
|
||||||
|
# Очищаем скопированные файлы
|
||||||
|
rm -rf ./deploy/dist
|
||||||
|
|
||||||
|
success "🎉 Образ успешно собран и отправлен в репозиторий!"
|
||||||
|
|
||||||
|
# Показываем информацию о собранном образе
|
||||||
|
log "📊 Информация о собранном образе:"
|
||||||
|
echo " Репозиторий: $REPOSITORY"
|
||||||
|
echo " Версия: $VERSION"
|
||||||
|
echo " Платформы: $PLATFORMS"
|
||||||
|
echo " Теги: $REPOSITORY:$VERSION, $REPOSITORY:latest"
|
||||||
|
|
||||||
|
# Проверяем образы в репозитории
|
||||||
|
log "🔍 Проверка образов в репозитории..."
|
||||||
|
docker buildx imagetools inspect "$REPOSITORY:$VERSION" || warning "Не удалось проверить образ в репозитории"
|
||||||
|
|
||||||
|
success "🎉 Сборка завершена успешно!"
|
||||||
|
|
||||||
|
# Показываем команды для использования
|
||||||
|
echo ""
|
||||||
|
log "📝 Полезные команды:"
|
||||||
|
echo " docker pull $REPOSITORY:$VERSION"
|
||||||
|
echo " docker run -p 8080:8080 $REPOSITORY:$VERSION"
|
||||||
|
echo " docker buildx imagetools inspect $REPOSITORY:$VERSION"
|
||||||
204
deploy/6.full-build.sh
Executable file
204
deploy/6.full-build.sh
Executable file
@@ -0,0 +1,204 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 🚀 LCG Full Build Script
|
||||||
|
# Полный скрипт сборки: бинарные файлы + Docker образ
|
||||||
|
|
||||||
|
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 "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
success() {
|
||||||
|
echo -e "${GREEN}✅ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
warning() {
|
||||||
|
echo -e "${YELLOW}⚠️ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
error() {
|
||||||
|
echo -e "${RED}❌ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Параметры
|
||||||
|
|
||||||
|
REPOSITORY=${1:-"kuznetcovay/lcg"}
|
||||||
|
VERSION=${2:-""}
|
||||||
|
PLATFORMS=${3:-"linux/amd64,linux/arm64"}
|
||||||
|
|
||||||
|
if [ -z "$VERSION" ]; then
|
||||||
|
error "Версия не указана! Использование: $0 <repository> <version> <platforms>"
|
||||||
|
echo "Пример: $0 kuznetcovay/lcg v1.0.0 linux/amd64,linux/arm64"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Записываем версию в файл VERSION.txt (в корневой директории проекта)
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
echo "$VERSION" > VERSION.txt
|
||||||
|
log "📝 Версия записана в VERSION.txt: $VERSION"
|
||||||
|
|
||||||
|
log "🚀 Полная сборка LCG (бинарные файлы + Docker образ)..."
|
||||||
|
|
||||||
|
# Этап 1: Сборка бинарных файлов
|
||||||
|
log "📦 Этап 1: Сборка бинарных файлов с goreleaser..."
|
||||||
|
if ! ./deploy/4.build-binaries.sh "$VERSION"; then
|
||||||
|
error "Ошибка при сборке бинарных файлов"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
success "✅ Бинарные файлы собраны успешно"
|
||||||
|
|
||||||
|
# Этап 2: Сборка Docker образа
|
||||||
|
log "🐳 Этап 2: Сборка Docker образа..."
|
||||||
|
if ! ./deploy/5.build-docker.sh "$REPOSITORY" "$VERSION" "$PLATFORMS"; then
|
||||||
|
error "Ошибка при сборке Docker образа"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
success "✅ Docker образы собраны успешно"
|
||||||
|
|
||||||
|
# Этап 3: Генерация deployment.yaml
|
||||||
|
log "📝 Этап 3: Генерация deployment.yaml..."
|
||||||
|
# Generate deployment.yaml with env substitution
|
||||||
|
export REPOSITORY=$REPOSITORY
|
||||||
|
export VERSION=$VERSION
|
||||||
|
export PLATFORMS=$PLATFORMS
|
||||||
|
export KUBECONFIG="${HOME}/.kube/config_hlab" && kubectx default
|
||||||
|
|
||||||
|
if ! envsubst < deploy/1.configmap.tmpl.yaml > kustomize/configmap.yaml; then
|
||||||
|
error "Ошибка при генерации deploy/1.configmap.yaml"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
success "✅ kustomize/configmap.yaml сгенерирован успешно"
|
||||||
|
|
||||||
|
if ! envsubst < deploy/3.deployment.tmpl.yaml > kustomize/deployment.yaml; then
|
||||||
|
error "Ошибка при генерации kustomize/deployment.yaml"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
success "✅ kustomize/deployment.yaml сгенерирован успешно"
|
||||||
|
|
||||||
|
if ! envsubst < deploy/ingress-route.tmpl.yaml > kustomize/ingress-route.yaml; then
|
||||||
|
error "Ошибка при генерации kustomize/ingress-route.yaml"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
success "✅ kustomize/ingress-route.yaml сгенерирован успешно"
|
||||||
|
|
||||||
|
if ! envsubst < deploy/service.tmpl.yaml > kustomize/service.yaml; then
|
||||||
|
error "Ошибка при генерации kustomize/service.yaml"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
success "✅ kustomize/service.yaml сгенерирован успешно"
|
||||||
|
|
||||||
|
if ! envsubst < deploy/kustomization.tmpl.yaml > kustomize/kustomization.yaml; then
|
||||||
|
error "Ошибка при генерации kustomize/kustomization.yaml"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
success "✅ kustomize/kustomization.yaml сгенерирован успешно"
|
||||||
|
|
||||||
|
# отключить reconciliation flux
|
||||||
|
if kubectl get kustomization lcg -n flux-system > /dev/null 2>&1; then
|
||||||
|
kubectl patch kustomization lcg -n flux-system --type=merge -p '{"spec":{"suspend":true}}'
|
||||||
|
else
|
||||||
|
echo "ℹ️ Kustomization 'lcg' does not exist in 'flux-system' namespace. Skipping suspend."
|
||||||
|
fi
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
|
||||||
|
# зафиксировать изменения в текущей ветке, если она не main
|
||||||
|
current_branch=$(git rev-parse --abbrev-ref HEAD)
|
||||||
|
if [ "$current_branch" != "main" ]; then
|
||||||
|
log "🔧 Исправления в текущей ветке: $current_branch"
|
||||||
|
# считать, что изменения уже сделаны
|
||||||
|
git add .
|
||||||
|
git commit -m "Исправления в ветке $current_branch"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# переключиться на ветку main и слить с текущей веткой, если не находимся на main
|
||||||
|
if [ "$current_branch" != "main" ]; then
|
||||||
|
git checkout main
|
||||||
|
git merge --no-ff -m "Merged branch '$current_branch' into main while building $VERSION" "$current_branch"
|
||||||
|
git push origin main
|
||||||
|
elif [ "$current_branch" = "main" ]; then
|
||||||
|
log "🔄 Вы находитесь на ветке main. Слияние с release..."
|
||||||
|
git add .
|
||||||
|
git commit -m "Исправления в ветке $current_branch"
|
||||||
|
git push origin main
|
||||||
|
fi
|
||||||
|
|
||||||
|
# переключиться на ветку release и слить с веткой main
|
||||||
|
if git show-ref --quiet refs/heads/release; then
|
||||||
|
log "ℹ️ Branch 'release' exists. Proceeding with merge."
|
||||||
|
git checkout release
|
||||||
|
git merge --no-ff -m "Merged main into release while building $VERSION" main
|
||||||
|
else
|
||||||
|
log "❌ Branch 'release' does not exist. Please create the branch before proceeding."
|
||||||
|
git checkout -b release
|
||||||
|
git merge --no-ff -m "Merged main into release while building $VERSION" main
|
||||||
|
fi
|
||||||
|
|
||||||
|
# если тег $VERSION существует, удалить его и принудительно запушить
|
||||||
|
tag_exists=$(git tag -l "$VERSION")
|
||||||
|
if [ "$tag_exists" ]; then
|
||||||
|
log "🗑️ Удаление существующего тега $VERSION"
|
||||||
|
git tag -d "$VERSION"
|
||||||
|
git push origin ":refs/tags/$VERSION"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create tag $VERSION and push to remote release branch and all tags
|
||||||
|
git tag "$VERSION"
|
||||||
|
git push origin release
|
||||||
|
git push origin --tags
|
||||||
|
|
||||||
|
# Push main branch
|
||||||
|
git checkout main
|
||||||
|
git push origin main
|
||||||
|
|
||||||
|
|
||||||
|
# Включить reconciliation flux
|
||||||
|
if kubectl get kustomization lcg -n flux-system > /dev/null 2>&1; then
|
||||||
|
kubectl patch kustomization lcg -n flux-system --type=merge -p '{"spec":{"suspend":false}}'
|
||||||
|
else
|
||||||
|
echo "ℹ️ Kustomization 'lcg' does not exist in 'flux-system' namespace. Skipping suspend."
|
||||||
|
fi
|
||||||
|
echo "🔄 Flux will automatically deploy $VERSION version in ~4-6 minutes..."
|
||||||
|
|
||||||
|
# Итоговая информация
|
||||||
|
echo ""
|
||||||
|
log "🎉 Полная сборка завершена успешно!"
|
||||||
|
echo ""
|
||||||
|
log "📊 Результат:"
|
||||||
|
echo " Репозиторий: $REPOSITORY"
|
||||||
|
echo " Версия: $VERSION"
|
||||||
|
echo " Платформы: $PLATFORMS"
|
||||||
|
echo " Теги: $REPOSITORY:$VERSION, $REPOSITORY:latest"
|
||||||
|
echo ""
|
||||||
|
echo ""
|
||||||
|
log "🔍 Информация о git коммитах:"
|
||||||
|
git_log=$(git log release -1 --pretty=format:"%H - %s")
|
||||||
|
echo "$git_log"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
|
||||||
|
log "📝 Команды для использования:"
|
||||||
|
echo " docker pull $REPOSITORY:$VERSION"
|
||||||
|
echo " docker run -p 8080:8080 $REPOSITORY:$VERSION"
|
||||||
|
echo " docker buildx imagetools inspect $REPOSITORY:$VERSION"
|
||||||
|
echo ""
|
||||||
|
log "🔍 Проверка образа:"
|
||||||
|
echo " docker run --rm $REPOSITORY:$VERSION /app/lcg --version"
|
||||||
|
echo ""
|
||||||
|
log "📝 Команды для использования:"
|
||||||
|
|
||||||
|
echo " kubectl get pods"
|
||||||
|
echo " kubectl get services"
|
||||||
|
echo " kubectl get ingress"
|
||||||
39
deploy/Dockerfile
Normal file
39
deploy/Dockerfile
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Однофазный build для LCG с предварительно собранным бинарным файлом
|
||||||
|
FROM alpine:3.22.2
|
||||||
|
|
||||||
|
# Устанавливаем зависимости
|
||||||
|
RUN apk --no-cache add ca-certificates tzdata
|
||||||
|
|
||||||
|
# Создаем пользователя
|
||||||
|
RUN adduser -D -s /bin/sh lcg
|
||||||
|
|
||||||
|
# Создаем директории и файлы
|
||||||
|
RUN mkdir -p /app/data /app/config /home/lcg/.config/lcg/gpt_results /home/lcg/.config/lcg/gpt_sys_prompts && \
|
||||||
|
echo '[]' > /home/lcg/.config/lcg/gpt_results/lcg_history.json && \
|
||||||
|
chown -R lcg:lcg /app /home/lcg/.config
|
||||||
|
|
||||||
|
# Копируем файл версии
|
||||||
|
COPY VERSION.txt /app/VERSION.txt
|
||||||
|
|
||||||
|
# Копируем предварительно собранный бинарный файл
|
||||||
|
# Ищем папку с бинарным файлом для текущей архитектуры
|
||||||
|
COPY dist/lcg_linux_${TARGETARCH}*/lcg_* /app/lcg
|
||||||
|
|
||||||
|
# Устанавливаем права
|
||||||
|
RUN chmod +x /app/lcg
|
||||||
|
|
||||||
|
# Переключаемся на пользователя lcg
|
||||||
|
USER lcg
|
||||||
|
|
||||||
|
# Устанавливаем рабочую директорию
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Открываем порт
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||||
|
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/login || exit 1
|
||||||
|
|
||||||
|
# Запускаем приложение
|
||||||
|
CMD ["./lcg", "serve", "-H", "0.0.0.0", "-p", "8080"]
|
||||||
1
deploy/VERSION.txt
Normal file
1
deploy/VERSION.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
v.2.0.19
|
||||||
42
deploy/hpa.yaml
Normal file
42
deploy/hpa.yaml
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
apiVersion: autoscaling/v2
|
||||||
|
kind: HorizontalPodAutoscaler
|
||||||
|
metadata:
|
||||||
|
name: lcg-hpa
|
||||||
|
namespace: lcg
|
||||||
|
spec:
|
||||||
|
scaleTargetRef:
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
name: lcg
|
||||||
|
minReplicas: 2
|
||||||
|
maxReplicas: 10
|
||||||
|
metrics:
|
||||||
|
- type: Resource
|
||||||
|
resource:
|
||||||
|
name: cpu
|
||||||
|
target:
|
||||||
|
type: Utilization
|
||||||
|
averageUtilization: 70
|
||||||
|
- type: Resource
|
||||||
|
resource:
|
||||||
|
name: memory
|
||||||
|
target:
|
||||||
|
type: Utilization
|
||||||
|
averageUtilization: 80
|
||||||
|
behavior:
|
||||||
|
scaleDown:
|
||||||
|
stabilizationWindowSeconds: 300
|
||||||
|
policies:
|
||||||
|
- type: Percent
|
||||||
|
value: 10
|
||||||
|
periodSeconds: 60
|
||||||
|
scaleUp:
|
||||||
|
stabilizationWindowSeconds: 60
|
||||||
|
policies:
|
||||||
|
- type: Percent
|
||||||
|
value: 50
|
||||||
|
periodSeconds: 60
|
||||||
|
- type: Pods
|
||||||
|
value: 2
|
||||||
|
periodSeconds: 60
|
||||||
|
selectPolicy: Max
|
||||||
63
deploy/ingress-route.tmpl.yaml
Normal file
63
deploy/ingress-route.tmpl.yaml
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: IngressRoute
|
||||||
|
metadata:
|
||||||
|
name: lcg-route
|
||||||
|
namespace: lcg
|
||||||
|
labels:
|
||||||
|
app: lcg
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- websecure
|
||||||
|
routes:
|
||||||
|
- kind: Rule
|
||||||
|
match: Host(`direct-dev.ru`) && PathPrefix(`/lcg`)
|
||||||
|
services:
|
||||||
|
- name: lcg
|
||||||
|
port: 8080
|
||||||
|
tls:
|
||||||
|
secretName: le-root-direct-dev-ru
|
||||||
|
---
|
||||||
|
|
||||||
|
# apiVersion: traefik.io/v1alpha1
|
||||||
|
# kind: IngressRoute
|
||||||
|
# metadata:
|
||||||
|
# name: lcg-route
|
||||||
|
# namespace: lcg
|
||||||
|
# spec:
|
||||||
|
# entryPoints:
|
||||||
|
# - websecure
|
||||||
|
# routes:
|
||||||
|
# - kind: Rule
|
||||||
|
# match: Host(`direct-dev.ru`) && PathPrefix(`/lcg`)
|
||||||
|
# services:
|
||||||
|
# - name: lcg
|
||||||
|
# port: 8080
|
||||||
|
# middlewares:
|
||||||
|
# - name: lcg-strip-prefix
|
||||||
|
# tls:
|
||||||
|
# secretName: le-root-direct-dev-ru
|
||||||
|
# ---
|
||||||
|
# apiVersion: traefik.io/v1alpha1
|
||||||
|
# kind: Middleware
|
||||||
|
# metadata:
|
||||||
|
# name: lcg-strip-prefix
|
||||||
|
# namespace: lcg
|
||||||
|
# spec:
|
||||||
|
# stripPrefix:
|
||||||
|
# prefixes:
|
||||||
|
# - /lcg
|
||||||
|
# ---
|
||||||
|
# apiVersion: traefik.io/v1alpha1
|
||||||
|
# kind: Middleware
|
||||||
|
# metadata:
|
||||||
|
# name: lcg-headers
|
||||||
|
# namespace: lcg
|
||||||
|
# spec:
|
||||||
|
# headers:
|
||||||
|
# customRequestHeaders:
|
||||||
|
# X-Forwarded-Proto: "https"
|
||||||
|
# X-Forwarded-Port: "443"
|
||||||
|
# customResponseHeaders:
|
||||||
|
# X-Frame-Options: "DENY"
|
||||||
|
# X-Content-Type-Options: "nosniff"
|
||||||
|
# X-XSS-Protection: "1; mode=block"
|
||||||
25
deploy/kustomization.tmpl.yaml
Normal file
25
deploy/kustomization.tmpl.yaml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
|
||||||
|
# Namespace
|
||||||
|
namespace: lcg
|
||||||
|
|
||||||
|
# Resources
|
||||||
|
resources:
|
||||||
|
- configmap.yaml
|
||||||
|
- secret.yaml
|
||||||
|
- deployment.yaml
|
||||||
|
- service.yaml
|
||||||
|
- ingress-route.yaml
|
||||||
|
|
||||||
|
# Common labels
|
||||||
|
# commonLabels:
|
||||||
|
# app: lcg
|
||||||
|
# version: ${VERSION}
|
||||||
|
# managed-by: kustomize
|
||||||
|
|
||||||
|
# Images
|
||||||
|
# images:
|
||||||
|
# - name: lcg
|
||||||
|
# newName: ${REPOSITORY}
|
||||||
|
# newTag: ${VERSION}
|
||||||
170
deploy/release-goreleaser.sh
Normal file
170
deploy/release-goreleaser.sh
Normal 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 "Готово."
|
||||||
|
|
||||||
|
|
||||||
16
deploy/service.tmpl.yaml
Normal file
16
deploy/service.tmpl.yaml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: lcg
|
||||||
|
namespace: lcg
|
||||||
|
labels:
|
||||||
|
app: lcg
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
ports:
|
||||||
|
- port: 8080
|
||||||
|
targetPort: 8080
|
||||||
|
protocol: TCP
|
||||||
|
name: http
|
||||||
|
selector:
|
||||||
|
app: lcg
|
||||||
232
docs/CONFIG_COMMAND.md
Normal file
232
docs/CONFIG_COMMAND.md
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
# 🔧 Команда config - Управление конфигурацией
|
||||||
|
|
||||||
|
## 📋 Описание
|
||||||
|
|
||||||
|
Команда `config` позволяет просматривать текущую конфигурацию приложения, включая все настройки, переменные окружения и значения по умолчанию.
|
||||||
|
|
||||||
|
## 🚀 Использование
|
||||||
|
|
||||||
|
### Краткий вывод конфигурации (по умолчанию)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
lcg config
|
||||||
|
# или
|
||||||
|
lcg co
|
||||||
|
```
|
||||||
|
|
||||||
|
**Вывод:**
|
||||||
|
|
||||||
|
``` text
|
||||||
|
Provider: ollama
|
||||||
|
Host: http://192.168.87.108:11434/
|
||||||
|
Model: hf.co/yandex/YandexGPT-5-Lite-8B-instruct-GGUF:Q4_K_M
|
||||||
|
Prompt: Reply with linux command and nothing else. Output with plain response - no need formatting. No need explanation. No need code blocks. No need ` symbols.
|
||||||
|
Timeout: 300 seconds
|
||||||
|
```
|
||||||
|
|
||||||
|
### Полный вывод конфигурации
|
||||||
|
|
||||||
|
```bash
|
||||||
|
lcg config --full
|
||||||
|
# или
|
||||||
|
lcg config -f
|
||||||
|
# или
|
||||||
|
lcg co --full
|
||||||
|
# или
|
||||||
|
lcg co -f
|
||||||
|
```
|
||||||
|
|
||||||
|
**Вывод (JSON формат):**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cwd": "/home/user/projects/golang/linux-command-gpt",
|
||||||
|
"host": "http://192.168.87.108:11434/",
|
||||||
|
"proxy_url": "/api/v1/protected/sberchat/chat",
|
||||||
|
"completions": "api/chat",
|
||||||
|
"model": "hf.co/yandex/YandexGPT-5-Lite-8B-instruct-GGUF:Q4_K_M",
|
||||||
|
"prompt": "Reply with linux command and nothing else. Output with plain response - no need formatting. No need explanation. No need code blocks. No need ` symbols.",
|
||||||
|
"api_key_file": ".openai_api_key",
|
||||||
|
"result_folder": "/home/user/.config/lcg/gpt_results",
|
||||||
|
"prompt_folder": "/home/user/.config/lcg/gpt_sys_prompts",
|
||||||
|
"provider_type": "ollama",
|
||||||
|
"jwt_token": "***not set***",
|
||||||
|
"prompt_id": "1",
|
||||||
|
"timeout": "300",
|
||||||
|
"result_history": "/home/user/.config/lcg/gpt_results/lcg_history.json",
|
||||||
|
"no_history_env": "",
|
||||||
|
"allow_execution": false,
|
||||||
|
"main_flags": {
|
||||||
|
"file": "",
|
||||||
|
"no_history": false,
|
||||||
|
"sys": "",
|
||||||
|
"prompt_id": 0,
|
||||||
|
"timeout": 0,
|
||||||
|
"debug": false
|
||||||
|
},
|
||||||
|
"server": {
|
||||||
|
"port": "8080",
|
||||||
|
"host": "localhost"
|
||||||
|
},
|
||||||
|
"validation": {
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Структура полной конфигурации
|
||||||
|
|
||||||
|
### Основные настройки
|
||||||
|
|
||||||
|
- **cwd** - текущая рабочая директория
|
||||||
|
- **host** - адрес API сервера
|
||||||
|
- **proxy_url** - путь к API эндпоинту
|
||||||
|
- **completions** - путь к эндпоинту completions
|
||||||
|
- **model** - используемая модель ИИ
|
||||||
|
- **prompt** - системный промпт по умолчанию
|
||||||
|
- **api_key_file** - файл с API ключом
|
||||||
|
- **result_folder** - папка для сохранения результатов
|
||||||
|
- **prompt_folder** - папка с системными промптами
|
||||||
|
- **provider_type** - тип провайдера (ollama/proxy)
|
||||||
|
- **jwt_token** - статус JWT токена (***set***/***from file***/***not set***)
|
||||||
|
- **prompt_id** - ID промпта по умолчанию
|
||||||
|
- **timeout** - таймаут запросов в секундах
|
||||||
|
- **result_history** - файл истории запросов
|
||||||
|
- **no_history_env** - переменная окружения для отключения истории
|
||||||
|
- **allow_execution** - разрешение выполнения команд
|
||||||
|
|
||||||
|
### Флаги командной строки (main_flags)
|
||||||
|
|
||||||
|
- **file** - файл для чтения
|
||||||
|
- **no_history** - отключение истории
|
||||||
|
- **sys** - системный промпт
|
||||||
|
- **prompt_id** - ID промпта
|
||||||
|
- **timeout** - таймаут
|
||||||
|
- **debug** - отладочный режим
|
||||||
|
|
||||||
|
### Настройки сервера (server)
|
||||||
|
|
||||||
|
- **port** - порт веб-сервера
|
||||||
|
- **host** - хост веб-сервера
|
||||||
|
|
||||||
|
### Настройки валидации (validation)
|
||||||
|
|
||||||
|
- **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)
|
||||||
|
|
||||||
|
## 🔒 Безопасность
|
||||||
|
|
||||||
|
При выводе полной конфигурации чувствительные данные маскируются:
|
||||||
|
|
||||||
|
- **JWT токены** - показывается статус (***set***/***from file***/***not set***)
|
||||||
|
- **API ключи** - не выводятся в открытом виде
|
||||||
|
- **Пароли** - не сохраняются в конфигурации
|
||||||
|
|
||||||
|
## 📝 Примеры использования
|
||||||
|
|
||||||
|
### Просмотр текущих настроек
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Краткий вывод
|
||||||
|
lcg config
|
||||||
|
|
||||||
|
# Полный вывод
|
||||||
|
lcg config --full
|
||||||
|
```
|
||||||
|
|
||||||
|
### Проверка настроек валидации
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Показать только настройки валидации
|
||||||
|
lcg config --full | jq '.validation'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Проверка настроек сервера
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Показать только настройки сервера
|
||||||
|
lcg config --full | jq '.server'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Проверка переменных окружения
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Показать все переменные окружения LCG
|
||||||
|
env | grep LCG
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Интеграция с другими инструментами
|
||||||
|
|
||||||
|
### Использование с jq
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Получить только модель
|
||||||
|
lcg config --full | jq -r '.model'
|
||||||
|
|
||||||
|
# Получить настройки валидации
|
||||||
|
lcg config --full | jq '.validation'
|
||||||
|
|
||||||
|
# Получить все пути
|
||||||
|
lcg config --full | jq '{result_folder, prompt_folder, result_history}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Использование с grep
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Найти все настройки с "timeout"
|
||||||
|
lcg config --full | grep -i timeout
|
||||||
|
|
||||||
|
# Найти все пути
|
||||||
|
lcg config --full | grep -E "(folder|history)"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Сохранение конфигурации в файл
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Сохранить полную конфигурацию
|
||||||
|
lcg config --full > config.json
|
||||||
|
|
||||||
|
# Сохранить только настройки валидации
|
||||||
|
lcg config --full | jq '.validation' > validation.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🐛 Отладка
|
||||||
|
|
||||||
|
### Проверка загрузки конфигурации
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Показать все настройки
|
||||||
|
lcg config --full
|
||||||
|
|
||||||
|
# Проверить переменные окружения
|
||||||
|
env | grep LCG
|
||||||
|
|
||||||
|
# Проверить файлы конфигурации
|
||||||
|
ls -la ~/.config/lcg/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Типичные проблемы
|
||||||
|
|
||||||
|
1. **Неправильные пути** - проверьте `result_folder` и `prompt_folder`
|
||||||
|
2. **Отсутствующие токены** - проверьте `jwt_token` статус
|
||||||
|
3. **Неправильные лимиты** - проверьте секцию `validation`
|
||||||
|
|
||||||
|
## 📚 Связанные команды
|
||||||
|
|
||||||
|
- `lcg --help` - общая справка
|
||||||
|
- `lcg config --help` - справка по команде config
|
||||||
|
- `lcg serve` - запуск веб-сервера
|
||||||
|
- `lcg prompts list` - список промптов
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Примечание**: Команда `config` показывает актуальное состояние конфигурации после применения всех переменных окружения и значений по умолчанию.
|
||||||
231
docs/CSRF_TESTING_GUIDE.md
Normal file
231
docs/CSRF_TESTING_GUIDE.md
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
# 🛡️ Руководство по тестированию CSRF защиты
|
||||||
|
|
||||||
|
## 📋 Обзор
|
||||||
|
|
||||||
|
Это руководство поможет вам протестировать CSRF защиту в LCG приложении и понять, как работают CSRF атаки.
|
||||||
|
|
||||||
|
## 🚀 Быстрый старт
|
||||||
|
|
||||||
|
### 1. Запуск сервера с CSRF защитой
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Запуск с аутентификацией и CSRF защитой
|
||||||
|
LCG_SERVER_REQUIRE_AUTH=true ./lcg serve -p 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Автоматическое тестирование
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Запуск автоматических тестов
|
||||||
|
./test_csrf.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Ручное тестирование
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Откройте в браузере
|
||||||
|
open csrf_test.html
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🧪 Типы тестов
|
||||||
|
|
||||||
|
### ✅ **Тест 1: Защищенные запросы**
|
||||||
|
|
||||||
|
- **Цель**: Проверить, что POST запросы без CSRF токена блокируются
|
||||||
|
- **Ожидаемый результат**: 403 Forbidden
|
||||||
|
- **Endpoints**: `/api/execute`, `/api/save-result`, `/api/add-to-history`
|
||||||
|
|
||||||
|
### ✅ **Тест 2: Разрешенные запросы**
|
||||||
|
|
||||||
|
- **Цель**: Проверить, что GET запросы работают
|
||||||
|
- **Ожидаемый результат**: 200 OK
|
||||||
|
- **Endpoints**: `/login`, `/`, `/history`
|
||||||
|
|
||||||
|
### ✅ **Тест 3: CSRF токены**
|
||||||
|
|
||||||
|
- **Цель**: Проверить наличие CSRF токенов в формах
|
||||||
|
- **Ожидаемый результат**: Токены присутствуют в HTML
|
||||||
|
|
||||||
|
### ✅ **Тест 4: Поддельные токены**
|
||||||
|
|
||||||
|
- **Цель**: Проверить защиту от поддельных токенов
|
||||||
|
- **Ожидаемый результат**: 403 Forbidden
|
||||||
|
|
||||||
|
## 🎯 Сценарии атак
|
||||||
|
|
||||||
|
### **Сценарий 1: Выполнение команд**
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- Злонамеренная форма -->
|
||||||
|
<form action="http://localhost:8080/api/execute" method="POST">
|
||||||
|
<input type="hidden" name="prompt" value="rm -rf /">
|
||||||
|
<input type="hidden" name="system_id" value="1">
|
||||||
|
<button type="submit">Нажми меня!</button>
|
||||||
|
</form>
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Сценарий 2: Сохранение данных**
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- Злонамеренная форма -->
|
||||||
|
<form action="http://localhost:8080/api/save-result" method="POST">
|
||||||
|
<input type="hidden" name="result" value="Вредоносные данные">
|
||||||
|
<input type="hidden" name="command" value="malicious_command">
|
||||||
|
<button type="submit">Сохранить</button>
|
||||||
|
</form>
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Сценарий 3: JavaScript атака**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Злонамеренный JavaScript
|
||||||
|
fetch('http://localhost:8080/api/execute', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({prompt: 'whoami', system_id: '1'})
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔍 Анализ результатов
|
||||||
|
|
||||||
|
### **✅ Защита работает, если:**
|
||||||
|
|
||||||
|
- Все POST запросы возвращают 403 Forbidden
|
||||||
|
- В ответах есть "CSRF token required"
|
||||||
|
- GET запросы работают нормально
|
||||||
|
- CSRF токены присутствуют в формах
|
||||||
|
|
||||||
|
### **❌ Уязвимость есть, если:**
|
||||||
|
|
||||||
|
- POST запросы выполняются успешно (200 OK)
|
||||||
|
- Команды выполняются на сервере
|
||||||
|
- Данные сохраняются без CSRF токенов
|
||||||
|
- Нет проверки Origin/Referer заголовков
|
||||||
|
|
||||||
|
## 🛠️ Инструменты тестирования
|
||||||
|
|
||||||
|
### **1. Автоматический скрипт**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./test_csrf.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
- Тестирует все основные endpoints
|
||||||
|
- Проверяет CSRF токены
|
||||||
|
- Выводит подробный отчет
|
||||||
|
|
||||||
|
### **2. HTML тестовая страница**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
open csrf_test.html
|
||||||
|
```
|
||||||
|
|
||||||
|
- Интерактивное тестирование
|
||||||
|
- Визуальная проверка результатов
|
||||||
|
- Тестирование в браузере
|
||||||
|
|
||||||
|
### **3. Демонстрационная атака**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
open csrf_demo.html
|
||||||
|
```
|
||||||
|
|
||||||
|
- Показывает, как работают CSRF атаки
|
||||||
|
- Демонстрирует уязвимости
|
||||||
|
- Образовательные цели
|
||||||
|
|
||||||
|
## 🔧 Настройка тестов
|
||||||
|
|
||||||
|
### **Переменные окружения для тестирования:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Включить аутентификацию
|
||||||
|
LCG_SERVER_REQUIRE_AUTH=true
|
||||||
|
|
||||||
|
# Настроить CSRF защиту
|
||||||
|
LCG_COOKIE_SECURE=false
|
||||||
|
LCG_DOMAIN=.localhost
|
||||||
|
LCG_COOKIE_PATH=/
|
||||||
|
|
||||||
|
# Запуск сервера
|
||||||
|
./lcg serve -H 0.0.0.0 -p 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Настройка reverse proxy для тестирования:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Для тестирования за reverse proxy
|
||||||
|
LCG_SERVER_REQUIRE_AUTH=true \
|
||||||
|
LCG_SERVER_ALLOW_HTTP=true \
|
||||||
|
LCG_DOMAIN=.yourdomain.com \
|
||||||
|
LCG_COOKIE_PATH=/lcg \
|
||||||
|
LCG_COOKIE_SECURE=false \
|
||||||
|
./lcg serve -H 0.0.0.0 -p 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Интерпретация результатов
|
||||||
|
|
||||||
|
### **Успешные тесты:**
|
||||||
|
|
||||||
|
``` text
|
||||||
|
✅ CSRF защита /api/execute: PASS - Запрос заблокирован (403 Forbidden)
|
||||||
|
✅ CSRF защита /api/save-result: PASS - Запрос заблокирован (403 Forbidden)
|
||||||
|
✅ CSRF защита /api/add-to-history: PASS - Запрос заблокирован (403 Forbidden)
|
||||||
|
✅ GET запросы: PASS - GET запросы работают (HTTP 200)
|
||||||
|
✅ CSRF токен на странице входа: PASS - Токен найден
|
||||||
|
✅ CSRF защита от поддельного токена: PASS - Поддельный токен заблокирован (403 Forbidden)
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Проблемные тесты:**
|
||||||
|
|
||||||
|
``` text
|
||||||
|
❌ CSRF защита /api/execute: FAIL - Запрос прошел (HTTP 200)
|
||||||
|
❌ CSRF защита /api/save-result: FAIL - Запрос прошел (HTTP 200)
|
||||||
|
❌ CSRF токен на странице входа: FAIL - Токен не найден
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚨 Частые проблемы
|
||||||
|
|
||||||
|
### **1. Cookies не работают**
|
||||||
|
|
||||||
|
- Проверьте настройки `LCG_DOMAIN`
|
||||||
|
- Убедитесь, что `LCG_COOKIE_PATH` правильный
|
||||||
|
- Проверьте настройки reverse proxy
|
||||||
|
|
||||||
|
### **2. CSRF токены не генерируются**
|
||||||
|
|
||||||
|
- Убедитесь, что `LCG_SERVER_REQUIRE_AUTH=true`
|
||||||
|
- Проверьте инициализацию CSRF менеджера
|
||||||
|
- Проверьте логи сервера
|
||||||
|
|
||||||
|
### **3. Запросы проходят без токенов**
|
||||||
|
|
||||||
|
- Проверьте middleware в `serve/middleware.go`
|
||||||
|
- Убедитесь, что CSRF middleware применяется
|
||||||
|
- Проверьте исключения в middleware
|
||||||
|
|
||||||
|
## 📝 Рекомендации
|
||||||
|
|
||||||
|
### **Для разработчиков:**
|
||||||
|
|
||||||
|
1. Всегда тестируйте CSRF защиту
|
||||||
|
2. Используйте автоматические тесты
|
||||||
|
3. Проверяйте все POST endpoints
|
||||||
|
4. Валидируйте CSRF токены
|
||||||
|
|
||||||
|
### **Для администраторов:**
|
||||||
|
|
||||||
|
1. Регулярно запускайте тесты
|
||||||
|
2. Мониторьте логи на подозрительную активность
|
||||||
|
3. Настройте правильные заголовки в reverse proxy
|
||||||
|
4. Используйте HTTPS в продакшене
|
||||||
|
|
||||||
|
## 🎓 Образовательные материалы
|
||||||
|
|
||||||
|
- **OWASP CSRF Prevention Cheat Sheet**: <https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html>
|
||||||
|
- **CSRF атаки**: <https://owasp.org/www-community/attacks/csrf>
|
||||||
|
- **SameSite cookies**: <https://web.dev/samesite-cookies-explained/>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**⚠️ ВНИМАНИЕ**: Эти тесты предназначены только для проверки безопасности вашего собственного приложения. Не используйте их для атак на чужие системы!
|
||||||
@@ -35,10 +35,17 @@ Explanations:
|
|||||||
|
|
||||||
Clipboard support requires `xclip` or `xsel`.
|
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
|
- Authentication: JWT-based authentication with HTTP-only cookies
|
||||||
- Public REST endpoint: `POST /execute` (curl-only) for programmatic access — see `API_GUIDE.md`
|
- 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
|
## Environment
|
||||||
|
|
||||||
@@ -133,10 +140,15 @@ The `serve` command provides both a web interface and REST API:
|
|||||||
|
|
||||||
**Web Interface:**
|
**Web Interface:**
|
||||||
|
|
||||||
- Browse results at `http://localhost:8080/`
|
- Browse results at `http://localhost:8080/` (or `http://localhost:8080<BASE_PATH>/` if `LCG_BASE_URL` set)
|
||||||
- Execute requests at `http://localhost:8080/run`
|
- Execute requests at `.../run`
|
||||||
- Manage prompts at `http://localhost:8080/prompts`
|
- Manage prompts at `.../prompts`
|
||||||
- View history at `http://localhost:8080/history`
|
- 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:**
|
**REST API:**
|
||||||
|
|
||||||
337
docs/RELEASE_GUIDE.md
Normal file
337
docs/RELEASE_GUIDE.md
Normal file
@@ -0,0 +1,337 @@
|
|||||||
|
# 🚀 Гайд по созданию релизов с помощью GoReleaser
|
||||||
|
|
||||||
|
Этот документ описывает процесс создания релизов для проекта `linux-command-gpt` с использованием GoReleaser.
|
||||||
|
|
||||||
|
## 📋 Содержание
|
||||||
|
|
||||||
|
- [Установка GoReleaser](#установка-goreleaser)
|
||||||
|
- [Конфигурация](#конфигурация)
|
||||||
|
- [Процесс создания релиза](#процесс-создания-релиза)
|
||||||
|
- [Автоматизация](#автоматизация)
|
||||||
|
- [Устранение проблем](#устранение-проблем)
|
||||||
|
|
||||||
|
## 🔧 Установка GoReleaser
|
||||||
|
|
||||||
|
### Linux/macOS
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Скачать и установить последнюю версию
|
||||||
|
curl -sL https://git.io/goreleaser | bash
|
||||||
|
|
||||||
|
# Или через Homebrew (macOS)
|
||||||
|
brew install goreleaser
|
||||||
|
|
||||||
|
# Или через Go
|
||||||
|
go install github.com/goreleaser/goreleaser@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Через Chocolatey
|
||||||
|
choco install goreleaser
|
||||||
|
|
||||||
|
# Или скачать с GitHub Releases
|
||||||
|
# https://github.com/goreleaser/goreleaser/releases
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚙️ Конфигурация
|
||||||
|
|
||||||
|
### Файл `.goreleaser.yaml`
|
||||||
|
|
||||||
|
В проекте используется следующая конфигурация GoReleaser:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
before:
|
||||||
|
hooks:
|
||||||
|
- go mod tidy
|
||||||
|
- go generate ./...
|
||||||
|
|
||||||
|
builds:
|
||||||
|
- binary: lcg
|
||||||
|
env:
|
||||||
|
- CGO_ENABLED=0
|
||||||
|
goarch:
|
||||||
|
- amd64
|
||||||
|
- arm64
|
||||||
|
- arm
|
||||||
|
goos:
|
||||||
|
- linux
|
||||||
|
- darwin
|
||||||
|
|
||||||
|
archives:
|
||||||
|
- formats: [tar.gz]
|
||||||
|
name_template: >-
|
||||||
|
{{ .ProjectName }}_
|
||||||
|
{{- title .Os }}_
|
||||||
|
{{- if eq .Arch "amd64" }}x86_64
|
||||||
|
{{- else if eq .Arch "386" }}i386
|
||||||
|
{{- else }}{{ .Arch }}{{ end }}
|
||||||
|
{{- if .Arm }}v{{ .Arm }}{{ end }}
|
||||||
|
format_overrides:
|
||||||
|
- goos: windows
|
||||||
|
formats: [zip]
|
||||||
|
|
||||||
|
changelog:
|
||||||
|
sort: asc
|
||||||
|
filters:
|
||||||
|
exclude:
|
||||||
|
- "^docs:"
|
||||||
|
- "^test:"
|
||||||
|
|
||||||
|
release:
|
||||||
|
footer: >-
|
||||||
|
---
|
||||||
|
Released by [GoReleaser](https://github.com/goreleaser/goreleaser).
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ключевые настройки
|
||||||
|
|
||||||
|
- **builds**: Сборка для Linux, macOS, Windows (amd64, arm64, arm)
|
||||||
|
- **archives**: Создание архивов tar.gz для Unix и zip для Windows
|
||||||
|
- **changelog**: Автоматическое создание changelog из git commits
|
||||||
|
- **release**: Настройки GitHub релиза
|
||||||
|
|
||||||
|
## 🚀 Процесс создания релиза
|
||||||
|
|
||||||
|
### 1. Подготовка
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Убедитесь, что все изменения закоммичены
|
||||||
|
git status
|
||||||
|
|
||||||
|
# Обновите версию в VERSION.txt
|
||||||
|
echo "v2.0.2" > VERSION.txt
|
||||||
|
|
||||||
|
# Создайте тег
|
||||||
|
git tag v2.0.2
|
||||||
|
git push origin v2.0.2
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Настройка переменных окружения
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Установите GitHub токен
|
||||||
|
export GITHUB_TOKEN="your_github_token_here"
|
||||||
|
|
||||||
|
# Или создайте файл .env
|
||||||
|
echo "GITHUB_TOKEN=your_github_token_here" > .env
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Создание релиза
|
||||||
|
|
||||||
|
#### Полный релиз
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Создать релиз с загрузкой на GitHub
|
||||||
|
goreleaser release
|
||||||
|
|
||||||
|
# Создать релиз без загрузки (только локально)
|
||||||
|
goreleaser release --clean
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Тестовый релиз (snapshot)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Создать тестовую сборку
|
||||||
|
goreleaser release --snapshot
|
||||||
|
|
||||||
|
# Тестовая сборка без загрузки
|
||||||
|
goreleaser release --snapshot --clean
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Проверка результатов
|
||||||
|
|
||||||
|
После выполнения команды GoReleaser создаст:
|
||||||
|
|
||||||
|
- **Архивы**: `dist/` - готовые архивы для всех платформ
|
||||||
|
- **Чексуммы**: `dist/checksums.txt` - контрольные суммы файлов
|
||||||
|
- **GitHub релиз**: Автоматически созданный релиз на GitHub
|
||||||
|
|
||||||
|
## 🤖 Автоматизация
|
||||||
|
|
||||||
|
### GitHub Actions
|
||||||
|
|
||||||
|
Создайте файл `.github/workflows/release.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: Release
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: '1.21'
|
||||||
|
- name: Run GoReleaser
|
||||||
|
uses: goreleaser/goreleaser-action@v4
|
||||||
|
with:
|
||||||
|
distribution: goreleaser
|
||||||
|
version: latest
|
||||||
|
args: release
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Локальные скрипты
|
||||||
|
|
||||||
|
В проекте есть готовые скрипты:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Предварительная подготовка
|
||||||
|
./shell-code/pre-release.sh
|
||||||
|
|
||||||
|
# Создание релиза
|
||||||
|
./shell-code/release.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📁 Структура релиза
|
||||||
|
|
||||||
|
После создания релиза в директории `dist/` будут созданы:
|
||||||
|
|
||||||
|
```
|
||||||
|
dist/
|
||||||
|
├── artifacts.json # Метаданные артефактов
|
||||||
|
├── CHANGELOG.md # Автоматически созданный changelog
|
||||||
|
├── config.yaml # Конфигурация GoReleaser
|
||||||
|
├── digests.txt # Хеши файлов
|
||||||
|
├── go-lcg_2.0.1_checksums.txt
|
||||||
|
├── go-lcg_Darwin_arm64.tar.gz
|
||||||
|
├── go-lcg_Darwin_x86_64.tar.gz
|
||||||
|
├── go-lcg_Linux_arm64.tar.gz
|
||||||
|
├── go-lcg_Linux_i386.tar.gz
|
||||||
|
├── go-lcg_Linux_x86_64.tar.gz
|
||||||
|
├── go-lcg_Windows_arm64.zip
|
||||||
|
├── go-lcg_Windows_i386.zip
|
||||||
|
├── go-lcg_Windows_x86_64.zip
|
||||||
|
└── metadata.json # Метаданные релиза
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔍 Устранение проблем
|
||||||
|
|
||||||
|
### Правильные флаги GoReleaser
|
||||||
|
|
||||||
|
**Важно**: В современных версиях GoReleaser флаг `--skip-publish` больше не поддерживается. Используйте:
|
||||||
|
|
||||||
|
- `--clean` - очищает директорию `dist/` перед сборкой
|
||||||
|
- `--snapshot` - создает тестовую сборку без создания тега
|
||||||
|
- `--debug` - подробный вывод для отладки
|
||||||
|
- `--skip-validate` - пропускает валидацию конфигурации
|
||||||
|
|
||||||
|
### Частые ошибки
|
||||||
|
|
||||||
|
#### 1. Ошибка аутентификации GitHub
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: failed to get GitHub token: missing github token
|
||||||
|
```
|
||||||
|
|
||||||
|
**Решение**: Установите `GITHUB_TOKEN` в переменные окружения.
|
||||||
|
|
||||||
|
#### 2. Ошибка создания тега
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: git tag v1.0.0 already exists
|
||||||
|
```
|
||||||
|
|
||||||
|
**Решение**: Удалите существующий тег или используйте другую версию.
|
||||||
|
|
||||||
|
#### 3. Ошибка сборки
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: failed to build for linux/amd64
|
||||||
|
```
|
||||||
|
|
||||||
|
**Решение**: Проверьте, что код компилируется локально:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go build -o lcg .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Отладка
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Подробный вывод
|
||||||
|
goreleaser release --debug
|
||||||
|
|
||||||
|
# Проверка конфигурации
|
||||||
|
goreleaser check
|
||||||
|
|
||||||
|
# Только сборка без релиза
|
||||||
|
goreleaser build
|
||||||
|
|
||||||
|
# Создание релиза без публикации (только локальная сборка)
|
||||||
|
goreleaser release --clean
|
||||||
|
|
||||||
|
# Создание snapshot релиза без публикации
|
||||||
|
goreleaser release --snapshot --clean
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 Лучшие практики
|
||||||
|
|
||||||
|
### 1. Версионирование
|
||||||
|
|
||||||
|
- Используйте семантическое версионирование (SemVer)
|
||||||
|
- Обновляйте `VERSION.txt` перед созданием релиза
|
||||||
|
- Создавайте теги в формате `v1.0.0`
|
||||||
|
|
||||||
|
### 2. Changelog
|
||||||
|
|
||||||
|
- Пишите понятные commit messages
|
||||||
|
- Используйте conventional commits для автоматического changelog
|
||||||
|
- Исключайте технические коммиты из changelog
|
||||||
|
|
||||||
|
### 3. Тестирование
|
||||||
|
|
||||||
|
- Всегда тестируйте snapshot релизы перед полным релизом
|
||||||
|
- Проверяйте сборки на разных платформах
|
||||||
|
- Тестируйте установку из релиза
|
||||||
|
|
||||||
|
### 4. Безопасность
|
||||||
|
|
||||||
|
- Никогда не коммитьте токены в репозиторий
|
||||||
|
- Используйте GitHub Secrets для CI/CD
|
||||||
|
- Регулярно обновляйте токены доступа
|
||||||
|
|
||||||
|
## 🎯 Пример полного процесса
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Подготовка
|
||||||
|
git checkout main
|
||||||
|
git pull origin main
|
||||||
|
|
||||||
|
# 2. Обновление версии
|
||||||
|
echo "v2.0.2" > VERSION.txt
|
||||||
|
git add VERSION.txt
|
||||||
|
git commit -m "chore: bump version to v2.0.2"
|
||||||
|
|
||||||
|
# 3. Создание тега
|
||||||
|
git tag v2.0.2
|
||||||
|
git push origin v2.0.2
|
||||||
|
|
||||||
|
# 4. Создание релиза
|
||||||
|
export GITHUB_TOKEN="your_token"
|
||||||
|
goreleaser release
|
||||||
|
|
||||||
|
# 5. Проверка
|
||||||
|
ls -la dist/
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📚 Дополнительные ресурсы
|
||||||
|
|
||||||
|
- [Официальная документация GoReleaser](https://goreleaser.com/)
|
||||||
|
- [Примеры конфигураций](https://github.com/goreleaser/goreleaser/tree/main/examples)
|
||||||
|
- [GitHub Actions для GoReleaser](https://github.com/goreleaser/goreleaser-action)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Примечание**: Этот гайд создан специально для проекта `linux-command-gpt`. Для других проектов может потребоваться адаптация конфигурации.
|
||||||
258
docs/REVERSE_PROXY_GUIDE.md
Normal file
258
docs/REVERSE_PROXY_GUIDE.md
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
# 🔄 Гайд по настройке LCG за Reverse Proxy
|
||||||
|
|
||||||
|
## 📋 Переменные окружения для Reverse Proxy
|
||||||
|
|
||||||
|
### 🔧 **Основные настройки:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Включить аутентификацию
|
||||||
|
LCG_SERVER_REQUIRE_AUTH=true
|
||||||
|
|
||||||
|
# Настроить домен для cookies (опционально)
|
||||||
|
LCG_DOMAIN=.yourdomain.com
|
||||||
|
|
||||||
|
# Настроить путь для cookies (для префикса пути)
|
||||||
|
LCG_COOKIE_PATH=/lcg
|
||||||
|
|
||||||
|
# Управление Secure флагом cookies
|
||||||
|
LCG_COOKIE_SECURE=false
|
||||||
|
|
||||||
|
# Разрешить HTTP (для работы за reverse proxy)
|
||||||
|
LCG_SERVER_ALLOW_HTTP=true
|
||||||
|
|
||||||
|
# Настроить хост и порт
|
||||||
|
LCG_SERVER_HOST=0.0.0.0
|
||||||
|
LCG_SERVER_PORT=8080
|
||||||
|
|
||||||
|
# Пароль для входа (по умолчанию: admin#123456)
|
||||||
|
LCG_SERVER_PASSWORD=your_secure_password
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 **Запуск за Reverse Proxy**
|
||||||
|
|
||||||
|
### **1. Nginx конфигурация:**
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name yourdomain.com;
|
||||||
|
|
||||||
|
# SSL настройки
|
||||||
|
ssl_certificate /path/to/cert.pem;
|
||||||
|
ssl_certificate_key /path/to/key.pem;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://localhost:8080;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
# Важно для работы cookies
|
||||||
|
proxy_cookie_domain localhost yourdomain.com;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### **2. Apache конфигурация:**
|
||||||
|
|
||||||
|
```apache
|
||||||
|
<VirtualHost *:443>
|
||||||
|
ServerName yourdomain.com
|
||||||
|
SSLEngine on
|
||||||
|
SSLCertificateFile /path/to/cert.pem
|
||||||
|
SSLCertificateKeyFile /path/to/key.pem
|
||||||
|
|
||||||
|
ProxyPreserveHost On
|
||||||
|
ProxyPass / http://localhost:8080/
|
||||||
|
ProxyPassReverse / http://localhost:8080/
|
||||||
|
|
||||||
|
# Настройки для cookies
|
||||||
|
ProxyPassReverseCookieDomain localhost yourdomain.com
|
||||||
|
</VirtualHost>
|
||||||
|
```
|
||||||
|
|
||||||
|
### **3. Caddy конфигурация:**
|
||||||
|
|
||||||
|
```caddy
|
||||||
|
yourdomain.com {
|
||||||
|
reverse_proxy localhost:8080 {
|
||||||
|
header_up Host {host}
|
||||||
|
header_up X-Real-IP {remote}
|
||||||
|
header_up X-Forwarded-For {remote}
|
||||||
|
header_up X-Forwarded-Proto {scheme}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🏃♂️ **Команды запуска**
|
||||||
|
|
||||||
|
### **Базовый запуск:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
LCG_SERVER_REQUIRE_AUTH=true LCG_SERVER_ALLOW_HTTP=true ./lcg serve -H 0.0.0.0 -p 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
### **С настройкой домена:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
LCG_SERVER_REQUIRE_AUTH=true \
|
||||||
|
LCG_SERVER_ALLOW_HTTP=true \
|
||||||
|
LCG_DOMAIN=.yourdomain.com \
|
||||||
|
LCG_COOKIE_SECURE=false \
|
||||||
|
./lcg serve -H 0.0.0.0 -p 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
### **С кастомным паролем:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
LCG_SERVER_REQUIRE_AUTH=true \
|
||||||
|
LCG_SERVER_ALLOW_HTTP=true \
|
||||||
|
LCG_SERVER_PASSWORD=my_secure_password \
|
||||||
|
LCG_DOMAIN=.yourdomain.com \
|
||||||
|
./lcg serve -H 0.0.0.0 -p 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔒 **Безопасность**
|
||||||
|
|
||||||
|
### **Рекомендуемые настройки:**
|
||||||
|
|
||||||
|
- ✅ `LCG_SERVER_REQUIRE_AUTH=true` - всегда включайте аутентификацию
|
||||||
|
- ✅ `LCG_COOKIE_SECURE=false` - для HTTP за reverse proxy
|
||||||
|
- ✅ `LCG_DOMAIN=.yourdomain.com` - для правильной работы cookies
|
||||||
|
- ✅ Сильный пароль в `LCG_SERVER_PASSWORD`
|
||||||
|
|
||||||
|
### **Настройки Reverse Proxy:**
|
||||||
|
|
||||||
|
- ✅ Передавайте заголовки `X-Forwarded-*`
|
||||||
|
- ✅ Настройте `proxy_cookie_domain` в Nginx
|
||||||
|
- ✅ Используйте HTTPS на уровне reverse proxy
|
||||||
|
|
||||||
|
## 🐳 **Docker Compose пример**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
lcg:
|
||||||
|
image: your-lcg-image
|
||||||
|
environment:
|
||||||
|
- LCG_SERVER_REQUIRE_AUTH=true
|
||||||
|
- LCG_SERVER_ALLOW_HTTP=true
|
||||||
|
- LCG_DOMAIN=.yourdomain.com
|
||||||
|
- LCG_COOKIE_SECURE=false
|
||||||
|
- LCG_SERVER_PASSWORD=secure_password
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
image: nginx:alpine
|
||||||
|
volumes:
|
||||||
|
- ./nginx.conf:/etc/nginx/nginx.conf
|
||||||
|
- ./ssl:/etc/nginx/ssl
|
||||||
|
ports:
|
||||||
|
- "443:443"
|
||||||
|
depends_on:
|
||||||
|
- lcg
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔍 **Диагностика проблем**
|
||||||
|
|
||||||
|
### **Проверка cookies:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Проверить установку cookies
|
||||||
|
curl -I https://yourdomain.com/login
|
||||||
|
|
||||||
|
# Проверить домен cookies
|
||||||
|
curl -v https://yourdomain.com/login 2>&1 | grep -i cookie
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Логи приложения:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Запуск с debug режимом
|
||||||
|
LCG_SERVER_REQUIRE_AUTH=true \
|
||||||
|
LCG_SERVER_ALLOW_HTTP=true \
|
||||||
|
./lcg -d serve -H 0.0.0.0 -p 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 **Примечания**
|
||||||
|
|
||||||
|
- **SameSite=Lax** - более мягкий режим для reverse proxy
|
||||||
|
- **Domain cookies** - работают только с указанным доменом
|
||||||
|
- **Secure=false** - обязательно для HTTP за reverse proxy
|
||||||
|
- **X-Forwarded-* заголовки** - важны для правильной работы
|
||||||
|
|
||||||
|
## 🆘 **Частые проблемы**
|
||||||
|
|
||||||
|
1. **Cookies не работают** → Проверьте `LCG_DOMAIN` и настройки reverse proxy
|
||||||
|
2. **Ошибка 403 CSRF** → Проверьте передачу cookies через reverse proxy
|
||||||
|
3. **Не работает аутентификация** → Убедитесь что `LCG_SERVER_REQUIRE_AUTH=true`
|
||||||
|
4. **Проблемы с HTTPS** → Настройте `LCG_COOKIE_SECURE=false` для HTTP за reverse proxy
|
||||||
|
|
||||||
|
## 🛣️ **Конфигурация с префиксом пути**
|
||||||
|
|
||||||
|
### **Пример: example.com/lcg**
|
||||||
|
|
||||||
|
#### **Переменные окружения для префикса:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
LCG_SERVER_REQUIRE_AUTH=true \
|
||||||
|
LCG_SERVER_ALLOW_HTTP=true \
|
||||||
|
LCG_DOMAIN=.example.com \
|
||||||
|
LCG_COOKIE_PATH=/lcg \
|
||||||
|
LCG_COOKIE_SECURE=false \
|
||||||
|
./lcg serve -H 0.0.0.0 -p 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **Nginx с префиксом:**
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name example.com;
|
||||||
|
|
||||||
|
location /lcg/ {
|
||||||
|
proxy_pass http://localhost:8080/;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
# Важно для работы cookies с префиксом
|
||||||
|
proxy_cookie_domain localhost example.com;
|
||||||
|
proxy_cookie_path / /lcg/;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **Apache с префиксом:**
|
||||||
|
|
||||||
|
```apache
|
||||||
|
<VirtualHost *:443>
|
||||||
|
ServerName example.com
|
||||||
|
SSLEngine on
|
||||||
|
|
||||||
|
ProxyPreserveHost On
|
||||||
|
ProxyPass /lcg/ http://localhost:8080/
|
||||||
|
ProxyPassReverse /lcg/ http://localhost:8080/
|
||||||
|
|
||||||
|
# Настройки для cookies с префиксом
|
||||||
|
ProxyPassReverseCookieDomain localhost example.com
|
||||||
|
ProxyPassReverseCookiePath / /lcg/
|
||||||
|
</VirtualHost>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **Caddy с префиксом:**
|
||||||
|
|
||||||
|
```caddy
|
||||||
|
example.com {
|
||||||
|
reverse_proxy /lcg/* localhost:8080 {
|
||||||
|
header_up Host {host}
|
||||||
|
header_up X-Real-IP {remote}
|
||||||
|
header_up X-Forwarded-For {remote}
|
||||||
|
header_up X-Forwarded-Proto {scheme}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
118
docs/ROADMAP.md
Normal file
118
docs/ROADMAP.md
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
# Дорожная карта развития (функциональность и безопасность)
|
||||||
|
|
||||||
|
Документ описывает план развития проекта на ближайшие релизы с фокусом на улучшение функциональности и усиление безопасности.
|
||||||
|
|
||||||
|
## Принципы
|
||||||
|
|
||||||
|
- Безопасность по умолчанию: новые возможности включают безопасные дефолты, опционально ослабляются.
|
||||||
|
- Обратная совместимость: не ломать существующие сценарии CLI и API.
|
||||||
|
- Прозрачность: чёткие Changelog, версии по SemVer, миграции и откаты.
|
||||||
|
- Качество: тесты, линтеры, аудит зависимостей, автоматизация релизов.
|
||||||
|
|
||||||
|
## Вехи и цели
|
||||||
|
|
||||||
|
### v2.1.0 — Формализация API и UX улучшения
|
||||||
|
|
||||||
|
- REST API
|
||||||
|
- Описать `POST /execute` в OpenAPI (swagger.yaml/json) и приложить в репозитории.
|
||||||
|
- Валидация входа по схеме: обязательные поля, ограничения длины, лимит размера тела.
|
||||||
|
- Явные коды ошибок и структура ответа (коды/сообщения).
|
||||||
|
- Безопасность API (первый этап)
|
||||||
|
- Дополнить защиту: ограничение размера тела (например, 64KB), тайм-ауты на чтение/запись.
|
||||||
|
- Rate limit (встроенный простой токен-бакет, по IP). Конфиг через env.
|
||||||
|
- Логирование попыток доступа и ошибок API (с редактированием PII).
|
||||||
|
- Веб-интерфейс
|
||||||
|
- Улучшения мобильной версии (доступность, контраст, a11y-метки).
|
||||||
|
- Переключатель темы (light/dark), сохранение предпочтений.
|
||||||
|
- Промпты
|
||||||
|
- Экспорт/импорт системных промптов (JSON) из UI/CLI.
|
||||||
|
- Превью при редактировании промптов в UI.
|
||||||
|
- Документация
|
||||||
|
- `API_GUIDE.md`: синхронизировать с OpenAPI.
|
||||||
|
- `USAGE_GUIDE.md`: добавить раздел «Ограничения API и лимиты».
|
||||||
|
|
||||||
|
### v2.2.0 — Усиление безопасности и управление доступом
|
||||||
|
|
||||||
|
- Аутентификация/Авторизация для веб-сервера
|
||||||
|
- Ввести токен доступа для API: `LCG_SERVER_TOKEN` (Bearer), отключаемо.
|
||||||
|
- Сессии UI (опционально): cookie HttpOnly + SameSite=strict, CSRF-защита форм.
|
||||||
|
- CORS: явный список разрешённых Origin через `LCG_CORS_ORIGINS`.
|
||||||
|
- Транспорт и заголовки безопасности
|
||||||
|
- Рекомендации по TLS терминации (пример конфигов nginx/caddy) в `serve/README.md`.
|
||||||
|
- Security headers: CSP, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, HSTS (при HTTPS).
|
||||||
|
- Хранилище и история
|
||||||
|
- Опциональное шифрование истории на диске (`LCG_HISTORY_ENCRYPTION_KEY_FILE`).
|
||||||
|
- Права на файлы истории и результатов: 0600, директории 0700.
|
||||||
|
- Настраиваемая ретенция истории (дни/размер). Авто-очистка.
|
||||||
|
- Наблюдаемость
|
||||||
|
- Аудит-лог действий UI/API с маскированием чувствительных данных.
|
||||||
|
- Включаемые/отключаемые метрики (prometheus endpoint — опционально, по отдельному порту/токену).
|
||||||
|
|
||||||
|
### v2.3.0 — Расширяемость и производительность
|
||||||
|
|
||||||
|
- Плагины провайдеров
|
||||||
|
- Интерфейс адаптеров (провайдеры LLM), регистрация через конфиг/билд-теги.
|
||||||
|
- Образцы адаптеров и гайды по разработке.
|
||||||
|
- Производительность
|
||||||
|
- Пулы HTTP-клиентов, connection reuse, упреждающие таймауты на уровне контекста.
|
||||||
|
- Кэширование результатов подробных объяснений (опционально, по ключу запроса).
|
||||||
|
- Расширения API
|
||||||
|
- Пакетная обработка запросов (batch) с квотой.
|
||||||
|
- Пагинация и фильтрация для `/history` (если будет публичный REST).
|
||||||
|
- Дистрибуция
|
||||||
|
- Улучшения .goreleaser: публикация SBOM, подписи (cosign), детерминированные сборки.
|
||||||
|
- Готовые пакеты: deb/rpm, инструкции для brew/scoop (по возможности).
|
||||||
|
|
||||||
|
## Backlog (кандидаты)
|
||||||
|
|
||||||
|
- Потоковая генерация (stream) и WebSocket-канал (при наличии поддержки у провайдеров).
|
||||||
|
- Оффлайн-режим/кэширование моделей для локальных провайдеров.
|
||||||
|
- Расширенный поиск по результатам/истории, теги и сохранённые фильтры.
|
||||||
|
- Резервное копирование и восстановление каталога результатов/истории.
|
||||||
|
- Улучшение доступности (a11y), горячие клавиши, локализация интерфейса.
|
||||||
|
|
||||||
|
## Техническое качество
|
||||||
|
|
||||||
|
- Обновление стека
|
||||||
|
- Обновить Go (минимум 1.20+), пересобрать и протестировать совместимость.
|
||||||
|
- Регулярные обновления зависимостей и проверка уязвимостей (`govulncheck`).
|
||||||
|
- Линтеры и проверка качества
|
||||||
|
- Включить `golangci-lint`, `staticcheck`, `gosec` в CI.
|
||||||
|
- Форматирование и единый стиль, pre-commit хуки.
|
||||||
|
- Тесты
|
||||||
|
- Unit-тесты на `serve/*` (маршруты, валидация входных данных, заголовки).
|
||||||
|
- Интеграционные тесты API `/execute` (позитив/негатив, лимиты, токены).
|
||||||
|
- Фаззинг критичных функций парсинга/валидации.
|
||||||
|
- CI/CD
|
||||||
|
- GitHub Actions: сборка, тесты, линты, релизы. Генерация чек-сумм, подписи, SBOM.
|
||||||
|
- Автоматическая публикация релизов и проверок артефактов.
|
||||||
|
|
||||||
|
## Конфигурация (новые/уточняемые переменные)
|
||||||
|
|
||||||
|
- `LCG_SERVER_TOKEN` — токен доступа для API (Bearer). Отключаемый режим.
|
||||||
|
- `LCG_RATE_LIMIT` — глобальные лимиты (например, `60/m`, `5/s`).
|
||||||
|
- `LCG_CORS_ORIGINS` — список разрешённых Origin.
|
||||||
|
- `LCG_HISTORY_ENCRYPTION_KEY_FILE` — путь к ключу для шифрования истории (опц.).
|
||||||
|
- `LCG_MAX_BODY_BYTES` — максимальный размер тела запроса, байты (по умолчанию 65536).
|
||||||
|
- `LCG_BROWSER_PATH` — путь к браузеру для `--browser`.
|
||||||
|
|
||||||
|
## Политика релизов
|
||||||
|
|
||||||
|
- SemVer: MINOR — функционал без ломаний, PATCH — багфиксы/мелкие улучшения.
|
||||||
|
- Каждый релиз: обновлённый `CHANGELOG.txt`, теги `vX.Y.Z`, двуязычная документация (RU/EN при возможности).
|
||||||
|
- Security Advisories: отдельный раздел/ISSUE шаблон для отчётов об уязвимостях.
|
||||||
|
|
||||||
|
## Критерии приемки (примеры)
|
||||||
|
|
||||||
|
- v2.1.0: OpenAPI спецификация доступна, API валидируется по схеме, лимит размера тела и таймауты соблюдаются, добавлены тесты в CI.
|
||||||
|
- v2.2.0: Доступ к API с токеном включаем/отключаем через env; активированы security-заголовки; есть базовые правила CORS; аудит-лог включается флагом.
|
||||||
|
- v2.3.0: Пулы клиентов, бенчмарки показывают улучшение p95 латентности, есть механизм подключения новых провайдеров.
|
||||||
|
|
||||||
|
## Риски и смягчение
|
||||||
|
|
||||||
|
- Ломание совместимости при усилении безопасности → режим совместимости через env/флаги.
|
||||||
|
- Рост сложности конфигурации → шаблоны конфигов и «рецепты» в README/serve/README.md.
|
||||||
|
- Производительные регрессии из-за валидации/лимитов → профилирование и кэширование на горячих путях.
|
||||||
|
|
||||||
|
---
|
||||||
|
Последнее обновление: 2025-10-22
|
||||||
199
docs/SECURITY_FEATURES.md
Normal file
199
docs/SECURITY_FEATURES.md
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
# 🔒 Функции безопасности LCG
|
||||||
|
|
||||||
|
## 🛡️ Автоматическое принуждение к HTTPS
|
||||||
|
|
||||||
|
### Логика безопасности
|
||||||
|
|
||||||
|
Приложение автоматически определяет, нужно ли использовать HTTPS:
|
||||||
|
|
||||||
|
1. **Небезопасные хосты** (не localhost/127.0.0.1) → **принудительно HTTPS**
|
||||||
|
2. **Безопасные хосты** (localhost/127.0.0.1) → HTTP (если не указано иное)
|
||||||
|
3. **Переменная `LCG_SERVER_ALLOW_HTTP=true`** → разрешает HTTP для любых хостов
|
||||||
|
|
||||||
|
### Примеры
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Небезопасно - принудительно HTTPS
|
||||||
|
LCG_SERVER_HOST=192.168.1.100 lcg serve
|
||||||
|
# Результат: https://192.168.1.100:8080
|
||||||
|
|
||||||
|
# Безопасно - HTTP по умолчанию
|
||||||
|
LCG_SERVER_HOST=localhost lcg serve
|
||||||
|
# Результат: http://localhost:8080
|
||||||
|
|
||||||
|
# Принудительно HTTP для любого хоста
|
||||||
|
LCG_SERVER_HOST=192.168.1.100 LCG_SERVER_ALLOW_HTTP=true lcg serve
|
||||||
|
# Результат: http://192.168.1.100:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔐 SSL/TLS сертификаты
|
||||||
|
|
||||||
|
### Автоматическая генерация
|
||||||
|
|
||||||
|
Приложение автоматически генерирует самоподписанный сертификат если:
|
||||||
|
|
||||||
|
1. Не указаны переменные `LCG_SERVER_SSL_CERT_FILE` и `LCG_SERVER_SSL_KEY_FILE`
|
||||||
|
2. Не найдены файлы в `~/.config/lcg/server/ssl/cert.pem` и `~/.config/lcg/server/ssl/key.pem`
|
||||||
|
|
||||||
|
### Расположение сертификатов
|
||||||
|
|
||||||
|
``` text
|
||||||
|
~/.config/lcg/
|
||||||
|
├── config/
|
||||||
|
│ └── server/
|
||||||
|
│ └── ssl/
|
||||||
|
│ ├── cert.pem # Сертификат
|
||||||
|
│ └── key.pem # Приватный ключ
|
||||||
|
```
|
||||||
|
|
||||||
|
### Переменные окружения
|
||||||
|
|
||||||
|
| Переменная | Описание | По умолчанию |
|
||||||
|
|------------|----------|--------------|
|
||||||
|
| `LCG_CONFIG_FOLDER` | Папка конфигурации | `~/.config/lcg/config` |
|
||||||
|
| `LCG_SERVER_ALLOW_HTTP` | Разрешить HTTP для любых хостов | `false` |
|
||||||
|
| `LCG_SERVER_SSL_CERT_FILE` | Путь к сертификату | `""` (авто) |
|
||||||
|
| `LCG_SERVER_SSL_KEY_FILE` | Путь к ключу | `""` (авто) |
|
||||||
|
|
||||||
|
## 🚀 Примеры использования
|
||||||
|
|
||||||
|
### Безопасный режим (по умолчанию)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Локальный сервер - HTTP
|
||||||
|
lcg serve
|
||||||
|
|
||||||
|
# Внешний сервер - принудительно HTTPS
|
||||||
|
LCG_SERVER_HOST=192.168.1.100 lcg serve
|
||||||
|
```
|
||||||
|
|
||||||
|
### Настройка SSL сертификатов
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Использовать собственные сертификаты
|
||||||
|
LCG_SERVER_SSL_CERT_FILE=/path/to/cert.pem \
|
||||||
|
LCG_SERVER_SSL_KEY_FILE=/path/to/key.pem \
|
||||||
|
lcg serve
|
||||||
|
|
||||||
|
# Разрешить HTTP для внешних хостов
|
||||||
|
LCG_SERVER_HOST=192.168.1.100 \
|
||||||
|
LCG_SERVER_ALLOW_HTTP=true \
|
||||||
|
lcg serve
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker контейнер
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
FROM golang:1.21-alpine AS builder
|
||||||
|
# ... build steps ...
|
||||||
|
|
||||||
|
FROM alpine:latest
|
||||||
|
COPY --from=builder /app/lcg /usr/local/bin/
|
||||||
|
ENV LCG_SERVER_HOST=0.0.0.0
|
||||||
|
ENV LCG_SERVER_ALLOW_HTTP=false
|
||||||
|
CMD ["lcg", "serve"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Systemd сервис
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=LCG Server
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=lcg
|
||||||
|
WorkingDirectory=/opt/lcg
|
||||||
|
ExecStart=/opt/lcg/lcg serve
|
||||||
|
Environment=LCG_SERVER_HOST=0.0.0.0
|
||||||
|
Environment=LCG_SERVER_ALLOW_HTTP=false
|
||||||
|
Restart=always
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Технические детали
|
||||||
|
|
||||||
|
### Генерация сертификата
|
||||||
|
|
||||||
|
Самоподписанный сертификат генерируется с:
|
||||||
|
|
||||||
|
- **Размер ключа**: 2048 бит RSA
|
||||||
|
- **Срок действия**: 1 год
|
||||||
|
- **Поддерживаемые хосты**: localhost, 127.0.0.1, указанный хост
|
||||||
|
- **Использование**: Server Authentication
|
||||||
|
|
||||||
|
### Безопасные хосты
|
||||||
|
|
||||||
|
Следующие хосты считаются безопасными для HTTP:
|
||||||
|
|
||||||
|
- `localhost`
|
||||||
|
- `127.0.0.1`
|
||||||
|
- `::1` (IPv6 localhost)
|
||||||
|
|
||||||
|
### Проверка безопасности
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Проверка хоста
|
||||||
|
if !ssl.IsSecureHost(host) {
|
||||||
|
// Принудительно HTTPS
|
||||||
|
useHTTPS = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка разрешения HTTP
|
||||||
|
if config.AppConfig.Server.AllowHTTP {
|
||||||
|
useHTTPS = false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠️ Отладка
|
||||||
|
|
||||||
|
### Проверка конфигурации
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Показать текущую конфигурацию
|
||||||
|
lcg config --full | jq '.server'
|
||||||
|
|
||||||
|
# Проверить SSL сертификаты
|
||||||
|
ls -la ~/.config/lcg/config/server/ssl/
|
||||||
|
|
||||||
|
# Проверить переменные окружения
|
||||||
|
env | grep LCG_SERVER
|
||||||
|
```
|
||||||
|
|
||||||
|
### Логи безопасности
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Запуск с отладкой
|
||||||
|
LCG_SERVER_HOST=192.168.1.100 lcg serve --debug
|
||||||
|
|
||||||
|
# Проверка SSL
|
||||||
|
openssl x509 -in ~/.config/lcg/config/server/ssl/cert.pem -text -noout
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚠️ Важные замечания
|
||||||
|
|
||||||
|
### Безопасность
|
||||||
|
|
||||||
|
1. **Самоподписанные сертификаты** - браузеры будут показывать предупреждение
|
||||||
|
2. **Продакшен** - используйте настоящие SSL сертификаты от CA
|
||||||
|
3. **Сетевой доступ** - HTTPS защищает трафик, но не аутентификацию
|
||||||
|
|
||||||
|
### Производительность
|
||||||
|
|
||||||
|
1. **HTTPS** - небольшая нагрузка на CPU для шифрования
|
||||||
|
2. **Сертификаты** - генерируются один раз, затем кэшируются
|
||||||
|
3. **Память** - сертификаты загружаются в память при запуске
|
||||||
|
|
||||||
|
## 📚 Связанные файлы
|
||||||
|
|
||||||
|
- `config/config.go` - конфигурация безопасности
|
||||||
|
- `ssl/ssl.go` - генерация и управление сертификатами
|
||||||
|
- `serve/serve.go` - HTTP/HTTPS сервер
|
||||||
|
- `SECURITY_FEATURES.md` - эта документация
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Результат**: Приложение теперь автоматически обеспечивает безопасность соединения в зависимости от конфигурации хоста!
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Что это
|
## Что это
|
||||||
|
|
||||||
Linux Command GPT (`lcg`) преобразует описание на естественном языке в готовую Linux‑команду. Инструмент поддерживает сменные провайдеры LLM (Ollama или Proxy), управление системными промптами, историю запросов, сохранение результатов, HTTP сервер для просмотра результатов и интерактивные действия над сгенерированной командой.
|
Linux Command GPT (`lcg`) преобразует описание на естественном языке в готовую команду для Linux или Windows. Инструмент автоматически определяет операционную систему и использует соответствующие промпты. Поддерживает сменные провайдеры LLM (Ollama или Proxy), управление системными промптами, историю запросов, сохранение результатов, HTTP сервер для просмотра результатов, аутентификацию, CSRF защиту, интерактивные действия над сгенерированной командой и деплой в Kubernetes.
|
||||||
|
|
||||||
## Требования
|
## Требования
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ sudo apt-get install xsel
|
|||||||
|
|
||||||
```bash
|
```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
|
cd ~/.linux-command-gpt
|
||||||
go build -o lcg
|
go build -o lcg
|
||||||
|
|
||||||
@@ -60,11 +60,18 @@ lcg --file /path/to/context.txt "хочу вывести список дирек
|
|||||||
Действия: (c)копировать, (s)сохранить, (r)перегенерировать, (e)выполнить, (v|vv|vvv)подробно, (n)ничего:
|
Действия: (c)копировать, (s)сохранить, (r)перегенерировать, (e)выполнить, (v|vv|vvv)подробно, (n)ничего:
|
||||||
```
|
```
|
||||||
|
|
||||||
### Что нового в 2.0.1
|
### Что нового в 2.0.14
|
||||||
|
|
||||||
- Улучшена мобильная версия веб‑интерфейса: корректные размеры кнопок, шрифтов и отступов; адаптивная верстка
|
- **Аутентификация**: Добавлена система аутентификации с JWT токенами и HTTP-only cookies
|
||||||
- Учитывается `prefers-reduced-motion` для снижения анимаций, если это задано в системе
|
- **CSRF защита**: Полная защита от CSRF атак с токенами и middleware
|
||||||
- Добавлен REST эндпоинт `POST /execute` (только через curl) — см. подробности и примеры в `API_GUIDE.md`
|
- **Безопасность**: Улучшенная безопасность с проверкой токенов и сессий
|
||||||
|
- **Kubernetes деплой**: Полный набор манифестов для деплоя в Kubernetes с Traefik
|
||||||
|
- **Flux CD**: GitOps конфигурация для автоматического деплоя
|
||||||
|
- **Reverse Proxy**: Поддержка работы за reverse proxy с настройкой cookies
|
||||||
|
- **Веб-интерфейс**: Улучшенный веб-интерфейс с современным дизайном
|
||||||
|
- **Мониторинг**: Prometheus метрики и ServiceMonitor
|
||||||
|
- **Масштабирование**: HPA для автоматического масштабирования
|
||||||
|
- **Тестирование**: Инструменты для тестирования CSRF защиты
|
||||||
|
|
||||||
## Переменные окружения
|
## Переменные окружения
|
||||||
|
|
||||||
@@ -90,6 +97,13 @@ lcg --file /path/to/context.txt "хочу вывести список дирек
|
|||||||
| `LCG_ALLOW_EXECUTION` | пусто | Если `1`/`true` — включает возможность выполнения команд через опцию `(e)` в меню действий. |
|
| `LCG_ALLOW_EXECUTION` | пусто | Если `1`/`true` — включает возможность выполнения команд через опцию `(e)` в меню действий. |
|
||||||
| `LCG_SERVER_PORT` | `8080` | Порт для HTTP сервера просмотра результатов. |
|
| `LCG_SERVER_PORT` | `8080` | Порт для HTTP сервера просмотра результатов. |
|
||||||
| `LCG_SERVER_HOST` | `localhost` | Хост для HTTP сервера просмотра результатов. |
|
| `LCG_SERVER_HOST` | `localhost` | Хост для HTTP сервера просмотра результатов. |
|
||||||
|
| `LCG_SERVER_REQUIRE_AUTH` | `false` | Требовать аутентификацию для доступа к веб-интерфейсу. |
|
||||||
|
| `LCG_SERVER_PASSWORD` | `admin#123456` | Пароль для аутентификации. |
|
||||||
|
| `LCG_COOKIE_SECURE` | `false` | Использовать Secure флаг для cookies (для HTTPS). |
|
||||||
|
| `LCG_DOMAIN` | пусто | Домен для cookies (для reverse proxy). |
|
||||||
|
| `LCG_COOKIE_PATH` | `/` | Путь для cookies (для reverse proxy). |
|
||||||
|
| `LCG_COOKIE_TTL_HOURS` | `168` | Время жизни cookies в часах (по умолчанию 7 дней). |
|
||||||
|
| `LCG_CSRF_SECRET` | пусто | Секрет для CSRF токенов (генерируется автоматически). |
|
||||||
|
|
||||||
Примеры настройки:
|
Примеры настройки:
|
||||||
|
|
||||||
@@ -104,6 +118,14 @@ export LCG_PROVIDER=proxy
|
|||||||
export LCG_HOST=http://localhost:8080
|
export LCG_HOST=http://localhost:8080
|
||||||
export LCG_MODEL=GigaChat-2
|
export LCG_MODEL=GigaChat-2
|
||||||
export LCG_JWT_TOKEN=your_jwt_token_here
|
export LCG_JWT_TOKEN=your_jwt_token_here
|
||||||
|
|
||||||
|
# Аутентификация и безопасность
|
||||||
|
export LCG_SERVER_REQUIRE_AUTH=true
|
||||||
|
export LCG_SERVER_PASSWORD=my_secure_password
|
||||||
|
export LCG_COOKIE_SECURE=false
|
||||||
|
export LCG_DOMAIN=.example.com
|
||||||
|
export LCG_COOKIE_PATH=/lcg
|
||||||
|
export LCG_COOKIE_TTL_HOURS=72 # 3 дня
|
||||||
```
|
```
|
||||||
|
|
||||||
## Базовый синтаксис
|
## Базовый синтаксис
|
||||||
@@ -146,6 +168,8 @@ lcg [глобальные опции] <описание команды>
|
|||||||
- `--port, -p` — порт сервера (по умолчанию из `LCG_SERVER_PORT`)
|
- `--port, -p` — порт сервера (по умолчанию из `LCG_SERVER_PORT`)
|
||||||
- `--host, -H` — хост сервера (по умолчанию из `LCG_SERVER_HOST`)
|
- `--host, -H` — хост сервера (по умолчанию из `LCG_SERVER_HOST`)
|
||||||
- `--browser, -b` — открыть браузер автоматически после старта
|
- `--browser, -b` — открыть браузер автоматически после старта
|
||||||
|
- `--require-auth` — включить аутентификацию (переопределяет `LCG_SERVER_REQUIRE_AUTH`)
|
||||||
|
- `--password` — пароль для аутентификации (переопределяет `LCG_SERVER_PASSWORD`)
|
||||||
|
|
||||||
### Подробные объяснения (v/vv/vvv)
|
### Подробные объяснения (v/vv/vvv)
|
||||||
|
|
||||||
@@ -196,6 +220,39 @@ lcg [глобальные опции] <описание команды>
|
|||||||
- Не вставляйте секреты в запросы. Перед выполнением (`e`) проверяйте команду вручную.
|
- Не вставляйте секреты в запросы. Перед выполнением (`e`) проверяйте команду вручную.
|
||||||
- Для структуры API см. `API_CONTRACT.md` (эндпоинты и форматы запросов/ответов).
|
- Для структуры API см. `API_CONTRACT.md` (эндпоинты и форматы запросов/ответов).
|
||||||
|
|
||||||
|
## Поддержка операционных систем
|
||||||
|
|
||||||
|
### Автоматическое определение ОС
|
||||||
|
|
||||||
|
Приложение автоматически определяет операционную систему и использует соответствующие промпты:
|
||||||
|
|
||||||
|
- **Linux/Unix системы** (включая macOS): используются промпты для Linux команд
|
||||||
|
- **Windows**: используются промпты для Windows команд (PowerShell, CMD, Batch)
|
||||||
|
|
||||||
|
### Промпты для Windows
|
||||||
|
|
||||||
|
На Windows системах доступны следующие встроенные промпты:
|
||||||
|
|
||||||
|
| ID | Name | Описание |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 1 | windows-command | Основной промпт для генерации Windows команд |
|
||||||
|
| 2 | windows-command-with-explanation | Промпт с подробным объяснением команд |
|
||||||
|
| 3 | windows-command-safe | Безопасный анализ команд с предупреждениями |
|
||||||
|
| 4 | windows-command-verbose | Подробный анализ с техническими деталями |
|
||||||
|
| 5 | windows-command-simple | Простое и понятное объяснение |
|
||||||
|
|
||||||
|
### Примеры использования на Windows
|
||||||
|
|
||||||
|
```cmd
|
||||||
|
# PowerShell команды
|
||||||
|
lcg "хочу получить список всех процессов"
|
||||||
|
lcg "показать информацию о дисках"
|
||||||
|
|
||||||
|
# CMD команды
|
||||||
|
lcg "создать папку test и перейти в неё"
|
||||||
|
lcg "найти все файлы .txt в текущей директории"
|
||||||
|
```
|
||||||
|
|
||||||
## Системные промпты
|
## Системные промпты
|
||||||
|
|
||||||
### Управление промптами
|
### Управление промптами
|
||||||
@@ -210,6 +267,9 @@ lcg [глобальные опции] <описание команды>
|
|||||||
|
|
||||||
### Встроенные промпты (ID 1–5)
|
### Встроенные промпты (ID 1–5)
|
||||||
|
|
||||||
|
Промпты автоматически выбираются в зависимости от операционной системы:
|
||||||
|
|
||||||
|
**Linux/Unix системы:**
|
||||||
| ID | Name | Описание |
|
| ID | Name | Описание |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| 1 | linux-command | «Ответь только Linux‑командой, без форматирования и объяснений». |
|
| 1 | linux-command | «Ответь только Linux‑командой, без форматирования и объяснений». |
|
||||||
@@ -218,6 +278,15 @@ lcg [глобальные опции] <описание команды>
|
|||||||
| 4 | linux-command-verbose | Команда с подробными объяснениями флагов и альтернатив. |
|
| 4 | linux-command-verbose | Команда с подробными объяснениями флагов и альтернатив. |
|
||||||
| 5 | linux-command-simple | Простые команды, избегать сложных опций. |
|
| 5 | linux-command-simple | Простые команды, избегать сложных опций. |
|
||||||
|
|
||||||
|
**Windows системы:**
|
||||||
|
| ID | Name | Описание |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 1 | windows-command | «Ответь только Windows‑командой, без форматирования и объяснений». |
|
||||||
|
| 2 | windows-command-with-explanation | Сгенерируй команду и кратко объясни, что она делает (формат: COMMAND: explanation). |
|
||||||
|
| 3 | windows-command-safe | Безопасные команды (без потери данных). Вывод — только команда. |
|
||||||
|
| 4 | windows-command-verbose | Команда с подробными объяснениями флагов и альтернатив. |
|
||||||
|
| 5 | windows-command-simple | Простые команды, избегать сложных опций. |
|
||||||
|
|
||||||
### Промпты подробности (ID 6–8)
|
### Промпты подробности (ID 6–8)
|
||||||
|
|
||||||
| ID | Name | Описание |
|
| ID | Name | Описание |
|
||||||
@@ -275,6 +344,12 @@ lcg serve
|
|||||||
- **Современный дизайн** — адаптивный интерфейс с карточками файлов
|
- **Современный дизайн** — адаптивный интерфейс с карточками файлов
|
||||||
- **Сортировка** — файлы отсортированы по дате изменения (новые сверху)
|
- **Сортировка** — файлы отсортированы по дате изменения (новые сверху)
|
||||||
- **Превью содержимого** — первые 200 символов каждого файла
|
- **Превью содержимого** — первые 200 символов каждого файла
|
||||||
|
- **Аутентификация** — защищенный доступ с JWT токенами
|
||||||
|
- **CSRF защита** — защита от межсайтовых атак
|
||||||
|
- **История запросов** (`/history`) — просмотр истории всех запросов
|
||||||
|
- **Управление промптами** (`/prompts`) — редактирование системных промптов
|
||||||
|
- **Выполнение команд** (`/run`) — интерактивное выполнение команд
|
||||||
|
- **Безопасность** — HTTP-only cookies, проверка токенов
|
||||||
|
|
||||||
Структура файла (команда):
|
Структура файла (команда):
|
||||||
|
|
||||||
@@ -358,6 +433,14 @@ lcg serve --port 9090
|
|||||||
|
|
||||||
# Запуск на всех интерфейсах
|
# Запуск на всех интерфейсах
|
||||||
lcg serve --host 0.0.0.0 --port 8080
|
lcg serve --host 0.0.0.0 --port 8080
|
||||||
|
|
||||||
|
# Запуск с аутентификацией
|
||||||
|
lcg serve --require-auth --password my_secure_password
|
||||||
|
|
||||||
|
# Запуск с переменными окружения
|
||||||
|
export LCG_SERVER_REQUIRE_AUTH=true
|
||||||
|
export LCG_SERVER_PASSWORD=admin#123456
|
||||||
|
lcg serve
|
||||||
```
|
```
|
||||||
|
|
||||||
## История
|
## История
|
||||||
@@ -377,6 +460,10 @@ lcg history list
|
|||||||
- Для `ollama`/`proxy` API‑ключ не нужен; команды `update-key`/`delete-key` просто уведомят об этом.
|
- Для `ollama`/`proxy` API‑ключ не нужен; команды `update-key`/`delete-key` просто уведомят об этом.
|
||||||
- HTTP сервер не запускается: проверьте, что порт свободен, используйте `--port` для смены порта.
|
- HTTP сервер не запускается: проверьте, что порт свободен, используйте `--port` для смены порта.
|
||||||
- Веб-интерфейс не отображает файлы: убедитесь, что в `LCG_RESULT_FOLDER` есть `.md` файлы.
|
- Веб-интерфейс не отображает файлы: убедитесь, что в `LCG_RESULT_FOLDER` есть `.md` файлы.
|
||||||
|
- **Аутентификация не работает**: проверьте `LCG_SERVER_REQUIRE_AUTH=true` и правильность пароля.
|
||||||
|
- **CSRF ошибки**: убедитесь, что токены передаются в заголовках `X-CSRF-Token`.
|
||||||
|
- **Cookies не сохраняются**: проверьте настройки `LCG_DOMAIN` и `LCG_COOKIE_PATH` для reverse proxy.
|
||||||
|
- **Kubernetes деплой не работает**: проверьте права доступа к кластеру и наличие всех манифестов.
|
||||||
|
|
||||||
## JSON‑история запросов
|
## JSON‑история запросов
|
||||||
|
|
||||||
@@ -408,17 +495,148 @@ lcg history list
|
|||||||
|
|
||||||
## Доступ к локальному API
|
## Доступ к локальному API
|
||||||
|
|
||||||
Эндпоинт: `POST /execute` (только через curl).
|
### Основные эндпоинты
|
||||||
|
|
||||||
|
- `POST /api/execute` — выполнение запросов к LLM
|
||||||
|
- `POST /api/save-result` — сохранение результатов
|
||||||
|
- `POST /api/add-to-history` — добавление в историю
|
||||||
|
- `GET /api/login` — страница аутентификации
|
||||||
|
- `POST /api/login` — аутентификация
|
||||||
|
- `POST /api/logout` — выход из системы
|
||||||
|
- `GET /metrics` — Prometheus метрики
|
||||||
|
|
||||||
|
### Примеры использования
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Запустить сервер
|
# Запустить сервер
|
||||||
lcg serve
|
lcg serve
|
||||||
|
|
||||||
# Выполнить запрос
|
# Выполнить запрос (без аутентификации)
|
||||||
curl -X POST http://localhost:8080/execute \
|
curl -X POST http://localhost:8080/api/execute \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-A curl \
|
-A curl \
|
||||||
-d '{"prompt": "create directory test", "verbose": "vv"}'
|
-d '{"prompt": "create directory test", "verbose": "vv"}'
|
||||||
|
|
||||||
|
# Аутентификация
|
||||||
|
curl -X POST http://localhost:8080/api/login \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"username": "admin", "password": "admin#123456"}'
|
||||||
|
|
||||||
|
# Выполнение с CSRF токеном
|
||||||
|
curl -X POST http://localhost:8080/api/execute \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-CSRF-Token: your_csrf_token" \
|
||||||
|
-H "Cookie: auth_token=your_jwt_token" \
|
||||||
|
-d '{"prompt": "create directory test"}'
|
||||||
```
|
```
|
||||||
|
|
||||||
Подробности и примеры: `API_GUIDE.md`.
|
Подробности и примеры: `API_GUIDE.md`.
|
||||||
|
|
||||||
|
## Kubernetes деплой
|
||||||
|
|
||||||
|
### Быстрый деплой
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Переход в папку деплоя
|
||||||
|
cd deploy
|
||||||
|
|
||||||
|
# Полный деплой (сборка + деплой + проверка)
|
||||||
|
./full-deploy.sh
|
||||||
|
|
||||||
|
# Или поэтапно
|
||||||
|
./build.sh lcg latest
|
||||||
|
./deploy.sh
|
||||||
|
./health-check.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Использование Make
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Справка
|
||||||
|
make help
|
||||||
|
|
||||||
|
# Сборка и деплой
|
||||||
|
make build
|
||||||
|
make deploy
|
||||||
|
|
||||||
|
# Мониторинг
|
||||||
|
make status
|
||||||
|
make logs
|
||||||
|
make monitor
|
||||||
|
|
||||||
|
# Удаление
|
||||||
|
make undeploy
|
||||||
|
```
|
||||||
|
|
||||||
|
### Flux CD (GitOps)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Настройка Flux CD
|
||||||
|
cd deploy/flux
|
||||||
|
./setup-flux.sh
|
||||||
|
|
||||||
|
# Создание Kustomization
|
||||||
|
./create_kustomization.sh
|
||||||
|
|
||||||
|
# Мониторинг
|
||||||
|
kubectl get kustomization lcg -n flux-system
|
||||||
|
```
|
||||||
|
|
||||||
|
### Конфигурация для reverse proxy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Настройка для работы за reverse proxy
|
||||||
|
export LCG_SERVER_REQUIRE_AUTH=true
|
||||||
|
export LCG_SERVER_ALLOW_HTTP=true
|
||||||
|
export LCG_DOMAIN=.example.com
|
||||||
|
export LCG_COOKIE_PATH=/lcg
|
||||||
|
export LCG_COOKIE_SECURE=false
|
||||||
|
|
||||||
|
# Запуск
|
||||||
|
./lcg serve -H 0.0.0.0 -p 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
### Мониторинг и безопасность
|
||||||
|
|
||||||
|
- **Prometheus метрики**: `/metrics` endpoint
|
||||||
|
- **Health checks**: автоматические проверки готовности
|
||||||
|
- **HPA**: автоматическое масштабирование (2-10 replicas)
|
||||||
|
- **CSRF защита**: токены для всех POST запросов
|
||||||
|
- **Аутентификация**: JWT токены в HTTP-only cookies
|
||||||
|
- **Security context**: non-root пользователь, минимальные права
|
||||||
|
|
||||||
|
Подробности: `deploy/README.md` и `deploy/flux/README.md`.
|
||||||
|
|
||||||
|
## Тестирование CSRF защиты
|
||||||
|
|
||||||
|
### Автоматическое тестирование
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Запуск тестов CSRF защиты
|
||||||
|
./test_csrf.sh
|
||||||
|
|
||||||
|
# Проверка результатов
|
||||||
|
echo "Проверьте вывод на наличие ошибок 403 Forbidden"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ручное тестирование
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Откройте csrf_test.html в браузере
|
||||||
|
open csrf_test.html
|
||||||
|
|
||||||
|
# Или используйте curl для тестирования
|
||||||
|
curl -X POST http://localhost:8080/api/execute \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"prompt": "test"}' \
|
||||||
|
-v
|
||||||
|
```
|
||||||
|
|
||||||
|
### Демонстрация уязвимости
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Откройте csrf_demo.html для демонстрации атаки
|
||||||
|
open csrf_demo.html
|
||||||
|
```
|
||||||
|
|
||||||
|
Подробности: `CSRF_TESTING_GUIDE.md`.
|
||||||
205
docs/VALIDATION_CONFIG.md
Normal file
205
docs/VALIDATION_CONFIG.md
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
# 🔧 Конфигурация валидации длины полей
|
||||||
|
|
||||||
|
## 📋 Переменные окружения
|
||||||
|
|
||||||
|
Все настройки валидации можно настроить через переменные окружения:
|
||||||
|
|
||||||
|
### Основные лимиты
|
||||||
|
|
||||||
|
| Переменная | Описание | По умолчанию |
|
||||||
|
|------------|----------|--------------|
|
||||||
|
| `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
|
||||||
|
# Увеличить лимит системного промпта до 3к символов
|
||||||
|
export LCG_MAX_SYSTEM_PROMPT_LENGTH=3000
|
||||||
|
|
||||||
|
# Уменьшить лимит пользовательского сообщения до 2к символов
|
||||||
|
export LCG_MAX_USER_MESSAGE_LENGTH=2000
|
||||||
|
|
||||||
|
# Увеличить лимит названия промпта до 3000 символов
|
||||||
|
export LCG_MAX_PROMPT_NAME_LENGTH=3000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Установка в .env файле
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .env файл
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
### Установка в systemd сервисе
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=Linux Command GPT
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=lcg
|
||||||
|
WorkingDirectory=/opt/lcg
|
||||||
|
ExecStart=/opt/lcg/lcg serve
|
||||||
|
Environment=LCG_MAX_SYSTEM_PROMPT_LENGTH=2000
|
||||||
|
Environment=LCG_MAX_USER_MESSAGE_LENGTH=4000
|
||||||
|
Environment=LCG_MAX_PROMPT_NAME_LENGTH=2000
|
||||||
|
Restart=always
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
### Установка в Docker
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
FROM golang:1.21-alpine AS builder
|
||||||
|
# ... build steps ...
|
||||||
|
|
||||||
|
FROM alpine:latest
|
||||||
|
COPY --from=builder /app/lcg /usr/local/bin/
|
||||||
|
ENV LCG_MAX_SYSTEM_PROMPT_LENGTH=2000
|
||||||
|
ENV LCG_MAX_USER_MESSAGE_LENGTH=4000
|
||||||
|
CMD ["lcg", "serve"]
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# docker-compose.yml
|
||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
lcg:
|
||||||
|
image: lcg:latest
|
||||||
|
environment:
|
||||||
|
- LCG_MAX_SYSTEM_PROMPT_LENGTH=2000
|
||||||
|
- LCG_MAX_USER_MESSAGE_LENGTH=4000
|
||||||
|
- LCG_MAX_PROMPT_NAME_LENGTH=2000
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔍 Где применяется валидация
|
||||||
|
|
||||||
|
### 1. Консольная часть (main.go)
|
||||||
|
- ✅ Валидация пользовательского сообщения
|
||||||
|
- ✅ Валидация системного промпта
|
||||||
|
- ✅ Цветные сообщения об ошибках
|
||||||
|
|
||||||
|
### 2. API эндпоинты
|
||||||
|
- ✅ `/execute` - валидация промпта и системного промпта
|
||||||
|
- ✅ `/api/save-result` - валидация всех полей
|
||||||
|
- ✅ `/api/add-to-history` - валидация всех полей
|
||||||
|
|
||||||
|
### 3. Веб-интерфейс
|
||||||
|
- ✅ Страница выполнения - валидация в JavaScript и на сервере
|
||||||
|
- ✅ Управление промптами - валидация всех полей формы
|
||||||
|
|
||||||
|
### 4. JavaScript валидация
|
||||||
|
- ✅ Клиентская валидация перед отправкой
|
||||||
|
- ✅ Динамические лимиты из конфигурации
|
||||||
|
- ✅ Понятные сообщения об ошибках
|
||||||
|
|
||||||
|
## 🛠️ Технические детали
|
||||||
|
|
||||||
|
### Структура конфигурации
|
||||||
|
|
||||||
|
```go
|
||||||
|
type ValidationConfig struct {
|
||||||
|
MaxSystemPromptLength int // LCG_MAX_SYSTEM_PROMPT_LENGTH
|
||||||
|
MaxUserMessageLength int // LCG_MAX_USER_MESSAGE_LENGTH
|
||||||
|
MaxPromptNameLength int // LCG_MAX_PROMPT_NAME_LENGTH
|
||||||
|
MaxPromptDescLength int // LCG_MAX_PROMPT_DESC_LENGTH
|
||||||
|
MaxCommandLength int // LCG_MAX_COMMAND_LENGTH
|
||||||
|
MaxExplanationLength int // LCG_MAX_EXPLANATION_LENGTH
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Функции валидации
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Основные функции
|
||||||
|
validation.ValidateSystemPrompt(prompt)
|
||||||
|
validation.ValidateUserMessage(message)
|
||||||
|
validation.ValidatePromptName(name)
|
||||||
|
validation.ValidatePromptDescription(description)
|
||||||
|
validation.ValidateCommand(command)
|
||||||
|
validation.ValidateExplanation(explanation)
|
||||||
|
|
||||||
|
// Вспомогательные функции
|
||||||
|
validation.TruncateSystemPrompt(prompt)
|
||||||
|
validation.TruncateUserMessage(message)
|
||||||
|
validation.FormatLengthInfo(systemPrompt, userMessage)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Обработка ошибок
|
||||||
|
|
||||||
|
- **API**: HTTP 400 с JSON сообщением об ошибке
|
||||||
|
- **Веб-интерфейс**: HTTP 400 с текстовым сообщением
|
||||||
|
- **Консоль**: Цветные сообщения об ошибках
|
||||||
|
- **JavaScript**: Alert с предупреждением
|
||||||
|
|
||||||
|
## 📝 Примеры сообщений об ошибках
|
||||||
|
|
||||||
|
```
|
||||||
|
❌ Ошибка: system_prompt: системный промпт слишком длинный: 2500 символов (максимум 2000)
|
||||||
|
❌ Ошибка: user_message: пользовательское сообщение слишком длинное: 4500 символов (максимум 4000)
|
||||||
|
❌ Ошибка: prompt_name: название промпта слишком длинное: 2500 символов (максимум 2000)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 Миграция с жестко заданных значений
|
||||||
|
|
||||||
|
Если ранее использовались жестко заданные значения в коде, теперь они автоматически заменяются на значения из конфигурации:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Старый код
|
||||||
|
if len(prompt) > 2000 {
|
||||||
|
return errors.New("too long")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Новый код
|
||||||
|
if err := validation.ValidateSystemPrompt(prompt); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Рекомендации по настройке
|
||||||
|
|
||||||
|
### Для разработки
|
||||||
|
```bash
|
||||||
|
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=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=1000
|
||||||
|
export LCG_MAX_USER_MESSAGE_LENGTH=2000
|
||||||
|
export LCG_MAX_PROMPT_NAME_LENGTH=1000
|
||||||
|
export LCG_MAX_PROMPT_DESC_LENGTH=2500
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Примечание**: Все значения настраиваются через переменные окружения и применяются ко всем частям приложения (консоль, веб-интерфейс, API).
|
||||||
63
docs/VERBOSE_PROMPT_EDITING.md
Normal file
63
docs/VERBOSE_PROMPT_EDITING.md
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# Редактирование промптов подробности
|
||||||
|
|
||||||
|
## 🎯 Реализованная функциональность
|
||||||
|
|
||||||
|
### ✅ **Что добавлено:**
|
||||||
|
|
||||||
|
1. **Функция редактирования в JavaScript:**
|
||||||
|
- `editVerbosePrompt(mode, content)` - открывает форму редактирования для промптов подробности
|
||||||
|
- Автоматически заполняет поля формы данными промпта
|
||||||
|
- Показывает режим в заголовке формы
|
||||||
|
|
||||||
|
2. **Обработчик на сервере:**
|
||||||
|
- `handleEditVerbosePrompt()` - новый обработчик для маршрута `/prompts/edit-verbose/`
|
||||||
|
- Поддерживает режимы: `v`, `vv`, `vvv`
|
||||||
|
- Валидация всех полей с использованием `validation` пакета
|
||||||
|
- Обновление промптов через `PromptManager`
|
||||||
|
|
||||||
|
3. **Маршрутизация:**
|
||||||
|
- Добавлен маршрут `/prompts/edit-verbose/` в `serve.go`
|
||||||
|
- Поддержка HTTP методов PUT
|
||||||
|
- Интеграция с существующей системой маршрутов
|
||||||
|
|
||||||
|
### 🔧 **Как работает:**
|
||||||
|
|
||||||
|
1. **Пользователь нажимает кнопку "✏️"** на промпте подробности
|
||||||
|
2. **JavaScript вызывает** `editVerbosePrompt(mode, content)`
|
||||||
|
3. **Форма открывается** с заполненными полями
|
||||||
|
4. **При сохранении** отправляется PUT запрос на `/prompts/edit-verbose/{mode}`
|
||||||
|
5. **Сервер обрабатывает** запрос через `handleEditVerbosePrompt()`
|
||||||
|
6. **Промпт обновляется** в файловой системе
|
||||||
|
7. **Страница перезагружается** с обновленными данными
|
||||||
|
|
||||||
|
### 📋 **Поддерживаемые режимы:**
|
||||||
|
|
||||||
|
- **`v`** → ID 6 (базовый verbose)
|
||||||
|
- **`vv`** → ID 7 (средний verbose)
|
||||||
|
- **`vvv`** → ID 8 (максимальный verbose)
|
||||||
|
|
||||||
|
### 🛡️ **Валидация:**
|
||||||
|
|
||||||
|
- **Содержимое:** максимум символов из `LCG_MAX_SYSTEM_PROMPT_LENGTH`
|
||||||
|
- **Название:** максимум символов из `LCG_MAX_PROMPT_NAME_LENGTH`
|
||||||
|
- **Описание:** максимум символов из `LCG_MAX_PROMPT_DESC_LENGTH`
|
||||||
|
|
||||||
|
### 🎨 **UI/UX:**
|
||||||
|
|
||||||
|
- **Единая форма** для редактирования всех типов промптов
|
||||||
|
- **Автоматическое определение** типа промпта (системный/verbose)
|
||||||
|
- **Правильная маршрутизация** запросов
|
||||||
|
- **Валидация на клиенте** и сервере
|
||||||
|
- **Отзывчивый дизайн** для мобильных устройств
|
||||||
|
|
||||||
|
## 🚀 **Использование:**
|
||||||
|
|
||||||
|
1. Откройте страницу `/prompts`
|
||||||
|
2. Перейдите на вкладку "📝 Промпты подробности"
|
||||||
|
3. Нажмите кнопку "✏️" на нужном промпте
|
||||||
|
4. Отредактируйте содержимое
|
||||||
|
5. Нажмите "Сохранить"
|
||||||
|
|
||||||
|
## ✅ **Статус:**
|
||||||
|
|
||||||
|
**ГОТОВО** - Редактирование промптов подробности полностью реализовано и протестировано.
|
||||||
16
git-sealed-cfg.yaml
Normal file
16
git-sealed-cfg.yaml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
apiVersion: bitnami.com/v1alpha1
|
||||||
|
kind: SealedSecret
|
||||||
|
metadata:
|
||||||
|
creationTimestamp: null
|
||||||
|
name: git-secrets
|
||||||
|
namespace: flux-system
|
||||||
|
spec:
|
||||||
|
encryptedData:
|
||||||
|
password: AgB8/7lk3onjQG9R2OzX02rv9Lwh3tkOkvV/DjySSGZDHnYtdOw6ttOZXSeQqibAfKIOqtbKvpGAmK0x0/1tQXfB1zg2unFh1p5oEa0sgYBNd0eeNV5nRXDpgl8gv3lWKLaLvLCmhxloYze1B6Js8UrPDGeH3lvxRk9A5oja87HEBnx/VIGm5h4crvb5fUn+7wibr+oQNP+LBRLiko4eqR8PZAA0qgWUvoJnAF6NIsGPEtNi6WjErtwwvYIsZbCIChjPHQll+FQYwSH7M9jXVEBPejiR92+XIR8RMMo6hsWWxkEvvZs5prNREirAOSErOHBRTMEYkI5JIAb6uaRBKZ2nP0G4YQAkx3N7DIZPD7MVQF3u5jIpqaRwP4S+v703/19uPID1D8RuXt99QUv4d7Bfs8PlwkV5/SOgx2ZPVn1viuieZZ9M9hdk5uXYqOqgLOjU4aQSVpVV5pdR7ifn2fcbcTsG6fE3srrjUt8c2t+bV1IBUw8CIzyA1lebxj31s2XRpY+6JsfihSneKm/RkR4ySkyLjF+BeAO7CuXeCdziSFsoCzRDGQqaf/3YKCv1HFbLUsAfHvbCOYuZH7X9UAw51OVqqhEpdD21Ms0W29NGRVUFz80/eglLpK5OmvlXr1kME2P9Wo8TEkLIK1Fjo10f6WzHsUU9eI1cfy6/JgyoaFn4Sjga9SaRgdMkv2neztyFLx6cRbdGui+5qh5Zewqaawvp72j7ZhosXVi0uBnKSoBkN6CLEzlw
|
||||||
|
username: AgB1HiFs7wfaiAaCrwGNr6zWfd8IoNMZUvJrKeaLbKqEJ2oAr3Db406mjq1gz/O0lhvOJqnmBR/Iel4PMVfixMBtmA8j2anytEkj7ZuruFF3OHOzEkN6OeYfJa9ddkJmUs+Zq62TxQdcaJufQnVfuSdScGvE+Exc8ruFWE0xM1cDgFsRp75d0lo6UmhxaJlBRewQexyH6izOmtecAZDX6WVyBopIGx2cPgNfyxp/4sU5CI5sGCjeqcq+5INRELk9kofbDIUnQuHxrH3nbzABNnK76sU6DhqS3jBmT5Pv9azB2AWaaYZ8z1di/fdYia3wKoQAbef36wKyQ+/ZLApnuaVkj9F96tcjqlV4T3SGp4179k99M8zf/7g1GwsljYRPEcRmc00L2FrC5m3S+oK8wIk6fLVT5KYJBlEP6ja3eTrgGDttosuNa4BcpzqLC2Ov6x1b6RKLRVPN9/+1eC9gMBxBqKnvsdjZn4kQb8yUYgjyRHMZl8kHehLnRDhIoE1O7xCQAMkrdeNyE1kAXsij2jgTDR7a72J86nnE0NPAyct4mCpn2x+2aFUVeyZ8u2e0nDLjxPIAs/ceN51ybz5f2cXH0JrEk3Yy0ZXTtHOF6NbZNcxrCrZGODYYIbtTpjI0MjYd+nxJt5TI3UN0pnrdR7FxHai+76Gw+fGWhXKQk+NgmSXsjyiQkBfoU7LCcnv8iFI5T3IpxtmlckZvcn/b
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
creationTimestamp: null
|
||||||
|
name: git-secrets
|
||||||
|
namespace: flux-system
|
||||||
|
---
|
||||||
6
go.mod
6
go.mod
@@ -1,11 +1,15 @@
|
|||||||
module github.com/direct-dev-ru/linux-command-gpt
|
module github.com/direct-dev-ru/linux-command-gpt
|
||||||
|
|
||||||
go 1.18
|
go 1.21
|
||||||
|
|
||||||
|
toolchain go1.23.4
|
||||||
|
|
||||||
require github.com/atotto/clipboard v0.1.4
|
require github.com/atotto/clipboard v0.1.4
|
||||||
|
|
||||||
require gopkg.in/yaml.v3 v3.0.1
|
require gopkg.in/yaml.v3 v3.0.1
|
||||||
|
|
||||||
|
require github.com/golang-jwt/jwt/v5 v5.3.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 //indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.5 //indirect
|
||||||
github.com/russross/blackfriday/v2 v2.1.0
|
github.com/russross/blackfriday/v2 v2.1.0
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -2,6 +2,8 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z
|
|||||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
|
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
|
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package gpt
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
_ "embed"
|
_ "embed"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
@@ -9,6 +10,9 @@ import (
|
|||||||
//go:embed builtin_prompts.yaml
|
//go:embed builtin_prompts.yaml
|
||||||
var builtinPromptsYAML string
|
var builtinPromptsYAML string
|
||||||
|
|
||||||
|
//go:embed builtin_prompts_windows.yaml
|
||||||
|
var builtinPromptsWindowsYAML string
|
||||||
|
|
||||||
var builtinPrompts string
|
var builtinPrompts string
|
||||||
|
|
||||||
// BuiltinPromptsData структура для YAML файла
|
// BuiltinPromptsData структура для YAML файла
|
||||||
@@ -117,7 +121,12 @@ func GetBuiltinPromptByIDAndLanguage(id int, lang string) *SystemPrompt {
|
|||||||
func InitBuiltinPrompts(embeddedBuiltinPromptsYAML string) {
|
func InitBuiltinPrompts(embeddedBuiltinPromptsYAML string) {
|
||||||
// Используем встроенный YAML, если переданный параметр пустой
|
// Используем встроенный YAML, если переданный параметр пустой
|
||||||
if embeddedBuiltinPromptsYAML == "" {
|
if embeddedBuiltinPromptsYAML == "" {
|
||||||
builtinPrompts = builtinPromptsYAML
|
// Выбираем промпты в зависимости от операционной системы
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
builtinPrompts = builtinPromptsWindowsYAML
|
||||||
|
} else {
|
||||||
|
builtinPrompts = builtinPromptsYAML
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
builtinPrompts = embeddedBuiltinPromptsYAML
|
builtinPrompts = embeddedBuiltinPromptsYAML
|
||||||
}
|
}
|
||||||
|
|||||||
262
gpt/builtin_prompts_windows.yaml
Normal file
262
gpt/builtin_prompts_windows.yaml
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
prompts:
|
||||||
|
- id: 1
|
||||||
|
name: "windows-command"
|
||||||
|
description:
|
||||||
|
en: "Main prompt for generating Windows commands"
|
||||||
|
ru: "Основной промпт для генерации Windows команд"
|
||||||
|
content:
|
||||||
|
en: |
|
||||||
|
You are a Windows command line expert.
|
||||||
|
Analyze the user's task, given in natural language, and suggest
|
||||||
|
a Windows command (PowerShell, CMD, or batch) that will help accomplish this task, and provide a detailed explanation of what it does,
|
||||||
|
its parameters and possible use cases.
|
||||||
|
Focus on practical examples and best practices.
|
||||||
|
In the response, you should only provide the commands or sequence of commands ready to copy and execute
|
||||||
|
in the command line without any explanation formatting or code blocks, without ```powershell``` or ```cmd```, ` or ``` symbols.
|
||||||
|
|
||||||
|
ru: |
|
||||||
|
Вы эксперт по Windows командам и командной строке.
|
||||||
|
Проанализируйте задачу пользователя на естественном языке и предложите Windows команду или набор команд (PowerShell, CMD или batch), которые помогут выполнить эту задачу, и предоставьте подробное объяснение того, что она делает, её параметры и возможные случаи использования.
|
||||||
|
Сосредоточьтесь на практических примерах и лучших практиках.
|
||||||
|
В ответе должна присутствовать только команда или последовательность команд,
|
||||||
|
готовая к копированию и выполнению в командной строке
|
||||||
|
без объяснений, выделений и форматирования наподобие ```powershell``` или ```cmd```, без символов ` или ```.
|
||||||
|
|
||||||
|
- id: 2
|
||||||
|
name: "windows-command-with-explanation"
|
||||||
|
description:
|
||||||
|
en: "Prompt with detailed command explanation"
|
||||||
|
ru: "Промпт с подробным объяснением команд"
|
||||||
|
content:
|
||||||
|
en: |
|
||||||
|
You are a Windows system administrator with extensive experience.
|
||||||
|
Generate Windows commands based on user task descriptions and provide comprehensive explanations.
|
||||||
|
|
||||||
|
Provide a detailed analysis including:
|
||||||
|
1. **Generated Command**: The Windows command that accomplishes the task
|
||||||
|
2. **Command Breakdown**: Explain each part of the command
|
||||||
|
3. **Parameters**: Explain each flag and option used
|
||||||
|
4. **Examples**: Show practical usage scenarios
|
||||||
|
5. **Security**: Highlight any security considerations
|
||||||
|
6. **Alternatives**: Suggest similar commands if applicable
|
||||||
|
7. **Best Practices**: Recommend optimal usage
|
||||||
|
|
||||||
|
Use clear formatting with headers and bullet points for readability.
|
||||||
|
ru: |
|
||||||
|
Вы системный администратор Windows с обширным опытом.
|
||||||
|
Генерируйте Windows команды на основе описаний задач пользователей и предоставляйте исчерпывающие объяснения.
|
||||||
|
|
||||||
|
Предоставьте подробный анализ, включая:
|
||||||
|
1. **Сгенерированная команда**: Windows команда, которая выполняет задачу
|
||||||
|
2. **Разбор команды**: Объясните каждую часть команды
|
||||||
|
3. **Параметры**: Объясните каждый используемый флаг и опцию
|
||||||
|
4. **Примеры**: Покажите практические сценарии использования
|
||||||
|
5. **Безопасность**: Выделите любые соображения безопасности
|
||||||
|
6. **Альтернативы**: Предложите похожие команды, если применимо
|
||||||
|
7. **Лучшие практики**: Рекомендуйте оптимальное использование
|
||||||
|
|
||||||
|
Используйте четкое форматирование с заголовками и маркерами для читаемости.
|
||||||
|
|
||||||
|
- id: 3
|
||||||
|
name: "windows-command-safe"
|
||||||
|
description:
|
||||||
|
en: "Safe command analysis with warnings"
|
||||||
|
ru: "Безопасный анализ команд с предупреждениями"
|
||||||
|
content:
|
||||||
|
en: |
|
||||||
|
You are a Windows security expert. Generate safe Windows commands based on user task descriptions with a focus on safety and security implications.
|
||||||
|
|
||||||
|
Provide a security-focused analysis:
|
||||||
|
1. **Generated Safe Command**: The secure Windows command for the task
|
||||||
|
2. **Safety Assessment**: Why this command is safe to run
|
||||||
|
3. **Potential Risks**: What could go wrong and how to mitigate
|
||||||
|
4. **Data Impact**: What files or data might be affected
|
||||||
|
5. **Permissions**: What permissions are required
|
||||||
|
6. **Recovery**: How to undo changes if needed
|
||||||
|
7. **Best Practices**: Safe alternatives or precautions
|
||||||
|
8. **Warnings**: Critical safety considerations
|
||||||
|
|
||||||
|
Always prioritize user safety and data protection.
|
||||||
|
ru: |
|
||||||
|
Вы эксперт по безопасности Windows. Генерируйте безопасные Windows команды на основе описаний задач пользователей с акцентом на безопасность и последствия для безопасности.
|
||||||
|
|
||||||
|
Предоставьте анализ, ориентированный на безопасность:
|
||||||
|
1. **Сгенерированная безопасная команда**: Безопасная Windows команда для задачи
|
||||||
|
2. **Оценка безопасности**: Почему эта команда безопасна для выполнения
|
||||||
|
3. **Потенциальные риски**: Что может пойти не так и как это смягчить
|
||||||
|
4. **Воздействие на данные**: Какие файлы или данные могут быть затронуты
|
||||||
|
5. **Разрешения**: Какие разрешения требуются
|
||||||
|
6. **Восстановление**: Как отменить изменения при необходимости
|
||||||
|
7. **Лучшие практики**: Безопасные альтернативы или меры предосторожности
|
||||||
|
8. **Предупреждения**: Критические соображения безопасности
|
||||||
|
|
||||||
|
Всегда приоритизируйте безопасность пользователя и защиту данных.
|
||||||
|
|
||||||
|
- id: 4
|
||||||
|
name: "windows-command-verbose"
|
||||||
|
description:
|
||||||
|
en: "Detailed analysis with technical details"
|
||||||
|
ru: "Подробный анализ с техническими деталями"
|
||||||
|
content:
|
||||||
|
en: |
|
||||||
|
You are a Windows kernel and system expert. Generate Windows commands based on user task descriptions and provide an in-depth technical analysis.
|
||||||
|
|
||||||
|
Deliver a comprehensive technical breakdown:
|
||||||
|
1. **Generated Command**: The Windows command that accomplishes the task
|
||||||
|
2. **System Level**: How the command interacts with the Windows kernel
|
||||||
|
3. **Process Flow**: Step-by-step execution details
|
||||||
|
4. **Resource Usage**: CPU, memory, I/O implications
|
||||||
|
5. **Registry**: Impact on Windows registry
|
||||||
|
6. **Services**: Windows services interactions
|
||||||
|
7. **Performance**: Optimization considerations
|
||||||
|
8. **Debugging**: Troubleshooting approaches
|
||||||
|
9. **Advanced Usage**: Expert-level techniques
|
||||||
|
|
||||||
|
Include technical details, system calls, and low-level operations.
|
||||||
|
ru: |
|
||||||
|
Вы эксперт по ядру Windows и системам. Генерируйте Windows команды на основе описаний задач пользователей и предоставляйте глубокий технический анализ.
|
||||||
|
|
||||||
|
Предоставьте исчерпывающий технический разбор:
|
||||||
|
1. **Сгенерированная команда**: Windows команда, которая выполняет задачу
|
||||||
|
2. **Системный уровень**: Как команда взаимодействует с ядром Windows
|
||||||
|
3. **Поток выполнения**: Детали пошагового выполнения
|
||||||
|
4. **Использование ресурсов**: Последствия для CPU, памяти, I/O
|
||||||
|
5. **Реестр**: Воздействие на реестр Windows
|
||||||
|
6. **Службы**: Взаимодействие со службами Windows
|
||||||
|
7. **Производительность**: Соображения по оптимизации
|
||||||
|
8. **Отладка**: Подходы к устранению неполадок
|
||||||
|
9. **Продвинутое использование**: Техники экспертного уровня
|
||||||
|
|
||||||
|
Включите технические детали, системные вызовы и низкоуровневые операции.
|
||||||
|
|
||||||
|
- id: 5
|
||||||
|
name: "windows-command-simple"
|
||||||
|
description:
|
||||||
|
en: "Simple and clear explanation"
|
||||||
|
ru: "Простое и понятное объяснение"
|
||||||
|
content:
|
||||||
|
en: |
|
||||||
|
You are a friendly Windows mentor. Explain the given command in simple, easy-to-understand terms.
|
||||||
|
|
||||||
|
Command: {{.command}}
|
||||||
|
|
||||||
|
Provide a beginner-friendly explanation:
|
||||||
|
1. **What it does**: Simple, clear description
|
||||||
|
2. **Why use it**: Common reasons to use this command
|
||||||
|
3. **Basic example**: Simple usage example
|
||||||
|
4. **What to expect**: Expected output or behavior
|
||||||
|
5. **Tips**: Helpful hints for beginners
|
||||||
|
|
||||||
|
Use plain language, avoid jargon, and focus on practical understanding.
|
||||||
|
ru: |
|
||||||
|
Вы дружелюбный наставник по Windows. Объясните данную команду простыми, понятными терминами.
|
||||||
|
|
||||||
|
Команда: {{.command}}
|
||||||
|
|
||||||
|
Предоставьте объяснение, подходящее для начинающих:
|
||||||
|
1. **Что она делает**: Простое, четкое описание
|
||||||
|
2. **Зачем использовать**: Общие причины использования этой команды
|
||||||
|
3. **Базовый пример**: Простой пример использования
|
||||||
|
4. **Что ожидать**: Ожидаемый вывод или поведение
|
||||||
|
5. **Советы**: Полезные подсказки для начинающих
|
||||||
|
|
||||||
|
Используйте простой язык, избегайте жаргона и сосредоточьтесь на практическом понимании.
|
||||||
|
|
||||||
|
- id: 6
|
||||||
|
name: "verbose-v"
|
||||||
|
description:
|
||||||
|
en: "Prompt for v mode (basic explanation)"
|
||||||
|
ru: "Промпт для режима v (базовое объяснение)"
|
||||||
|
content:
|
||||||
|
en: |
|
||||||
|
You are a Windows command expert. You can provide a clear and concise explanation of the given Windows command.
|
||||||
|
Your explanation should include:
|
||||||
|
1. What this command does for the task
|
||||||
|
2. Main parameters and their purpose
|
||||||
|
3. Common use cases
|
||||||
|
4. Any important warnings or considerations
|
||||||
|
ru: |
|
||||||
|
Вы эксперт по Windows командам. Вы можете предоставьте четкое и краткое объяснение заданной Windows команды.
|
||||||
|
Ваши краткие объяснения должны включать:
|
||||||
|
1. Что делает эта команда
|
||||||
|
2. Основные параметры и их назначение
|
||||||
|
3. Общие случаи использования
|
||||||
|
4. Любые важные предупреждения или соображения
|
||||||
|
|
||||||
|
- id: 7
|
||||||
|
name: "verbose-vv"
|
||||||
|
description:
|
||||||
|
en: "Prompt for vv mode (detailed explanation)"
|
||||||
|
ru: "Промпт для режима vv (подробное объяснение)"
|
||||||
|
content:
|
||||||
|
en: |
|
||||||
|
You are a Windows system expert. Provide a detailed technical explanation of the given command.
|
||||||
|
|
||||||
|
Provide a comprehensive analysis:
|
||||||
|
1. **Command Purpose**: What it accomplishes
|
||||||
|
2. **Syntax Breakdown**: Detailed parameter analysis
|
||||||
|
3. **Technical Details**: How it works internally
|
||||||
|
4. **Use Cases**: Practical scenarios and examples
|
||||||
|
5. **Performance Impact**: Resource usage and optimization
|
||||||
|
6. **Security Considerations**: Potential risks and mitigations
|
||||||
|
7. **Advanced Usage**: Expert techniques and tips
|
||||||
|
8. **Troubleshooting**: Common issues and solutions
|
||||||
|
|
||||||
|
Include technical depth while maintaining clarity.
|
||||||
|
ru: |
|
||||||
|
Вы эксперт по Windows системам. Предоставьте подробное техническое объяснение заданной команды.
|
||||||
|
|
||||||
|
Предоставьте исчерпывающий анализ:
|
||||||
|
1. **Цель команды**: Что она достигает
|
||||||
|
2. **Разбор синтаксиса**: Подробный анализ параметров
|
||||||
|
3. **Технические детали**: Как она работает внутренне
|
||||||
|
4. **Случаи использования**: Практические сценарии и примеры
|
||||||
|
5. **Влияние на производительность**: Использование ресурсов и оптимизация
|
||||||
|
6. **Соображения безопасности**: Потенциальные риски и меры по их снижению
|
||||||
|
7. **Продвинутое использование**: Экспертные техники и советы
|
||||||
|
8. **Устранение неполадок**: Общие проблемы и решения
|
||||||
|
|
||||||
|
Включите техническую глубину, сохраняя ясность.
|
||||||
|
|
||||||
|
- id: 8
|
||||||
|
name: "verbose-vvv"
|
||||||
|
description:
|
||||||
|
en: "Prompt for vvv mode (maximum detailed explanation)"
|
||||||
|
ru: "Промпт для режима vvv (максимально подробное объяснение)"
|
||||||
|
content:
|
||||||
|
en: |
|
||||||
|
You are a Windows kernel and system architecture expert. Provide an exhaustive technical analysis of the given command.
|
||||||
|
|
||||||
|
Deliver a comprehensive technical deep-dive:
|
||||||
|
1. **System Architecture**: How it fits into the Windows ecosystem
|
||||||
|
2. **Kernel Interaction**: System calls and kernel operations
|
||||||
|
3. **Process Management**: Process creation, scheduling, and lifecycle
|
||||||
|
4. **Memory Management**: Memory allocation and management
|
||||||
|
5. **Registry Operations**: Registry I/O operations and impact
|
||||||
|
6. **Network Stack**: Network operations and protocols
|
||||||
|
7. **Security Model**: Permissions, capabilities, and security implications
|
||||||
|
8. **Performance Analysis**: CPU, memory, I/O, and network impact
|
||||||
|
9. **Debugging and Profiling**: Advanced troubleshooting techniques
|
||||||
|
10. **Source Code Analysis**: Key implementation details
|
||||||
|
11. **Alternative Implementations**: Different approaches and trade-offs
|
||||||
|
12. **Historical Context**: Evolution and development history
|
||||||
|
|
||||||
|
Provide maximum technical depth with system-level insights, code examples, and architectural understanding.
|
||||||
|
ru: |
|
||||||
|
Вы эксперт по ядру Windows и системной архитектуре. Предоставьте исчерпывающий технический анализ заданной команды.
|
||||||
|
|
||||||
|
Предоставьте исчерпывающий технический глубокий анализ:
|
||||||
|
1. **Системная архитектура**: Как она вписывается в экосистему Windows
|
||||||
|
2. **Взаимодействие с ядром**: Системные вызовы и операции ядра
|
||||||
|
3. **Управление процессами**: Создание, планирование и жизненный цикл процессов
|
||||||
|
4. **Управление памятью**: Выделение и управление памятью
|
||||||
|
5. **Операции реестра**: I/O операции реестра и воздействие
|
||||||
|
6. **Сетевой стек**: Сетевые операции и протоколы
|
||||||
|
7. **Модель безопасности**: Разрешения, возможности и последствия безопасности
|
||||||
|
8. **Анализ производительности**: Воздействие на CPU, память, I/O и сеть
|
||||||
|
9. **Отладка и профилирование**: Продвинутые техники устранения неполадок
|
||||||
|
10. **Анализ исходного кода**: Ключевые детали реализации
|
||||||
|
11. **Альтернативные реализации**: Разные подходы и компромиссы
|
||||||
|
12. **Исторический контекст**: Эволюция и история разработки
|
||||||
|
|
||||||
|
Предоставьте максимальную техническую глубину с системными инсайтами, примерами кода и архитектурным пониманием.
|
||||||
@@ -57,6 +57,7 @@ func (pm *PromptManager) createInitialPromptsFile() {
|
|||||||
pm.Language = "ru"
|
pm.Language = "ru"
|
||||||
|
|
||||||
// Загружаем все встроенные промпты из YAML на русском языке
|
// Загружаем все встроенные промпты из YAML на русском языке
|
||||||
|
// Функция GetBuiltinPromptsByLanguage уже учитывает операционную систему
|
||||||
pm.Prompts = GetBuiltinPromptsByLanguage("ru")
|
pm.Prompts = GetBuiltinPromptsByLanguage("ru")
|
||||||
|
|
||||||
// Сохраняем все промпты в файл
|
// Сохраняем все промпты в файл
|
||||||
@@ -65,40 +66,8 @@ func (pm *PromptManager) createInitialPromptsFile() {
|
|||||||
|
|
||||||
// loadDefaultPrompts загружает предустановленные промпты
|
// loadDefaultPrompts загружает предустановленные промпты
|
||||||
func (pm *PromptManager) LoadDefaultPrompts() {
|
func (pm *PromptManager) LoadDefaultPrompts() {
|
||||||
defaultPrompts := []SystemPrompt{
|
// Используем встроенные промпты, которые автоматически выбираются по ОС
|
||||||
{
|
pm.Prompts = GetBuiltinPromptsByLanguage("en")
|
||||||
ID: 1,
|
|
||||||
Name: "linux-command",
|
|
||||||
Description: "Generate Linux commands (default)",
|
|
||||||
Content: "Reply with linux command and nothing else. Output with plain response - no need formatting. No need explanation. No need code blocks. No need ` symbols.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: 2,
|
|
||||||
Name: "linux-command-with-explanation",
|
|
||||||
Description: "Generate Linux commands with explanation",
|
|
||||||
Content: "Generate a Linux command and provide a brief explanation of what it does. Format: COMMAND: explanation",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: 3,
|
|
||||||
Name: "linux-command-safe",
|
|
||||||
Description: "Generate safe Linux commands",
|
|
||||||
Content: "Generate a safe Linux command that won't cause data loss or system damage. Reply with linux command and nothing else. Output with plain response - no need formatting.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: 4,
|
|
||||||
Name: "linux-command-verbose",
|
|
||||||
Description: "Generate Linux commands with detailed explanation",
|
|
||||||
Content: "Generate a Linux command and provide detailed explanation including what each flag does and potential alternatives.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: 5,
|
|
||||||
Name: "linux-command-simple",
|
|
||||||
Description: "Generate simple Linux commands",
|
|
||||||
Content: "Generate a simple, easy-to-understand Linux command. Avoid complex flags and options when possible.",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
pm.Prompts = defaultPrompts
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadAllPrompts загружает все промпты из файла sys_prompts
|
// loadAllPrompts загружает все промпты из файла sys_prompts
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Provider интерфейс для работы с разными LLM провайдерами
|
// Provider интерфейс для работы с разными LLM провайдерами
|
||||||
@@ -112,7 +114,7 @@ func (p *ProxyAPIProvider) Chat(messages []Chat) (string, error) {
|
|||||||
return "", fmt.Errorf("ошибка маршалинга запроса: %w", err)
|
return "", fmt.Errorf("ошибка маршалинга запроса: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequest("POST", p.BaseURL+"/api/v1/protected/sberchat/chat", bytes.NewBuffer(jsonData))
|
req, err := http.NewRequest("POST", p.BaseURL+config.AppConfig.Server.ProxyUrl, bytes.NewBuffer(jsonData))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("ошибка создания запроса: %w", err)
|
return "", fmt.Errorf("ошибка создания запроса: %w", err)
|
||||||
}
|
}
|
||||||
@@ -122,6 +124,11 @@ func (p *ProxyAPIProvider) Chat(messages []Chat) (string, error) {
|
|||||||
req.Header.Set("Authorization", "Bearer "+p.JWTToken)
|
req.Header.Set("Authorization", "Bearer "+p.JWTToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config.AppConfig.MainFlags.Debug {
|
||||||
|
fmt.Println("Chat URL: ", p.BaseURL+config.AppConfig.Server.ProxyUrl)
|
||||||
|
fmt.Println("ProxyChatRequest: ", req)
|
||||||
|
}
|
||||||
|
|
||||||
resp, err := p.HTTPClient.Do(req)
|
resp, err := p.HTTPClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("ошибка выполнения запроса: %w", err)
|
return "", fmt.Errorf("ошибка выполнения запроса: %w", err)
|
||||||
@@ -155,7 +162,7 @@ func (p *ProxyAPIProvider) Chat(messages []Chat) (string, error) {
|
|||||||
|
|
||||||
// Health для ProxyAPIProvider
|
// Health для ProxyAPIProvider
|
||||||
func (p *ProxyAPIProvider) Health() error {
|
func (p *ProxyAPIProvider) Health() error {
|
||||||
req, err := http.NewRequest("GET", p.BaseURL+"/api/v1/protected/sberchat/health", nil)
|
req, err := http.NewRequest("GET", p.BaseURL+config.AppConfig.Server.HealthUrl, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("ошибка создания health check запроса: %w", err)
|
return fmt.Errorf("ошибка создания health check запроса: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
45
kustomize/configmap.yaml
Normal file
45
kustomize/configmap.yaml
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: lcg-config
|
||||||
|
namespace: lcg
|
||||||
|
data:
|
||||||
|
# Основные настройки
|
||||||
|
LCG_VERSION: "v.2.0.19"
|
||||||
|
LCG_BASE_PATH: "/lcg"
|
||||||
|
LCG_SERVER_HOST: "0.0.0.0"
|
||||||
|
LCG_SERVER_PORT: "8080"
|
||||||
|
LCG_SERVER_ALLOW_HTTP: "true"
|
||||||
|
LCG_APP_NAME: "Linux Command GPT"
|
||||||
|
LCG_RESULT_FOLDER: "/app/data/results"
|
||||||
|
LCG_PROMPT_FOLDER: "/app/data/prompts"
|
||||||
|
LCG_CONFIG_FOLDER: "/app/data/config"
|
||||||
|
LCG_NO_HISTORY: "false"
|
||||||
|
LCG_ALLOW_EXECUTION: "false"
|
||||||
|
LCG_DEBUG: "true"
|
||||||
|
LCG_PROVIDER: "proxy"
|
||||||
|
|
||||||
|
# Настройки аутентификации
|
||||||
|
LCG_SERVER_REQUIRE_AUTH: "true"
|
||||||
|
|
||||||
|
LCG_COOKIE_SECURE: "true"
|
||||||
|
LCG_COOKIE_TTL_HOURS: "168"
|
||||||
|
LCG_DOMAIN: "direct-dev.ru"
|
||||||
|
LCG_COOKIE_PATH: "/lcg"
|
||||||
|
|
||||||
|
# Настройки провайдера (по умолчанию)
|
||||||
|
LCG_PROVIDER_TYPE: "proxy"
|
||||||
|
LCG_HOST: "https://direct-dev.ru"
|
||||||
|
LCG_HEALTH_URL: "/api/v1/protected/sberchat/health"
|
||||||
|
LCG_PROXY_URL: "/api/v1/protected/sberchat/chat"
|
||||||
|
LCG_MODEL: "GigaChat-2-Max"
|
||||||
|
|
||||||
|
# Настройки валидации
|
||||||
|
LCG_MAX_SYSTEM_PROMPT_LENGTH: "2000"
|
||||||
|
LCG_MAX_USER_MESSAGE_LENGTH: "4000"
|
||||||
|
LCG_MAX_PROMPT_NAME_LENGTH: "2000"
|
||||||
|
LCG_MAX_PROMPT_DESC_LENGTH: "50000"
|
||||||
|
|
||||||
|
# Настройки таймаутов
|
||||||
|
LCG_TIMEOUT: "300"
|
||||||
|
|
||||||
91
kustomize/deployment.yaml
Normal file
91
kustomize/deployment.yaml
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: lcg
|
||||||
|
namespace: lcg
|
||||||
|
labels:
|
||||||
|
app: lcg
|
||||||
|
version: v.2.0.19
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: lcg
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: lcg
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: lcg
|
||||||
|
image: kuznetcovay/lcg:v.2.0.19
|
||||||
|
imagePullPolicy: Always
|
||||||
|
ports:
|
||||||
|
- containerPort: 8080
|
||||||
|
protocol: TCP
|
||||||
|
envFrom:
|
||||||
|
- configMapRef:
|
||||||
|
name: lcg-config
|
||||||
|
- secretRef:
|
||||||
|
name: lcg-secrets
|
||||||
|
env:
|
||||||
|
# Pod information
|
||||||
|
- name: POD_NAME
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: metadata.name
|
||||||
|
- name: POD_NAMESPACE
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: metadata.namespace
|
||||||
|
- name: NODE_NAME
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: spec.nodeName
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpu: 1000m
|
||||||
|
memory: 1Gi
|
||||||
|
requests:
|
||||||
|
cpu: 250m
|
||||||
|
memory: 512Mi
|
||||||
|
volumeMounts:
|
||||||
|
- name: lcg-data
|
||||||
|
mountPath: /app/data
|
||||||
|
- name: lcg-config
|
||||||
|
mountPath: /app/config
|
||||||
|
readOnly: true
|
||||||
|
# Health checks
|
||||||
|
startupProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: 8080
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 5
|
||||||
|
failureThreshold: 30
|
||||||
|
readinessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: 8080
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 10
|
||||||
|
livenessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: 8080
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 60
|
||||||
|
volumes:
|
||||||
|
- name: lcg-data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: lcg-data
|
||||||
|
- name: lcg-config
|
||||||
|
configMap:
|
||||||
|
name: lcg-config
|
||||||
|
# Security context
|
||||||
|
securityContext:
|
||||||
|
runAsNonRoot: true
|
||||||
|
runAsUser: 1001
|
||||||
|
readOnlyRootFilesystem: false
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
capabilities:
|
||||||
|
drop:
|
||||||
|
- ALL
|
||||||
|
restartPolicy: Always
|
||||||
63
kustomize/ingress-route.yaml
Normal file
63
kustomize/ingress-route.yaml
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: IngressRoute
|
||||||
|
metadata:
|
||||||
|
name: lcg-route
|
||||||
|
namespace: lcg
|
||||||
|
labels:
|
||||||
|
app: lcg
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- websecure
|
||||||
|
routes:
|
||||||
|
- kind: Rule
|
||||||
|
match: Host(`direct-dev.ru`) && PathPrefix(`/lcg`)
|
||||||
|
services:
|
||||||
|
- name: lcg
|
||||||
|
port: 8080
|
||||||
|
tls:
|
||||||
|
secretName: le-root-direct-dev-ru
|
||||||
|
---
|
||||||
|
|
||||||
|
# apiVersion: traefik.io/v1alpha1
|
||||||
|
# kind: IngressRoute
|
||||||
|
# metadata:
|
||||||
|
# name: lcg-route
|
||||||
|
# namespace: lcg
|
||||||
|
# spec:
|
||||||
|
# entryPoints:
|
||||||
|
# - websecure
|
||||||
|
# routes:
|
||||||
|
# - kind: Rule
|
||||||
|
# match: Host(`direct-dev.ru`) && PathPrefix(`/lcg`)
|
||||||
|
# services:
|
||||||
|
# - name: lcg
|
||||||
|
# port: 8080
|
||||||
|
# middlewares:
|
||||||
|
# - name: lcg-strip-prefix
|
||||||
|
# tls:
|
||||||
|
# secretName: le-root-direct-dev-ru
|
||||||
|
# ---
|
||||||
|
# apiVersion: traefik.io/v1alpha1
|
||||||
|
# kind: Middleware
|
||||||
|
# metadata:
|
||||||
|
# name: lcg-strip-prefix
|
||||||
|
# namespace: lcg
|
||||||
|
# spec:
|
||||||
|
# stripPrefix:
|
||||||
|
# prefixes:
|
||||||
|
# - /lcg
|
||||||
|
# ---
|
||||||
|
# apiVersion: traefik.io/v1alpha1
|
||||||
|
# kind: Middleware
|
||||||
|
# metadata:
|
||||||
|
# name: lcg-headers
|
||||||
|
# namespace: lcg
|
||||||
|
# spec:
|
||||||
|
# headers:
|
||||||
|
# customRequestHeaders:
|
||||||
|
# X-Forwarded-Proto: "https"
|
||||||
|
# X-Forwarded-Port: "443"
|
||||||
|
# customResponseHeaders:
|
||||||
|
# X-Frame-Options: "DENY"
|
||||||
|
# X-Content-Type-Options: "nosniff"
|
||||||
|
# X-XSS-Protection: "1; mode=block"
|
||||||
25
kustomize/kustomization.yaml
Normal file
25
kustomize/kustomization.yaml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
|
||||||
|
# Namespace
|
||||||
|
namespace: lcg
|
||||||
|
|
||||||
|
# Resources
|
||||||
|
resources:
|
||||||
|
- configmap.yaml
|
||||||
|
- secret.yaml
|
||||||
|
- deployment.yaml
|
||||||
|
- service.yaml
|
||||||
|
- ingress-route.yaml
|
||||||
|
|
||||||
|
# Common labels
|
||||||
|
# commonLabels:
|
||||||
|
# app: lcg
|
||||||
|
# version: v.2.0.19
|
||||||
|
# managed-by: kustomize
|
||||||
|
|
||||||
|
# Images
|
||||||
|
# images:
|
||||||
|
# - name: lcg
|
||||||
|
# newName: kuznetcovay/lcg
|
||||||
|
# newTag: v.2.0.19
|
||||||
18
kustomize/secret.yaml
Normal file
18
kustomize/secret.yaml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
apiVersion: bitnami.com/v1alpha1
|
||||||
|
kind: SealedSecret
|
||||||
|
metadata:
|
||||||
|
creationTimestamp: null
|
||||||
|
name: lcg-secrets
|
||||||
|
namespace: lcg
|
||||||
|
spec:
|
||||||
|
encryptedData:
|
||||||
|
LCG_CSRF_SECRET: AgDYqxr2Xlf0Hmqgb9u1OildXogus4pFcqB80Dfc1uYw7wnHNOZZP+Hto1OAifXGg2eOYbB1QEsbW/lLSim/C2zpAjNUfRTkwDhQhK3FWmLOjruKBYHZxHyMuhbBmGhVTFbKApOc9zQXTASMLgIeP2vK5yDhVRr97/OJmMKeTOq8FX8CxXZPKTjehronFS3syUlIocboi0PveFf0dgj7nkYePBMST/FKGe2I1NbpOXYH2VrZWOHeoLYDUrEQMQvSAS1mnxAPkdEOuqbWMJA7cs+KKiIFsoy5eBkOXe8hoQqLfddi7ifnJ6h32StQg67qrAm4IMClS/U9iZK5C3Rm/COsywp8Mp3e1iF0hQ2yPuiHMBibxb3aRKwIryJjeo7x51PMMjJaErbTU+bwaAvMav7znOQ14N410bp2Io6KBNsOLW0DG8/mvmQcEn4Q5f4ZtSLcvaq5BQ0NjzrrIv+eCyvNzA28oAyuR0VieJdrUPqpgELrT64MwC+4m5dqdrNPdWkxbOXPG8ghs1nMSSfI+aU+JjT1vupvJA8m83NPqh+YBewmZiTwq5PdvnlAYZ+SYFLZwDWpdUTnK/hqiUkSDOJbE0pbcm03BaG9FjTJpNsmOcKu05Rs8QfWFJMOqYKC4dTA1itxTkyCxz9Y8KFJzX0RXuksN19xzLiZm+e586VtxExEfkYuBo01R39LOtTey9e8pIB9dKwFsHRAORDaZwkaKRe3RlyF8FoSq4khr79CYdGw6IAYzrg8sXAbWawXPg1osQpNR825PejO3/TwbJDPKfjo8RaHHiFa7UhaHbrm/4FwAj4gSGVL
|
||||||
|
LCG_JWT_SECRET: AgBq2Eb0fh1lO0FP2/ebnwuyiTyrAF+dD3iu7uLVgo7JuLnKRA4998rB1OZGRcZ+MNLpajIa20ezJ3aV2wCl24PSJErhzKC3IPSXoLnpJ0EfXrFGS0Pd86TQqAf0TMN2ALFrHY/LywYW09I90/Ct93WqjDKY4yVPbyVni+njUSwavp0iMEWDjENRTdWDK3f/ym1i8SMJgSjX5N6dazwyWVSeWC8oHAGPBF8rr+rhPnY1Ds7QeR/8M0GK7YLiI2nnKljxo7LjXA61jbgCLyiATcWYRylkylcO+ZU8bAsaOSJGXEjtO6s0GHY7Y9CcHQlb1VTZCVfzu/Pb7OSbIiL4wVWHi0DYScTJluVr5SorUjti8HprW9LJE0sy7GIxMdsRZ/7jfJY/s1nxFJph3Q+wkImoPD7R0Njpkwph14CH3xfhPxBpXTQO14hlhx1VYtVkYeWAtcFht+z1qFz0vn+eYkm9B28U+2uxP4WA5AyyMZBPJVekUxQiEmr2nnowbV+zu0+wRtor/lPREQBInbf9tpvth5fpnXtUCS4P2Ne4WWlY0Y5Sij5Z20l0FA0CwmMOliWUcl9vIAy+of3RvM9XGgGqMWTuJP2QFEt7E8VKzM/aI8XgdJuExSCDjd/cyOIgnz5Byy1YvdICqL1CrVpvL0mdy+KMobr59Fgdv+5q0hiHvXF8p2bkG1DHgWiSK3WARDpKzAs+cfsPomD3jMhJZFOopp1Vu9Jpy+l4IsWoY8W1KajPW2DQWjwULRt+ewNr1mAfj+8ESVSXH8zqLv+hkXeef28h7McPcfH9fx9ejs9ovS/E6Sks6EWp
|
||||||
|
LCG_JWT_TOKEN: AgB6PPDflcIav6fqhCi80Ysv9HPkI5zXIjqfot3jaYON3fNmpKNDIhyvKk4YvLbT4PEZfY+JZP/f17MoJ1eikeiZAO7klkg3wNq3h6TcRTuwM/ST9R/KsWqnfLxm5HzGBsqh39cwv9eU2ovAMXqXPJeO/23HcjOqZg7cWZ2WfknaAUydJc39Cue4zmgxlpIxF37p6/rvJqUGByOOUzlDHoVV3TORi+j6dui352PGG6gVCzcCVGNSsbf4j1VibJ1Lz06WEayMi7ZYkD18rsiKHcFGo2SBhEjGGo00Cbdq0EOUTu6k1Q47evHMLFAhdFK3T2gESB4NfMaAL+6gHS7ouI6SbyOCeAZIGT8e3ggM7MlIsNBrLUeDLEwZG8DjHGItY9KcJG/YxbjZ24b9/IzWDduR8VIUG6XCIrGwQd2jlH8GXmrsq+3KkQr81Tj6Z4/QIa4mcgqSKBr8nCzf31GQhhWgj143VwZtPuHUaAbSsZ4ISbo1PoISUaHymWh1J6qjjrzsvfOdeiKHihA8nLe4ggnV8nrQ5EusA+DzmL/Ti+K+2cc277nC0J1pFhuZs37xi+eQT6TMyUeE12uyCHlG1SiwG8t0wfv2N/yzdugW2eILZbRDZnEoN46lLoeXrTGRiFi25/6Jue0/iTo1AV7ameK4J2teGIhYROqB06kResWVECWm2mWhMpJ7Am5ij7tho3Ot75wrmgXXWCb962MzmIpJG8VIimtoIRNVtlu7+cxMb4D9KFb/i69cMkb+7R+Vm6c37T0T0R/o6QCY+MP0w29xgbGz5PNcLEh//avz4E1JI+AsbvtHOi8/aZ9F7c2DcTfXDcxoA2suxJjRy8Y4uu6rrKUWhklla5G/hs0rZsuTM9iruFasV+AybXDLN2/YNqSAj4oDzi/lYNwvQm5CTZwklHK/fwNPbfCNkY5C94rvzW+OJJ2mR1rcHCfHVWYW/IQRfE11mZVX2m0HT70rDPVopYSHrmlvuuTk3ky5gXym+/FOKBq+BcE0GiDDGl3C5VFtiREhpW4J7zRux5QnHk5fIVyEAZlidKsNSNLwq73+E6W77kMNDU7KCRH23A9BIJPOpN87oZDVX1eBghiM/qBOzP04fw3C6dxu+W/OQvTwZmxLtod07Y64EbdaeqJtjnd7GihAEW2jj+Wkcfz9WHTw38cNpyLqcU3ap13790qVJO6V27b0OmiEiloMYyYHUwcHs8wQA946ns0XOz7zw3r1goJgJS6il93dAThK2UBzw3DIY4yJGrmscPZesWSqL3a+ElGjZWz6n9idmIN7L7oViR7A+p17zwFnpczz/VsV+vj8DwSBwLsw6Q==
|
||||||
|
LCG_SERVER_PASSWORD: AgBYXu3YpewbdveXVFDGKBzJe8Gur70LxYSL3kmM7Q65lTU/Q4smpPKhb/bDPntNc4XmNFUfVZ/P5iv2bCdgZB0ccMp6eGZUKKfa7PVJ1/I1hAVwSWYmkD673tbfF7CoGrgtTYw93IaU4t1aWBRiqgapktRawFY1SEIoU6dAx3nrKivyFFJx5akHnm+7vr0GMAtE+H8P/htW+bg1peg5rVYkQwMyeYdefQ+AA/RMDG74XlRGv0EfU6S/LmJ0HF84jb5VNjyJMAD7NssuSUXglpVhfTRwZZAD789/hTgElQnR1JUAIyRHeY6zerU5sXaaS7l+MJGxMMNGsfgOFQ57kzA7Eafumf0ChKfdGl7c4UaEcq6zENSp1Rrzc84WLoghwhvVIhszuGkSE9aLelJJamBMOnm/rlwOuQJJsMAlTiuGO7pewsHKDQXnuZGKOQyAflEX/SNXzUSdYEGonuWGkjWopZrvjO9TwBReFUfsV/ALoDgjA2b7nFpXff0Ffx+EvYtuudDZFw1PobBAHy2aFkMuUP0GxVh14nbmC38VNMiug4+xfl92TxbxRkOkx/tufb2p2QFaglW5TK9I3ysHxiCODhq+YdhlW+gDS+mzBEQZbXd+3TMjh7sE1DGqbloMDhfvJJA6u6C2t6olW3BghU/AS20tOgNC+aIgWzetC6ODOKJQLAq2jjXG+PmUoJDUhs1SbS7uIsIlLIt/Y62b6roVAK81zQPt3w==
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
creationTimestamp: null
|
||||||
|
name: lcg-secrets
|
||||||
|
namespace: lcg
|
||||||
|
---
|
||||||
16
kustomize/service.yaml
Normal file
16
kustomize/service.yaml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: lcg
|
||||||
|
namespace: lcg
|
||||||
|
labels:
|
||||||
|
app: lcg
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
ports:
|
||||||
|
- port: 8080
|
||||||
|
targetPort: 8080
|
||||||
|
protocol: TCP
|
||||||
|
name: http
|
||||||
|
selector:
|
||||||
|
app: lcg
|
||||||
366
main.go
366
main.go
@@ -2,12 +2,14 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
_ "embed"
|
_ "embed"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"os/user"
|
"os/user"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -18,13 +20,22 @@ import (
|
|||||||
"github.com/direct-dev-ru/linux-command-gpt/gpt"
|
"github.com/direct-dev-ru/linux-command-gpt/gpt"
|
||||||
"github.com/direct-dev-ru/linux-command-gpt/reader"
|
"github.com/direct-dev-ru/linux-command-gpt/reader"
|
||||||
"github.com/direct-dev-ru/linux-command-gpt/serve"
|
"github.com/direct-dev-ru/linux-command-gpt/serve"
|
||||||
|
"github.com/direct-dev-ru/linux-command-gpt/validation"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed VERSION.txt
|
//go:embed VERSION.txt
|
||||||
var Version string
|
var Version string
|
||||||
|
|
||||||
// используем глобальный экземпляр конфига из пакета config
|
//go:embed build-conditions.yaml
|
||||||
|
var BuildConditionsFromYaml string
|
||||||
|
|
||||||
|
type buildConditions struct {
|
||||||
|
NoServe bool `yaml:"no-serve"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var CompileConditions buildConditions
|
||||||
|
|
||||||
// disableHistory управляет записью/обновлением истории на уровне процесса (флаг имеет приоритет над env)
|
// disableHistory управляет записью/обновлением истории на уровне процесса (флаг имеет приоритет над env)
|
||||||
var disableHistory bool
|
var disableHistory bool
|
||||||
@@ -44,6 +55,14 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
||||||
|
if err := yaml.Unmarshal([]byte(BuildConditionsFromYaml), &CompileConditions); err != nil {
|
||||||
|
fmt.Println("Error parsing build conditions:", err)
|
||||||
|
CompileConditions.NoServe = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// fmt.Println("Build conditions:", CompileConditions)
|
||||||
|
|
||||||
_ = colorBlue
|
_ = colorBlue
|
||||||
|
|
||||||
gpt.InitBuiltinPrompts("")
|
gpt.InitBuiltinPrompts("")
|
||||||
@@ -55,9 +74,15 @@ func main() {
|
|||||||
|
|
||||||
app := &cli.App{
|
app := &cli.App{
|
||||||
Name: "lcg",
|
Name: "lcg",
|
||||||
Usage: "Linux Command GPT - Генерация Linux команд из описаний",
|
Usage: config.AppConfig.AppName + " - Генерация Linux команд из описаний",
|
||||||
Version: Version,
|
Version: Version,
|
||||||
Commands: getCommands(),
|
Commands: getCommands(),
|
||||||
|
Before: func(c *cli.Context) error {
|
||||||
|
// Применяем флаги приложения к конфигурации перед выполнением любой команды
|
||||||
|
// Это гарантирует, что флаги будут применены даже для команд, которые не используют основной Action
|
||||||
|
applyAppFlagsToConfig(c)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
UsageText: `
|
UsageText: `
|
||||||
lcg [опции] <описание команды>
|
lcg [опции] <описание команды>
|
||||||
|
|
||||||
@@ -66,15 +91,59 @@ lcg [опции] <описание команды>
|
|||||||
lcg --file /path/to/file.txt "хочу вывести все директории с помощью ls"
|
lcg --file /path/to/file.txt "хочу вывести все директории с помощью ls"
|
||||||
`,
|
`,
|
||||||
Description: `
|
Description: `
|
||||||
Linux Command GPT - инструмент для генерации Linux команд из описаний на естественном языке.
|
{{.AppName}} - инструмент для генерации Linux команд из описаний на естественном языке.
|
||||||
Поддерживает чтение частей промпта из файлов и позволяет сохранять, копировать или перегенерировать результаты.
|
Поддерживает чтение частей промпта из файлов и позволяет сохранять, копировать или перегенерировать результаты.
|
||||||
может задавать системный промпт или выбирать из предустановленных промптов.
|
может задавать системный промпт или выбирать из предустановленных промптов.
|
||||||
|
|
||||||
Переменные окружения:
|
Переменные окружения:
|
||||||
LCG_HOST Endpoint для LLM API (по умолчанию: http://192.168.87.108:11434/)
|
|
||||||
LCG_MODEL Название модели (по умолчанию: codegeex4)
|
Основные настройки:
|
||||||
LCG_PROMPT Текст промпта по умолчанию
|
LCG_HOST Endpoint для LLM API (по умолчанию: http://192.168.87.108:11434/)
|
||||||
LCG_PROVIDER Тип провайдера: "ollama" или "proxy" (по умолчанию: ollama)
|
LCG_MODEL Название модели (по умолчанию: hf.co/yandex/YandexGPT-5-Lite-8B-instruct-GGUF:Q4_K_M)
|
||||||
LCG_JWT_TOKEN JWT токен для proxy провайдера
|
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{
|
Flags: []cli.Flag{
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
@@ -82,12 +151,25 @@ Linux Command GPT - инструмент для генерации Linux ком
|
|||||||
Aliases: []string{"f"},
|
Aliases: []string{"f"},
|
||||||
Usage: "Read part of the command from a file",
|
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{
|
&cli.BoolFlag{
|
||||||
Name: "no-history",
|
Name: "no-history",
|
||||||
Aliases: []string{"nh"},
|
Aliases: []string{"nh"},
|
||||||
Usage: "Disable writing/updating command history (overrides LCG_NO_HISTORY)",
|
Usage: "Disable writing/updating command history (overrides LCG_NO_HISTORY)",
|
||||||
Value: false,
|
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{
|
&cli.StringFlag{
|
||||||
Name: "sys",
|
Name: "sys",
|
||||||
Aliases: []string{"s"},
|
Aliases: []string{"s"},
|
||||||
@@ -119,16 +201,25 @@ Linux Command GPT - инструмент для генерации Linux ком
|
|||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
file := c.String("file")
|
file := c.String("file")
|
||||||
system := c.String("sys")
|
system := c.String("sys")
|
||||||
|
model := c.String("model")
|
||||||
|
query := c.String("query")
|
||||||
// обновляем конфиг на основе флагов
|
// обновляем конфиг на основе флагов
|
||||||
if system != "" {
|
if c.IsSet("sys") && system != "" {
|
||||||
config.AppConfig.Prompt = system
|
config.AppConfig.Prompt = system
|
||||||
}
|
}
|
||||||
|
if c.IsSet("query") && query != "" {
|
||||||
|
config.AppConfig.Query = query
|
||||||
|
}
|
||||||
if c.IsSet("timeout") {
|
if c.IsSet("timeout") {
|
||||||
config.AppConfig.Timeout = fmt.Sprintf("%d", c.Int("timeout"))
|
config.AppConfig.Timeout = fmt.Sprintf("%d", c.Int("timeout"))
|
||||||
}
|
}
|
||||||
|
if c.IsSet("model") {
|
||||||
|
config.AppConfig.Model = model
|
||||||
|
}
|
||||||
|
|
||||||
promptID := c.Int("prompt-id")
|
promptID := c.Int("prompt-id")
|
||||||
timeout := c.Int("timeout")
|
timeout := c.Int("timeout")
|
||||||
// сохраняем конкретные значения флагов
|
|
||||||
config.AppConfig.MainFlags = config.MainFlags{
|
config.AppConfig.MainFlags = config.MainFlags{
|
||||||
File: file,
|
File: file,
|
||||||
NoHistory: c.Bool("no-history"),
|
NoHistory: c.Bool("no-history"),
|
||||||
@@ -138,9 +229,12 @@ Linux Command GPT - инструмент для генерации Linux ком
|
|||||||
Debug: c.Bool("debug"),
|
Debug: c.Bool("debug"),
|
||||||
}
|
}
|
||||||
disableHistory = config.AppConfig.MainFlags.NoHistory || config.AppConfig.IsNoHistoryEnabled()
|
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()
|
args := c.Args().Slice()
|
||||||
|
|
||||||
if len(args) == 0 {
|
if len(args) == 0 && config.AppConfig.Query == "" {
|
||||||
cli.ShowAppHelp(c)
|
cli.ShowAppHelp(c)
|
||||||
showTips()
|
showTips()
|
||||||
return nil
|
return nil
|
||||||
@@ -157,6 +251,18 @@ Linux Command GPT - инструмент для генерации Linux ком
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
executeMain(file, system, strings.Join(args, " "), timeout)
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
@@ -177,8 +283,33 @@ Linux Command GPT - инструмент для генерации Linux ком
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
func getCommands() []*cli.Command {
|
||||||
return []*cli.Command{
|
commands := []*cli.Command{
|
||||||
{
|
{
|
||||||
Name: "update-key",
|
Name: "update-key",
|
||||||
Aliases: []string{"u"},
|
Aliases: []string{"u"},
|
||||||
@@ -308,24 +439,24 @@ func getCommands() []*cli.Command {
|
|||||||
Name: "config",
|
Name: "config",
|
||||||
Aliases: []string{"co"}, // Изменено с "c" на "co"
|
Aliases: []string{"co"}, // Изменено с "c" на "co"
|
||||||
Usage: "Show current configuration",
|
Usage: "Show current configuration",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "full",
|
||||||
|
Aliases: []string{"f"},
|
||||||
|
Usage: "Show full configuration object",
|
||||||
|
},
|
||||||
|
},
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
fmt.Printf("Provider: %s\n", config.AppConfig.ProviderType)
|
// Флаги приложения уже применены через глобальный Before hook
|
||||||
fmt.Printf("Host: %s\n", config.AppConfig.Host)
|
// Но применяем их еще раз на случай, если глобальный Before не сработал
|
||||||
fmt.Printf("Model: %s\n", config.AppConfig.Model)
|
applyAppFlagsToConfig(c)
|
||||||
fmt.Printf("Prompt: %s\n", config.AppConfig.Prompt)
|
|
||||||
fmt.Printf("Timeout: %s seconds\n", config.AppConfig.Timeout)
|
if c.Bool("full") {
|
||||||
if config.AppConfig.ProviderType == "proxy" {
|
// Выводим полную конфигурацию в JSON формате
|
||||||
fmt.Printf("JWT Token: %s\n", func() string {
|
showFullConfig()
|
||||||
if config.AppConfig.JwtToken != "" {
|
} else {
|
||||||
return "***set***"
|
// Выводим краткую конфигурацию
|
||||||
}
|
showShortConfig()
|
||||||
currentUser, _ := user.Current()
|
|
||||||
jwtFile := currentUser.HomeDir + "/.proxy_jwt_token"
|
|
||||||
if _, err := os.Stat(jwtFile); err == nil {
|
|
||||||
return "***from file***"
|
|
||||||
}
|
|
||||||
return "***not set***"
|
|
||||||
}())
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
@@ -546,16 +677,47 @@ func getCommands() []*cli.Command {
|
|||||||
host := c.String("host")
|
host := c.String("host")
|
||||||
openBrowser := c.Bool("browser")
|
openBrowser := c.Bool("browser")
|
||||||
|
|
||||||
// Пробрасываем глобальный флаг debug для web-сервера
|
// Пробрасываем debug: флаг или переменная окружения LCG_DEBUG
|
||||||
// Позволяет запускать: lcg -d serve -p ...
|
// Позволяет запускать: LCG_DEBUG=1 lcg serve ... или lcg -d serve ...
|
||||||
if c.Bool("debug") {
|
config.AppConfig.MainFlags.Debug = c.Bool("debug") || config.GetEnvBool("LCG_DEBUG", false)
|
||||||
config.AppConfig.MainFlags.Debug = true
|
|
||||||
|
// Обновляем конфигурацию сервера с новыми параметрами
|
||||||
|
config.AppConfig.Server.Host = host
|
||||||
|
config.AppConfig.Server.Port = port
|
||||||
|
// Пересчитываем AllowHTTP на основе нового хоста
|
||||||
|
config.AppConfig.Server.AllowHTTP = getServerAllowHTTPForHost(host)
|
||||||
|
|
||||||
|
// Определяем протокол на основе хоста
|
||||||
|
useHTTPS := !config.AppConfig.Server.AllowHTTP
|
||||||
|
protocol := "http"
|
||||||
|
if useHTTPS {
|
||||||
|
protocol = "https"
|
||||||
}
|
}
|
||||||
|
|
||||||
printColored(fmt.Sprintf("🌐 Запускаю HTTP сервер на %s:%s\n", host, port), colorCyan)
|
printColored(fmt.Sprintf("🌐 Запускаю %s сервер на %s:%s\n", strings.ToUpper(protocol), host, port), colorCyan)
|
||||||
printColored(fmt.Sprintf("📁 Папка результатов: %s\n", config.AppConfig.ResultFolder), colorYellow)
|
printColored(fmt.Sprintf("📁 Папка результатов: %s\n", config.AppConfig.ResultFolder), colorYellow)
|
||||||
|
|
||||||
url := fmt.Sprintf("http://%s:%s", host, port)
|
// Предупреждение о самоподписанном сертификате
|
||||||
|
if useHTTPS {
|
||||||
|
printColored("⚠️ Используется самоподписанный SSL сертификат\n", colorYellow)
|
||||||
|
printColored(" Браузер может показать предупреждение о безопасности\n", colorYellow)
|
||||||
|
printColored(" Нажмите 'Дополнительно' → 'Перейти на сайт' для продолжения\n", colorYellow)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Для автооткрытия браузера заменяем 0.0.0.0 на localhost
|
||||||
|
browserHost := host
|
||||||
|
if host == "0.0.0.0" {
|
||||||
|
browserHost = "localhost"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Учитываем BasePath в URL
|
||||||
|
basePath := config.AppConfig.Server.BasePath
|
||||||
|
if basePath == "" || basePath == "/" {
|
||||||
|
basePath = ""
|
||||||
|
} else {
|
||||||
|
basePath = strings.TrimSuffix(basePath, "/")
|
||||||
|
}
|
||||||
|
url := fmt.Sprintf("%s://%s:%s%s", protocol, browserHost, port, basePath)
|
||||||
|
|
||||||
if openBrowser {
|
if openBrowser {
|
||||||
printColored("🌍 Открываю браузер...\n", colorGreen)
|
printColored("🌍 Открываю браузер...\n", colorGreen)
|
||||||
@@ -573,9 +735,34 @@ func getCommands() []*cli.Command {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if CompileConditions.NoServe {
|
||||||
|
filteredCommands := []*cli.Command{}
|
||||||
|
for _, cmd := range commands {
|
||||||
|
if cmd.Name != "serve" {
|
||||||
|
filteredCommands = append(filteredCommands, cmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
commands = filteredCommands
|
||||||
|
}
|
||||||
|
|
||||||
|
return commands
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func executeMain(file, system, commandInput string, timeout int) {
|
func executeMain(file, system, commandInput string, timeout int) {
|
||||||
|
// Валидация длины пользовательского сообщения
|
||||||
|
if err := validation.ValidateUserMessage(commandInput); err != nil {
|
||||||
|
printColored(fmt.Sprintf("❌ Ошибка: %s\n", err.Error()), colorRed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Валидация длины системного промпта
|
||||||
|
if err := validation.ValidateSystemPrompt(system); err != nil {
|
||||||
|
printColored(fmt.Sprintf("❌ Ошибка: %s\n", err.Error()), colorRed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Выводим debug информацию если включен флаг
|
// Выводим debug информацию если включен флаг
|
||||||
if config.AppConfig.MainFlags.Debug {
|
if config.AppConfig.MainFlags.Debug {
|
||||||
printDebugInfo(file, system, commandInput, timeout)
|
printDebugInfo(file, system, commandInput, timeout)
|
||||||
@@ -884,3 +1071,112 @@ func openBrowserURL(url string) error {
|
|||||||
|
|
||||||
return fmt.Errorf("не найден ни один из поддерживаемых браузеров")
|
return fmt.Errorf("не найден ни один из поддерживаемых браузеров")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getServerAllowHTTPForHost определяет AllowHTTP для конкретного хоста
|
||||||
|
func getServerAllowHTTPForHost(host string) bool {
|
||||||
|
// Если переменная явно установлена, используем её
|
||||||
|
if value, exists := os.LookupEnv("LCG_SERVER_ALLOW_HTTP"); exists {
|
||||||
|
if boolValue, err := strconv.ParseBool(value); err == nil {
|
||||||
|
return boolValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если переменная не установлена, определяем по умолчанию на основе хоста
|
||||||
|
return isSecureHost(host)
|
||||||
|
}
|
||||||
|
|
||||||
|
// isSecureHost проверяет, является ли хост безопасным для HTTP
|
||||||
|
func isSecureHost(host string) bool {
|
||||||
|
secureHosts := []string{"localhost", "127.0.0.1", "::1"}
|
||||||
|
return slices.Contains(secureHosts, host)
|
||||||
|
}
|
||||||
|
|
||||||
|
// showShortConfig показывает краткую конфигурацию
|
||||||
|
func showShortConfig() {
|
||||||
|
fmt.Printf("Provider: %s\n", config.AppConfig.ProviderType)
|
||||||
|
fmt.Printf("Host: %s\n", config.AppConfig.Host)
|
||||||
|
fmt.Printf("Model: %s\n", config.AppConfig.Model)
|
||||||
|
fmt.Printf("Prompt: %s\n", config.AppConfig.Prompt)
|
||||||
|
fmt.Printf("Timeout: %s seconds\n", config.AppConfig.Timeout)
|
||||||
|
if config.AppConfig.ProviderType == "proxy" {
|
||||||
|
fmt.Printf("JWT Token: %s\n", func() string {
|
||||||
|
if config.AppConfig.JwtToken != "" {
|
||||||
|
return "***set***"
|
||||||
|
}
|
||||||
|
currentUser, _ := user.Current()
|
||||||
|
jwtFile := currentUser.HomeDir + "/.proxy_jwt_token"
|
||||||
|
if _, err := os.Stat(jwtFile); err == nil {
|
||||||
|
return "***from file***"
|
||||||
|
}
|
||||||
|
return "***not set***"
|
||||||
|
}())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// showFullConfig показывает полную конфигурацию в JSON формате
|
||||||
|
func showFullConfig() {
|
||||||
|
// Создаем структуру для безопасного вывода (скрываем чувствительные данные)
|
||||||
|
type SafeConfig struct {
|
||||||
|
Cwd string `json:"cwd"`
|
||||||
|
Host string `json:"host"`
|
||||||
|
Completions string `json:"completions"`
|
||||||
|
Model string `json:"model"`
|
||||||
|
Prompt string `json:"prompt"`
|
||||||
|
ApiKeyFile string `json:"api_key_file"`
|
||||||
|
ResultFolder string `json:"result_folder"`
|
||||||
|
PromptFolder string `json:"prompt_folder"`
|
||||||
|
ProviderType string `json:"provider_type"`
|
||||||
|
JwtToken string `json:"jwt_token"` // Показываем статус, не сам токен
|
||||||
|
PromptID string `json:"prompt_id"`
|
||||||
|
Timeout string `json:"timeout"`
|
||||||
|
ResultHistory string `json:"result_history"`
|
||||||
|
NoHistoryEnv string `json:"no_history_env"`
|
||||||
|
AllowExecution bool `json:"allow_execution"`
|
||||||
|
MainFlags config.MainFlags `json:"main_flags"`
|
||||||
|
Server config.ServerConfig `json:"server"`
|
||||||
|
Validation config.ValidationConfig `json:"validation"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем безопасную копию конфигурации
|
||||||
|
safeConfig := SafeConfig{
|
||||||
|
Cwd: config.AppConfig.Cwd,
|
||||||
|
Host: config.AppConfig.Host,
|
||||||
|
Completions: config.AppConfig.Completions,
|
||||||
|
Model: config.AppConfig.Model,
|
||||||
|
Prompt: config.AppConfig.Prompt,
|
||||||
|
ApiKeyFile: config.AppConfig.ApiKeyFile,
|
||||||
|
ResultFolder: config.AppConfig.ResultFolder,
|
||||||
|
PromptFolder: config.AppConfig.PromptFolder,
|
||||||
|
ProviderType: config.AppConfig.ProviderType,
|
||||||
|
JwtToken: func() string {
|
||||||
|
if config.AppConfig.JwtToken != "" {
|
||||||
|
return "***set***"
|
||||||
|
}
|
||||||
|
currentUser, _ := user.Current()
|
||||||
|
jwtFile := currentUser.HomeDir + "/.proxy_jwt_token"
|
||||||
|
if _, err := os.Stat(jwtFile); err == nil {
|
||||||
|
return "***from file***"
|
||||||
|
}
|
||||||
|
return "***not set***"
|
||||||
|
}(),
|
||||||
|
PromptID: config.AppConfig.PromptID,
|
||||||
|
Timeout: config.AppConfig.Timeout,
|
||||||
|
ResultHistory: config.AppConfig.ResultHistory,
|
||||||
|
NoHistoryEnv: config.AppConfig.NoHistoryEnv,
|
||||||
|
AllowExecution: config.AppConfig.AllowExecution,
|
||||||
|
MainFlags: config.AppConfig.MainFlags,
|
||||||
|
Server: config.AppConfig.Server,
|
||||||
|
Validation: config.AppConfig.Validation,
|
||||||
|
}
|
||||||
|
|
||||||
|
safeConfig.Server.Password = "***"
|
||||||
|
|
||||||
|
// Выводим JSON с отступами
|
||||||
|
jsonData, err := json.MarshalIndent(safeConfig, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Ошибка сериализации конфигурации: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(string(jsonData))
|
||||||
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
package main
|
|
||||||
37
serve/api.go
37
serve/api.go
@@ -10,6 +10,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/direct-dev-ru/linux-command-gpt/config"
|
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||||
|
"github.com/direct-dev-ru/linux-command-gpt/validation"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SaveResultRequest представляет запрос на сохранение результата
|
// SaveResultRequest представляет запрос на сохранение результата
|
||||||
@@ -62,6 +63,20 @@ func handleSaveResult(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Валидация длины полей
|
||||||
|
if err := validation.ValidateUserMessage(req.Prompt); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := validation.ValidateCommand(req.Command); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := validation.ValidateExplanation(req.Explanation); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Создаем папку результатов если не существует
|
// Создаем папку результатов если не существует
|
||||||
if err := os.MkdirAll(config.AppConfig.ResultFolder, 0755); err != nil {
|
if err := os.MkdirAll(config.AppConfig.ResultFolder, 0755); err != nil {
|
||||||
apiJsonResponse(w, SaveResultResponse{
|
apiJsonResponse(w, SaveResultResponse{
|
||||||
@@ -124,6 +139,28 @@ func handleAddToHistory(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Валидация длины полей
|
||||||
|
if err := validation.ValidateUserMessage(req.Prompt); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := validation.ValidateCommand(req.Command); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := validation.ValidateCommand(req.Response); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := validation.ValidateExplanation(req.Explanation); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := validation.ValidateSystemPrompt(req.System); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Проверяем, есть ли уже такой запрос в истории
|
// Проверяем, есть ли уже такой запрос в истории
|
||||||
entries, err := Read(config.AppConfig.ResultHistory)
|
entries, err := Read(config.AppConfig.ResultHistory)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
268
serve/auth.go
Normal file
268
serve/auth.go
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
package serve
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JWTClaims представляет claims для JWT токена
|
||||||
|
type JWTClaims struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
jwt.RegisteredClaims
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthRequest представляет запрос на аутентификацию
|
||||||
|
type AuthRequest struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthResponse представляет ответ на аутентификацию
|
||||||
|
type AuthResponse struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// JWTSecretKey генерирует или загружает секретный ключ для JWT
|
||||||
|
func getJWTSecretKey() ([]byte, error) {
|
||||||
|
// Пытаемся загрузить из переменной окружения
|
||||||
|
if secret := os.Getenv("LCG_JWT_SECRET"); secret != "" {
|
||||||
|
return []byte(secret), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Пытаемся загрузить из файла
|
||||||
|
secretFile := fmt.Sprintf("%s/server/jwt_secret", config.AppConfig.Server.ConfigFolder)
|
||||||
|
if data, err := os.ReadFile(secretFile); err == nil {
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Генерируем новый секретный ключ
|
||||||
|
secret := make([]byte, 32)
|
||||||
|
if _, err := rand.Read(secret); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to generate JWT secret: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем директорию если не существует
|
||||||
|
if err := os.MkdirAll(fmt.Sprintf("%s/server", config.AppConfig.Server.ConfigFolder), 0755); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create config directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохраняем секретный ключ в файл
|
||||||
|
if err := os.WriteFile(secretFile, secret, 0600); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to save JWT secret: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return secret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateJWTToken создает JWT токен для пользователя
|
||||||
|
func generateJWTToken(username string) (string, error) {
|
||||||
|
secret, err := getJWTSecretKey()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем claims
|
||||||
|
claims := JWTClaims{
|
||||||
|
Username: username,
|
||||||
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)), // Токен действителен 24 часа
|
||||||
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||||
|
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||||
|
Issuer: "lcg-server",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем токен
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
|
return token.SignedString(secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateJWTToken проверяет JWT токен
|
||||||
|
func validateJWTToken(tokenString string) (*JWTClaims, error) {
|
||||||
|
secret, err := getJWTSecretKey()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Парсим токен
|
||||||
|
token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (any, error) {
|
||||||
|
// Проверяем метод подписи
|
||||||
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
|
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||||
|
}
|
||||||
|
return secret, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем валидность токена
|
||||||
|
if claims, ok := token.Claims.(*JWTClaims); ok && token.Valid {
|
||||||
|
return claims, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("invalid token")
|
||||||
|
}
|
||||||
|
|
||||||
|
// getTokenFromCookie извлекает JWT токен из cookies
|
||||||
|
func getTokenFromCookie(r *http.Request) (string, error) {
|
||||||
|
cookie, err := r.Cookie("auth_token")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return cookie.Value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// setAuthCookie устанавливает HTTP-only cookie с JWT токеном
|
||||||
|
func setAuthCookie(w http.ResponseWriter, token string) {
|
||||||
|
cookie := &http.Cookie{
|
||||||
|
Name: "auth_token",
|
||||||
|
Value: token,
|
||||||
|
Path: config.AppConfig.Server.CookiePath,
|
||||||
|
HttpOnly: true,
|
||||||
|
Secure: config.AppConfig.Server.CookieSecure,
|
||||||
|
SameSite: http.SameSiteLaxMode, // Более мягкий режим для reverse proxy
|
||||||
|
MaxAge: config.AppConfig.Server.CookieTTLHours * 60 * 60,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем домен если указан
|
||||||
|
if config.AppConfig.Server.Domain != "" {
|
||||||
|
cookie.Domain = config.AppConfig.Server.Domain
|
||||||
|
}
|
||||||
|
|
||||||
|
http.SetCookie(w, cookie)
|
||||||
|
}
|
||||||
|
|
||||||
|
// clearAuthCookie удаляет cookie с токеном
|
||||||
|
func clearAuthCookie(w http.ResponseWriter) {
|
||||||
|
cookie := &http.Cookie{
|
||||||
|
Name: "auth_token",
|
||||||
|
Value: "",
|
||||||
|
Path: config.AppConfig.Server.CookiePath,
|
||||||
|
HttpOnly: true,
|
||||||
|
Secure: config.AppConfig.Server.CookieSecure,
|
||||||
|
SameSite: http.SameSiteLaxMode,
|
||||||
|
MaxAge: -1, // Удаляем cookie
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем домен если указан
|
||||||
|
if config.AppConfig.Server.Domain != "" {
|
||||||
|
cookie.Domain = config.AppConfig.Server.Domain
|
||||||
|
}
|
||||||
|
|
||||||
|
http.SetCookie(w, cookie)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleLogin обрабатывает запрос на вход
|
||||||
|
func handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req AuthRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
apiJsonResponse(w, AuthResponse{
|
||||||
|
Success: false,
|
||||||
|
Error: "Invalid request body",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем пароль
|
||||||
|
if req.Password != config.AppConfig.Server.Password {
|
||||||
|
apiJsonResponse(w, AuthResponse{
|
||||||
|
Success: false,
|
||||||
|
Error: "Неверный пароль",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Генерируем JWT токен
|
||||||
|
token, err := generateJWTToken(req.Username)
|
||||||
|
if err != nil {
|
||||||
|
apiJsonResponse(w, AuthResponse{
|
||||||
|
Success: false,
|
||||||
|
Error: "Failed to generate token",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Устанавливаем cookie
|
||||||
|
setAuthCookie(w, token)
|
||||||
|
|
||||||
|
apiJsonResponse(w, AuthResponse{
|
||||||
|
Success: true,
|
||||||
|
Message: "Успешная авторизация",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleLogout обрабатывает запрос на выход
|
||||||
|
func handleLogout(w http.ResponseWriter, r *http.Request) {
|
||||||
|
clearAuthCookie(w)
|
||||||
|
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleValidateToken обрабатывает проверку валидности токена
|
||||||
|
func handleValidateToken(w http.ResponseWriter, r *http.Request) {
|
||||||
|
token, err := getTokenFromCookie(r)
|
||||||
|
if err != nil {
|
||||||
|
apiJsonResponse(w, AuthResponse{
|
||||||
|
Success: false,
|
||||||
|
Error: "Token not found",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = validateJWTToken(token)
|
||||||
|
if err != nil {
|
||||||
|
apiJsonResponse(w, AuthResponse{
|
||||||
|
Success: false,
|
||||||
|
Error: "Invalid token",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiJsonResponse(w, AuthResponse{
|
||||||
|
Success: true,
|
||||||
|
Message: "Token is valid",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// requireAuth middleware проверяет аутентификацию
|
||||||
|
func requireAuth(next http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Проверяем, требуется ли аутентификация
|
||||||
|
if !config.AppConfig.Server.RequireAuth {
|
||||||
|
next(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем токен из cookie
|
||||||
|
token, err := getTokenFromCookie(r)
|
||||||
|
if err != nil {
|
||||||
|
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем валидность токена
|
||||||
|
_, err = validateJWTToken(token)
|
||||||
|
if err != nil {
|
||||||
|
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Токен валиден, продолжаем
|
||||||
|
next(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
272
serve/csrf.go
Normal file
272
serve/csrf.go
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
package serve
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
|
||||||
|
// CSRFData содержит данные для CSRF токена
|
||||||
|
type CSRFData struct {
|
||||||
|
Token string
|
||||||
|
Timestamp int64
|
||||||
|
UserID string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCSRFManager создает новый менеджер CSRF
|
||||||
|
func NewCSRFManager() (*CSRFManager, error) {
|
||||||
|
secret, err := getCSRFSecretKey()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &CSRFManager{secretKey: secret}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getCSRFSecretKey получает или генерирует секретный ключ для CSRF
|
||||||
|
func getCSRFSecretKey() ([]byte, error) {
|
||||||
|
// Пытаемся загрузить из переменной окружения
|
||||||
|
if secret := os.Getenv("LCG_CSRF_SECRET"); secret != "" {
|
||||||
|
return []byte(secret), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Пытаемся загрузить из файла
|
||||||
|
secretFile := fmt.Sprintf("%s/server/csrf_secret", config.AppConfig.Server.ConfigFolder)
|
||||||
|
if data, err := os.ReadFile(secretFile); err == nil {
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Генерируем новый секретный ключ
|
||||||
|
secret := make([]byte, 32)
|
||||||
|
if _, err := rand.Read(secret); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to generate CSRF secret: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем директорию если не существует
|
||||||
|
if err := os.MkdirAll(fmt.Sprintf("%s/server", config.AppConfig.Server.ConfigFolder), 0755); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create config directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохраняем секретный ключ в файл
|
||||||
|
if err := os.WriteFile(secretFile, secret, 0600); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to save CSRF secret: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return secret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateToken генерирует CSRF токен для пользователя
|
||||||
|
func (c *CSRFManager) GenerateToken(userID string) (string, error) {
|
||||||
|
// Создаем данные токена
|
||||||
|
data := CSRFData{
|
||||||
|
Token: generateRandomString(32),
|
||||||
|
Timestamp: time.Now().Unix(),
|
||||||
|
UserID: userID,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем подпись
|
||||||
|
signature := c.createSignature(data)
|
||||||
|
|
||||||
|
// Кодируем данные в base64
|
||||||
|
encodedData := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%d:%s", data.Token, data.Timestamp, data.UserID)))
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s.%s", encodedData, signature), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateToken проверяет CSRF токен
|
||||||
|
func (c *CSRFManager) ValidateToken(token, userID string) bool {
|
||||||
|
// Разделяем токен на данные и подпись
|
||||||
|
parts := splitToken(token)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
encodedData, signature := parts[0], parts[1]
|
||||||
|
|
||||||
|
// Декодируем данные
|
||||||
|
dataBytes, err := base64.StdEncoding.DecodeString(encodedData)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Парсим данные
|
||||||
|
dataParts := splitString(string(dataBytes), ":")
|
||||||
|
if len(dataParts) != 3 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenValue, timestampStr, tokenUserID := dataParts[0], dataParts[1], dataParts[2]
|
||||||
|
|
||||||
|
// Проверяем пользователя
|
||||||
|
if tokenUserID != userID {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем время жизни токена (минимум 12 часов)
|
||||||
|
timestamp, err := parseInt64(timestampStr)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Минимальное время жизни токена: 12 часов (не менее 12 часов согласно требованиям)
|
||||||
|
if time.Now().Unix()-timestamp > CSRFTokenLifetimeSeconds {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем данные для проверки подписи
|
||||||
|
data := CSRFData{
|
||||||
|
Token: tokenValue,
|
||||||
|
Timestamp: timestamp,
|
||||||
|
UserID: tokenUserID,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем подпись
|
||||||
|
expectedSignature := c.createSignature(data)
|
||||||
|
return signature == expectedSignature
|
||||||
|
}
|
||||||
|
|
||||||
|
// createSignature создает подпись для данных
|
||||||
|
func (c *CSRFManager) createSignature(data CSRFData) string {
|
||||||
|
message := fmt.Sprintf("%s:%d:%s", data.Token, data.Timestamp, data.UserID)
|
||||||
|
hash := sha256.Sum256(append(c.secretKey, []byte(message)...))
|
||||||
|
return hex.EncodeToString(hash[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// getTokenFromCookie извлекает CSRF токен из cookie
|
||||||
|
func GetCSRFTokenFromCookie(r *http.Request) string {
|
||||||
|
cookie, err := r.Cookie("csrf_token")
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return cookie.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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: CSRFTokenLifetimeSeconds, // Минимум 12 часов в секундах
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем домен если указан
|
||||||
|
if config.AppConfig.Server.Domain != "" {
|
||||||
|
cookie.Domain = config.AppConfig.Server.Domain
|
||||||
|
}
|
||||||
|
|
||||||
|
http.SetCookie(w, cookie)
|
||||||
|
}
|
||||||
|
|
||||||
|
// clearCSRFCookie удаляет CSRF cookie
|
||||||
|
func СlearCSRFCookie(w http.ResponseWriter) {
|
||||||
|
cookie := &http.Cookie{
|
||||||
|
Name: "csrf_token",
|
||||||
|
Value: "",
|
||||||
|
Path: config.AppConfig.Server.CookiePath,
|
||||||
|
HttpOnly: true,
|
||||||
|
Secure: config.AppConfig.Server.CookieSecure,
|
||||||
|
SameSite: http.SameSiteLaxMode,
|
||||||
|
MaxAge: -1,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем домен если указан
|
||||||
|
if config.AppConfig.Server.Domain != "" {
|
||||||
|
cookie.Domain = config.AppConfig.Server.Domain
|
||||||
|
}
|
||||||
|
|
||||||
|
http.SetCookie(w, cookie)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateRandomString генерирует случайную строку
|
||||||
|
func generateRandomString(length int) string {
|
||||||
|
bytes := make([]byte, length)
|
||||||
|
rand.Read(bytes)
|
||||||
|
return base64.URLEncoding.EncodeToString(bytes)[:length]
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitToken разделяет токен на части
|
||||||
|
func splitToken(token string) []string {
|
||||||
|
// Ищем последнюю точку
|
||||||
|
lastDot := -1
|
||||||
|
for i := len(token) - 1; i >= 0; i-- {
|
||||||
|
if token[i] == '.' {
|
||||||
|
lastDot = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if lastDot == -1 {
|
||||||
|
return []string{token}
|
||||||
|
}
|
||||||
|
|
||||||
|
return []string{token[:lastDot], token[lastDot+1:]}
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitString разделяет строку по разделителю
|
||||||
|
func splitString(s, sep string) []string {
|
||||||
|
if s == "" {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []string
|
||||||
|
start := 0
|
||||||
|
for i := 0; i < len(s); i++ {
|
||||||
|
if i+len(sep) <= len(s) && s[i:i+len(sep)] == sep {
|
||||||
|
result = append(result, s[start:i])
|
||||||
|
start = i + len(sep)
|
||||||
|
i += len(sep) - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = append(result, s[start:])
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseInt64 парсит строку в int64
|
||||||
|
func parseInt64(s string) (int64, error) {
|
||||||
|
var result int64
|
||||||
|
for _, char := range s {
|
||||||
|
if char < '0' || char > '9' {
|
||||||
|
return 0, fmt.Errorf("invalid number: %s", s)
|
||||||
|
}
|
||||||
|
result = result*10 + int64(char-'0')
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Глобальный экземпляр CSRF менеджера
|
||||||
|
var csrfManager *CSRFManager
|
||||||
|
|
||||||
|
// InitCSRFManager инициализирует глобальный CSRF менеджер
|
||||||
|
func InitCSRFManager() error {
|
||||||
|
var err error
|
||||||
|
csrfManager, err = NewCSRFManager()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCSRFManager возвращает глобальный CSRF менеджер
|
||||||
|
func GetCSRFManager() *CSRFManager {
|
||||||
|
return csrfManager
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/direct-dev-ru/linux-command-gpt/config"
|
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||||
"github.com/direct-dev-ru/linux-command-gpt/gpt"
|
"github.com/direct-dev-ru/linux-command-gpt/gpt"
|
||||||
|
"github.com/direct-dev-ru/linux-command-gpt/validation"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ExecuteRequest представляет запрос на выполнение
|
// ExecuteRequest представляет запрос на выполнение
|
||||||
@@ -58,9 +59,20 @@ func handleExecute(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Валидация длины пользовательского сообщения
|
||||||
|
if err := validation.ValidateUserMessage(req.Prompt); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Определяем системный промпт
|
// Определяем системный промпт
|
||||||
systemPrompt := ""
|
systemPrompt := ""
|
||||||
if req.SystemText != "" {
|
if req.SystemText != "" {
|
||||||
|
// Валидация длины пользовательского системного промпта
|
||||||
|
if err := validation.ValidateSystemPrompt(req.SystemText); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
systemPrompt = req.SystemText
|
systemPrompt = req.SystemText
|
||||||
} else if req.SystemID > 0 && req.SystemID <= 5 {
|
} else if req.SystemID > 0 && req.SystemID <= 5 {
|
||||||
// Получаем системный промпт по ID
|
// Получаем системный промпт по ID
|
||||||
@@ -70,9 +82,19 @@ func handleExecute(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, "Failed to get system prompt", http.StatusInternalServerError)
|
http.Error(w, "Failed to get system prompt", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Валидация длины системного промпта из базы
|
||||||
|
if err := validation.ValidateSystemPrompt(prompt.Content); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
systemPrompt = prompt.Content
|
systemPrompt = prompt.Content
|
||||||
} else {
|
} else {
|
||||||
// Используем промпт по умолчанию
|
// Используем промпт по умолчанию
|
||||||
|
// Валидация длины системного промпта по умолчанию
|
||||||
|
if err := validation.ValidateSystemPrompt(config.AppConfig.Prompt); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
systemPrompt = config.AppConfig.Prompt
|
systemPrompt = config.AppConfig.Prompt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/direct-dev-ru/linux-command-gpt/config"
|
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||||
"github.com/direct-dev-ru/linux-command-gpt/gpt"
|
"github.com/direct-dev-ru/linux-command-gpt/gpt"
|
||||||
"github.com/direct-dev-ru/linux-command-gpt/serve/templates"
|
"github.com/direct-dev-ru/linux-command-gpt/serve/templates"
|
||||||
|
"github.com/direct-dev-ru/linux-command-gpt/validation"
|
||||||
"github.com/russross/blackfriday/v2"
|
"github.com/russross/blackfriday/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -22,6 +23,14 @@ type ExecutePageData struct {
|
|||||||
ResultSection template.HTML
|
ResultSection template.HTML
|
||||||
VerboseButtons template.HTML
|
VerboseButtons template.HTML
|
||||||
ActionButtons template.HTML
|
ActionButtons template.HTML
|
||||||
|
CSRFToken string
|
||||||
|
ProviderType string
|
||||||
|
Model string
|
||||||
|
Host string
|
||||||
|
BasePath string
|
||||||
|
AppName string
|
||||||
|
// Поля конфигурации для валидации
|
||||||
|
MaxUserMessageLength int
|
||||||
}
|
}
|
||||||
|
|
||||||
// SystemPromptOption представляет опцию системного промпта
|
// SystemPromptOption представляет опцию системного промпта
|
||||||
@@ -47,7 +56,7 @@ func handleExecutePage(w http.ResponseWriter, r *http.Request) {
|
|||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
// Показываем форму
|
// Показываем форму
|
||||||
showExecuteForm(w)
|
showExecuteForm(w, r)
|
||||||
case http.MethodPost:
|
case http.MethodPost:
|
||||||
// Обрабатываем выполнение
|
// Обрабатываем выполнение
|
||||||
handleExecuteRequest(w, r)
|
handleExecuteRequest(w, r)
|
||||||
@@ -57,7 +66,25 @@ func handleExecutePage(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// showExecuteForm показывает форму выполнения
|
// showExecuteForm показывает форму выполнения
|
||||||
func showExecuteForm(w http.ResponseWriter) {
|
func showExecuteForm(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Генерируем CSRF токен
|
||||||
|
csrfManager := GetCSRFManager()
|
||||||
|
if csrfManager == nil {
|
||||||
|
http.Error(w, "CSRF manager not initialized", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем сессионный ID
|
||||||
|
sessionID := getSessionID(r)
|
||||||
|
csrfToken, err := csrfManager.GenerateToken(sessionID)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Failed to generate CSRF token", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Устанавливаем CSRF токен в cookie
|
||||||
|
setCSRFCookie(w, csrfToken)
|
||||||
|
|
||||||
// Получаем системные промпты
|
// Получаем системные промпты
|
||||||
pm := gpt.NewPromptManager(config.AppConfig.PromptFolder)
|
pm := gpt.NewPromptManager(config.AppConfig.PromptFolder)
|
||||||
|
|
||||||
@@ -74,13 +101,20 @@ func showExecuteForm(w http.ResponseWriter) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
data := ExecutePageData{
|
data := ExecutePageData{
|
||||||
Title: "Выполнение запроса",
|
Title: "Выполнение запроса",
|
||||||
Header: "Выполнение запроса",
|
Header: "Выполнение запроса",
|
||||||
CurrentPrompt: "",
|
CurrentPrompt: "",
|
||||||
SystemOptions: systemOptions,
|
SystemOptions: systemOptions,
|
||||||
ResultSection: template.HTML(""),
|
ResultSection: template.HTML(""),
|
||||||
VerboseButtons: template.HTML(""),
|
VerboseButtons: template.HTML(""),
|
||||||
ActionButtons: template.HTML(""),
|
ActionButtons: template.HTML(""),
|
||||||
|
CSRFToken: csrfToken,
|
||||||
|
ProviderType: config.AppConfig.ProviderType,
|
||||||
|
Model: config.AppConfig.Model,
|
||||||
|
Host: config.AppConfig.Host,
|
||||||
|
BasePath: getBasePath(),
|
||||||
|
AppName: config.AppConfig.AppName,
|
||||||
|
MaxUserMessageLength: config.AppConfig.Validation.MaxUserMessageLength,
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
@@ -102,6 +136,12 @@ func handleExecuteRequest(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Валидация длины пользовательского сообщения
|
||||||
|
if err := validation.ValidateUserMessage(prompt); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
systemID := 1
|
systemID := 1
|
||||||
if systemIDStr != "" {
|
if systemIDStr != "" {
|
||||||
if id, err := strconv.Atoi(systemIDStr); err == nil && id >= 1 && id <= 5 {
|
if id, err := strconv.Atoi(systemIDStr); err == nil && id >= 1 && id <= 5 {
|
||||||
@@ -116,6 +156,12 @@ func handleExecuteRequest(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Валидация длины системного промпта
|
||||||
|
if err := validation.ValidateSystemPrompt(systemPrompt.Content); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Создаем GPT клиент
|
// Создаем GPT клиент
|
||||||
gpt3 := gpt.NewGpt3(
|
gpt3 := gpt.NewGpt3(
|
||||||
config.AppConfig.ProviderType,
|
config.AppConfig.ProviderType,
|
||||||
@@ -178,14 +224,30 @@ func handleExecuteRequest(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Генерируем CSRF токен для результата
|
||||||
|
csrfManager := GetCSRFManager()
|
||||||
|
sessionID := getSessionID(r)
|
||||||
|
csrfToken, err := csrfManager.GenerateToken(sessionID)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Failed to generate CSRF token", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
data := ExecutePageData{
|
data := ExecutePageData{
|
||||||
Title: "Результат выполнения",
|
Title: "Результат выполнения",
|
||||||
Header: "Результат выполнения",
|
Header: "Результат выполнения",
|
||||||
CurrentPrompt: prompt,
|
CurrentPrompt: prompt,
|
||||||
SystemOptions: systemOptions,
|
SystemOptions: systemOptions,
|
||||||
ResultSection: template.HTML(formatResultSection(result)),
|
ResultSection: template.HTML(formatResultSection(result)),
|
||||||
VerboseButtons: template.HTML(formatVerboseButtons(result)),
|
VerboseButtons: template.HTML(formatVerboseButtons(result)),
|
||||||
ActionButtons: template.HTML(formatActionButtons(result)),
|
ActionButtons: template.HTML(formatActionButtons(result)),
|
||||||
|
CSRFToken: csrfToken,
|
||||||
|
ProviderType: config.AppConfig.ProviderType,
|
||||||
|
Model: config.AppConfig.Model,
|
||||||
|
Host: config.AppConfig.Host,
|
||||||
|
BasePath: getBasePath(),
|
||||||
|
AppName: config.AppConfig.AppName,
|
||||||
|
MaxUserMessageLength: config.AppConfig.Validation.MaxUserMessageLength,
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -38,9 +39,13 @@ func handleHistoryPage(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
data := struct {
|
data := struct {
|
||||||
Entries []HistoryEntryInfo
|
Entries []HistoryEntryInfo
|
||||||
|
BasePath string
|
||||||
|
AppName string
|
||||||
}{
|
}{
|
||||||
Entries: historyEntries,
|
Entries: historyEntries,
|
||||||
|
BasePath: getBasePath(),
|
||||||
|
AppName: config.AppConfig.AppName,
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
@@ -54,6 +59,11 @@ func readHistoryEntries() ([]HistoryEntryInfo, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Сортируем записи по времени в убывающем порядке (новые сначала)
|
||||||
|
sort.Slice(entries, func(i, j int) bool {
|
||||||
|
return entries[i].Timestamp.After(entries[j].Timestamp)
|
||||||
|
})
|
||||||
|
|
||||||
var result []HistoryEntryInfo
|
var result []HistoryEntryInfo
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
result = append(result, HistoryEntryInfo{
|
result = append(result, HistoryEntryInfo{
|
||||||
@@ -74,7 +84,15 @@ func handleDeleteHistoryEntry(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
indexStr := strings.TrimPrefix(r.URL.Path, "/history/delete/")
|
// Убираем BasePath из URL перед извлечением индекса
|
||||||
|
basePath := config.AppConfig.Server.BasePath
|
||||||
|
var indexStr string
|
||||||
|
if basePath != "" && basePath != "/" {
|
||||||
|
basePath = strings.TrimSuffix(basePath, "/")
|
||||||
|
indexStr = strings.TrimPrefix(r.URL.Path, basePath+"/history/delete/")
|
||||||
|
} else {
|
||||||
|
indexStr = strings.TrimPrefix(r.URL.Path, "/history/delete/")
|
||||||
|
}
|
||||||
index, err := strconv.Atoi(indexStr)
|
index, err := strconv.Atoi(indexStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Invalid index", http.StatusBadRequest)
|
http.Error(w, "Invalid index", http.StatusBadRequest)
|
||||||
@@ -110,8 +128,15 @@ func handleClearHistory(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// handleHistoryView обрабатывает просмотр записи истории
|
// handleHistoryView обрабатывает просмотр записи истории
|
||||||
func handleHistoryView(w http.ResponseWriter, r *http.Request) {
|
func handleHistoryView(w http.ResponseWriter, r *http.Request) {
|
||||||
// Получаем индекс из URL
|
// Получаем индекс из URL, учитывая BasePath
|
||||||
indexStr := strings.TrimPrefix(r.URL.Path, "/history/view/")
|
basePath := config.AppConfig.Server.BasePath
|
||||||
|
var indexStr string
|
||||||
|
if basePath != "" && basePath != "/" {
|
||||||
|
basePath = strings.TrimSuffix(basePath, "/")
|
||||||
|
indexStr = strings.TrimPrefix(r.URL.Path, basePath+"/history/view/")
|
||||||
|
} else {
|
||||||
|
indexStr = strings.TrimPrefix(r.URL.Path, "/history/view/")
|
||||||
|
}
|
||||||
index, err := strconv.Atoi(indexStr)
|
index, err := strconv.Atoi(indexStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
@@ -151,18 +176,31 @@ func handleHistoryView(w http.ResponseWriter, r *http.Request) {
|
|||||||
</div>`, string(explanationHTML))
|
</div>`, string(explanationHTML))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Создаем HTML страницу
|
// Создаем данные для шаблона
|
||||||
htmlPage := fmt.Sprintf(templates.HistoryViewTemplate,
|
data := struct {
|
||||||
index, // title
|
Index int
|
||||||
index, // header
|
Timestamp string
|
||||||
targetEntry.Timestamp.Format("02.01.2006 15:04:05"), // timestamp
|
Command string
|
||||||
index, // meta index
|
Response string
|
||||||
targetEntry.Command, // command
|
ExplanationHTML template.HTML
|
||||||
targetEntry.Response, // response
|
BasePath string
|
||||||
explanationSection, // explanation (if exists)
|
}{
|
||||||
index, // delete button index
|
Index: index,
|
||||||
)
|
Timestamp: targetEntry.Timestamp.Format("02.01.2006 15:04:05"),
|
||||||
|
Command: targetEntry.Command,
|
||||||
|
Response: targetEntry.Response,
|
||||||
|
ExplanationHTML: template.HTML(explanationSection),
|
||||||
|
BasePath: getBasePath(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Парсим и выполняем шаблон
|
||||||
|
tmpl := templates.HistoryViewTemplate
|
||||||
|
t, err := template.New("history_view").Parse(tmpl)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Ошибка шаблона", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
w.Write([]byte(htmlPage))
|
t.Execute(w, data)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,9 +21,20 @@ type HistoryEntry struct {
|
|||||||
// read читает записи истории из файла
|
// read читает записи истории из файла
|
||||||
func Read(historyPath string) ([]HistoryEntry, error) {
|
func Read(historyPath string) ([]HistoryEntry, error) {
|
||||||
data, err := os.ReadFile(historyPath)
|
data, err := os.ReadFile(historyPath)
|
||||||
if err != nil || len(data) == 0 {
|
if err != nil {
|
||||||
|
// Если файл не существует, создаем пустой файл истории
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
emptyHistory := []HistoryEntry{}
|
||||||
|
if writeErr := Write(historyPath, emptyHistory); writeErr != nil {
|
||||||
|
return nil, fmt.Errorf("не удалось создать файл истории: %v", writeErr)
|
||||||
|
}
|
||||||
|
return emptyHistory, nil
|
||||||
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if len(data) == 0 {
|
||||||
|
return []HistoryEntry{}, nil
|
||||||
|
}
|
||||||
var items []HistoryEntry
|
var items []HistoryEntry
|
||||||
if err := json.Unmarshal(data, &items); err != nil {
|
if err := json.Unmarshal(data, &items); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
105
serve/login.go
Normal file
105
serve/login.go
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
package serve
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||||
|
"github.com/direct-dev-ru/linux-command-gpt/serve/templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
// handleLoginPage обрабатывает страницу входа
|
||||||
|
func handleLoginPage(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Если пользователь уже авторизован, перенаправляем на главную
|
||||||
|
if isAuthenticated(r) {
|
||||||
|
http.Redirect(w, r, makePath("/"), http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Генерируем CSRF токен
|
||||||
|
csrfManager := GetCSRFManager()
|
||||||
|
if csrfManager == nil {
|
||||||
|
http.Error(w, "CSRF manager not initialized", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Для неавторизованных пользователей используем сессионный ID
|
||||||
|
sessionID := getSessionID(r)
|
||||||
|
csrfToken, err := csrfManager.GenerateToken(sessionID)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Failed to generate CSRF token", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Устанавливаем CSRF токен в cookie
|
||||||
|
setCSRFCookie(w, csrfToken)
|
||||||
|
|
||||||
|
data := LoginPageData{
|
||||||
|
Title: "Авторизация - LCG",
|
||||||
|
Message: "",
|
||||||
|
Error: "",
|
||||||
|
CSRFToken: csrfToken,
|
||||||
|
BasePath: getBasePath(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := RenderLoginPage(w, data); err != nil {
|
||||||
|
http.Error(w, "Failed to render login page", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// isAuthenticated проверяет, авторизован ли пользователь
|
||||||
|
func isAuthenticated(r *http.Request) bool {
|
||||||
|
// Проверяем, требуется ли аутентификация
|
||||||
|
if !config.AppConfig.Server.RequireAuth {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем токен из cookie
|
||||||
|
token, err := getTokenFromCookie(r)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем валидность токена
|
||||||
|
_, err = validateJWTToken(token)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoginPageData представляет данные для страницы входа
|
||||||
|
type LoginPageData struct {
|
||||||
|
Title string
|
||||||
|
Message string
|
||||||
|
Error string
|
||||||
|
CSRFToken string
|
||||||
|
BasePath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderLoginPage рендерит страницу входа
|
||||||
|
func RenderLoginPage(w http.ResponseWriter, data LoginPageData) error {
|
||||||
|
tmpl, err := template.New("login").Parse(templates.LoginPageTemplate)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
return tmpl.Execute(w, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getSessionID получает или создает сессионный ID для пользователя
|
||||||
|
func getSessionID(r *http.Request) string {
|
||||||
|
// Пытаемся получить из cookie
|
||||||
|
if cookie, err := r.Cookie("session_id"); err == nil {
|
||||||
|
return cookie.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если нет cookie, генерируем новый ID на основе IP и User-Agent
|
||||||
|
ip := r.RemoteAddr
|
||||||
|
userAgent := r.Header.Get("User-Agent")
|
||||||
|
|
||||||
|
// Создаем простой хеш для сессии
|
||||||
|
hash := sha256.Sum256([]byte(ip + userAgent))
|
||||||
|
return hex.EncodeToString(hash[:])[:16]
|
||||||
|
}
|
||||||
114
serve/middleware.go
Normal file
114
serve/middleware.go
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
package serve
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuthMiddleware проверяет аутентификацию для всех запросов
|
||||||
|
func AuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Проверяем, требуется ли аутентификация
|
||||||
|
if !config.AppConfig.Server.RequireAuth {
|
||||||
|
next(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Исключаем страницу входа и API логина из проверки (с учетом BasePath)
|
||||||
|
if r.URL.Path == makePath("/login") || r.URL.Path == makePath("/api/login") || r.URL.Path == makePath("/api/validate-token") {
|
||||||
|
next(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем аутентификацию
|
||||||
|
if !isAuthenticated(r) {
|
||||||
|
// Для API запросов возвращаем JSON ошибку
|
||||||
|
if isAPIRequest(r) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
w.Write([]byte(`{"success": false, "error": "Authentication required"}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Для веб-запросов перенаправляем на страницу входа (с учетом BasePath)
|
||||||
|
http.Redirect(w, r, makePath("/login"), http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Пользователь аутентифицирован, продолжаем
|
||||||
|
next(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CSRFMiddleware проверяет CSRF токены для POST/PUT/DELETE запросов
|
||||||
|
func CSRFMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Проверяем только изменяющие запросы
|
||||||
|
if r.Method == "GET" || r.Method == "HEAD" || r.Method == "OPTIONS" {
|
||||||
|
next(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Исключаем некоторые API endpoints (с учетом BasePath)
|
||||||
|
if r.URL.Path == makePath("/api/login") || r.URL.Path == makePath("/api/logout") {
|
||||||
|
next(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем CSRF токен из заголовка или формы
|
||||||
|
csrfToken := r.Header.Get("X-CSRF-Token")
|
||||||
|
if csrfToken == "" {
|
||||||
|
csrfToken = r.FormValue("csrf_token")
|
||||||
|
}
|
||||||
|
|
||||||
|
if csrfToken == "" {
|
||||||
|
// Для API запросов возвращаем JSON ошибку
|
||||||
|
if isAPIRequest(r) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusForbidden)
|
||||||
|
w.Write([]byte(`{"success": false, "error": "CSRF token required"}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Для веб-запросов возвращаем ошибку
|
||||||
|
http.Error(w, "CSRF token required", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем сессионный ID
|
||||||
|
sessionID := getSessionID(r)
|
||||||
|
|
||||||
|
// Проверяем CSRF токен
|
||||||
|
csrfManager := GetCSRFManager()
|
||||||
|
if csrfManager == nil || !csrfManager.ValidateToken(csrfToken, sessionID) {
|
||||||
|
// Для API запросов возвращаем JSON ошибку
|
||||||
|
if isAPIRequest(r) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusForbidden)
|
||||||
|
w.Write([]byte(`{"success": false, "error": "Invalid CSRF token"}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Для веб-запросов возвращаем ошибку
|
||||||
|
http.Error(w, "Invalid CSRF token", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// CSRF токен валиден, продолжаем
|
||||||
|
next(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// isAPIRequest проверяет, является ли запрос API запросом
|
||||||
|
func isAPIRequest(r *http.Request) bool {
|
||||||
|
path := r.URL.Path
|
||||||
|
apiPrefix := makePath("/api")
|
||||||
|
return strings.HasPrefix(path, apiPrefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequireAuth обертка для requireAuth из auth.go
|
||||||
|
func RequireAuth(next http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return requireAuth(next)
|
||||||
|
}
|
||||||
122
serve/prompts.go
122
serve/prompts.go
@@ -9,8 +9,10 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||||
"github.com/direct-dev-ru/linux-command-gpt/gpt"
|
"github.com/direct-dev-ru/linux-command-gpt/gpt"
|
||||||
"github.com/direct-dev-ru/linux-command-gpt/serve/templates"
|
"github.com/direct-dev-ru/linux-command-gpt/serve/templates"
|
||||||
|
"github.com/direct-dev-ru/linux-command-gpt/validation"
|
||||||
)
|
)
|
||||||
|
|
||||||
// VerbosePrompt структура для промптов подробности
|
// VerbosePrompt структура для промптов подробности
|
||||||
@@ -82,13 +84,23 @@ func handlePromptsPage(w http.ResponseWriter, r *http.Request) {
|
|||||||
verbosePrompts := getVerbosePromptsFromFile(pm.Prompts, lang)
|
verbosePrompts := getVerbosePromptsFromFile(pm.Prompts, lang)
|
||||||
|
|
||||||
data := struct {
|
data := struct {
|
||||||
Prompts []PromptWithDefault
|
Prompts []PromptWithDefault
|
||||||
VerbosePrompts []VerbosePrompt
|
VerbosePrompts []VerbosePrompt
|
||||||
Lang string
|
Lang string
|
||||||
|
MaxSystemPromptLength int
|
||||||
|
MaxPromptNameLength int
|
||||||
|
MaxPromptDescLength int
|
||||||
|
BasePath string
|
||||||
|
AppName string
|
||||||
}{
|
}{
|
||||||
Prompts: promptsWithDefault,
|
Prompts: promptsWithDefault,
|
||||||
VerbosePrompts: verbosePrompts,
|
VerbosePrompts: verbosePrompts,
|
||||||
Lang: lang,
|
Lang: lang,
|
||||||
|
MaxSystemPromptLength: config.AppConfig.Validation.MaxSystemPromptLength,
|
||||||
|
MaxPromptNameLength: config.AppConfig.Validation.MaxPromptNameLength,
|
||||||
|
MaxPromptDescLength: config.AppConfig.Validation.MaxPromptDescLength,
|
||||||
|
BasePath: getBasePath(),
|
||||||
|
AppName: config.AppConfig.AppName,
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
@@ -124,6 +136,20 @@ func handleAddPrompt(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Валидация длины полей
|
||||||
|
if err := validation.ValidateSystemPrompt(promptData.Content); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := validation.ValidatePromptName(promptData.Name); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := validation.ValidatePromptDescription(promptData.Description); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Добавляем промпт
|
// Добавляем промпт
|
||||||
if err := pm.AddPrompt(promptData.Name, promptData.Description, promptData.Content); err != nil {
|
if err := pm.AddPrompt(promptData.Name, promptData.Description, promptData.Content); err != nil {
|
||||||
http.Error(w, fmt.Sprintf("Ошибка добавления промпта: %v", err), http.StatusInternalServerError)
|
http.Error(w, fmt.Sprintf("Ошибка добавления промпта: %v", err), http.StatusInternalServerError)
|
||||||
@@ -171,6 +197,20 @@ func handleEditPrompt(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Валидация длины полей
|
||||||
|
if err := validation.ValidateSystemPrompt(promptData.Content); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := validation.ValidatePromptName(promptData.Name); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := validation.ValidatePromptDescription(promptData.Description); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Обновляем промпт
|
// Обновляем промпт
|
||||||
if err := pm.UpdatePrompt(id, promptData.Name, promptData.Description, promptData.Content); err != nil {
|
if err := pm.UpdatePrompt(id, promptData.Name, promptData.Description, promptData.Content); err != nil {
|
||||||
http.Error(w, fmt.Sprintf("Ошибка обновления промпта: %v", err), http.StatusInternalServerError)
|
http.Error(w, fmt.Sprintf("Ошибка обновления промпта: %v", err), http.StatusInternalServerError)
|
||||||
@@ -181,6 +221,76 @@ func handleEditPrompt(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Write([]byte("Промпт успешно обновлен"))
|
w.Write([]byte("Промпт успешно обновлен"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleEditVerbosePrompt обрабатывает редактирование промпта подробности
|
||||||
|
func handleEditVerbosePrompt(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "PUT" {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем режим из URL
|
||||||
|
mode := strings.TrimPrefix(r.URL.Path, "/prompts/edit-verbose/")
|
||||||
|
|
||||||
|
// Получаем домашнюю директорию пользователя
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Ошибка получения домашней директории", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем менеджер промптов
|
||||||
|
pm := gpt.NewPromptManager(homeDir)
|
||||||
|
|
||||||
|
// Парсим JSON данные
|
||||||
|
var promptData struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&promptData); err != nil {
|
||||||
|
http.Error(w, "Ошибка парсинга JSON", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Валидация длины полей
|
||||||
|
if err := validation.ValidateSystemPrompt(promptData.Content); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := validation.ValidatePromptName(promptData.Name); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := validation.ValidatePromptDescription(promptData.Description); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Определяем ID по режиму
|
||||||
|
var id int
|
||||||
|
switch mode {
|
||||||
|
case "v":
|
||||||
|
id = 6
|
||||||
|
case "vv":
|
||||||
|
id = 7
|
||||||
|
case "vvv":
|
||||||
|
id = 8
|
||||||
|
default:
|
||||||
|
http.Error(w, "Неверный режим промпта", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем промпт
|
||||||
|
if err := pm.UpdatePrompt(id, promptData.Name, promptData.Description, promptData.Content); err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("Ошибка обновления промпта: %v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte("Промпт подробности успешно обновлен"))
|
||||||
|
}
|
||||||
|
|
||||||
// handleDeletePrompt обрабатывает удаление промпта
|
// handleDeletePrompt обрабатывает удаление промпта
|
||||||
func handleDeletePrompt(w http.ResponseWriter, r *http.Request) {
|
func handleDeletePrompt(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != "DELETE" {
|
if r.Method != "DELETE" {
|
||||||
|
|||||||
211
serve/results.go
211
serve/results.go
@@ -8,19 +8,43 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
"github.com/direct-dev-ru/linux-command-gpt/config"
|
"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/serve/templates"
|
||||||
"github.com/russross/blackfriday/v2"
|
"github.com/russross/blackfriday/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// generateAbbreviation создает аббревиатуру из первых букв слов в названии приложения
|
||||||
|
func generateAbbreviation(appName string) string {
|
||||||
|
words := strings.Fields(appName)
|
||||||
|
var abbreviation strings.Builder
|
||||||
|
|
||||||
|
for _, word := range words {
|
||||||
|
if len(word) > 0 {
|
||||||
|
// Берем первую букву слова, если это буква
|
||||||
|
firstRune := []rune(word)[0]
|
||||||
|
if unicode.IsLetter(firstRune) {
|
||||||
|
abbreviation.WriteRune(unicode.ToUpper(firstRune))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result := abbreviation.String()
|
||||||
|
if result == "" {
|
||||||
|
return "LCG" // Fallback если не удалось сгенерировать аббревиатуру
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
// FileInfo содержит информацию о файле
|
// FileInfo содержит информацию о файле
|
||||||
type FileInfo struct {
|
type FileInfo struct {
|
||||||
Name string
|
Name string
|
||||||
Size string
|
DisplayName string
|
||||||
ModTime string
|
Size string
|
||||||
Preview string
|
ModTime string
|
||||||
Content string // Полное содержимое для поиска
|
Preview template.HTML
|
||||||
|
Content string // Полное содержимое для поиска
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleResultsPage обрабатывает главную страницу со списком файлов
|
// handleResultsPage обрабатывает главную страницу со списком файлов
|
||||||
@@ -52,13 +76,19 @@ func handleResultsPage(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
data := struct {
|
data := struct {
|
||||||
Files []FileInfo
|
Files []FileInfo
|
||||||
TotalFiles int
|
TotalFiles int
|
||||||
RecentFiles int
|
RecentFiles int
|
||||||
|
BasePath string
|
||||||
|
AppName string
|
||||||
|
AppAbbreviation string
|
||||||
}{
|
}{
|
||||||
Files: files,
|
Files: files,
|
||||||
TotalFiles: len(files),
|
TotalFiles: len(files),
|
||||||
RecentFiles: recentCount,
|
RecentFiles: recentCount,
|
||||||
|
BasePath: getBasePath(),
|
||||||
|
AppName: config.AppConfig.AppName,
|
||||||
|
AppAbbreviation: generateAbbreviation(config.AppConfig.AppName),
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
@@ -83,44 +113,15 @@ func getResultFiles() ([]FileInfo, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Читаем превью файла (первые 200 символов) и конвертируем Markdown
|
// Читаем превью файла (первые 200 символов) как обычный текст
|
||||||
preview := ""
|
preview := ""
|
||||||
fullContent := ""
|
fullContent := ""
|
||||||
if content, err := os.ReadFile(filepath.Join(config.AppConfig.ResultFolder, entry.Name())); err == nil {
|
if content, err := os.ReadFile(filepath.Join(config.AppConfig.ResultFolder, entry.Name())); err == nil {
|
||||||
// Сохраняем полное содержимое для поиска
|
// Сохраняем полное содержимое для поиска
|
||||||
fullContent = string(content)
|
fullContent = string(content)
|
||||||
// Конвертируем Markdown в HTML для превью
|
|
||||||
htmlContent := blackfriday.Run(content)
|
|
||||||
preview = strings.TrimSpace(string(htmlContent))
|
|
||||||
// Удаляем HTML теги для превью
|
|
||||||
preview = strings.ReplaceAll(preview, "<h1>", "")
|
|
||||||
preview = strings.ReplaceAll(preview, "</h1>", "")
|
|
||||||
preview = strings.ReplaceAll(preview, "<h2>", "")
|
|
||||||
preview = strings.ReplaceAll(preview, "</h2>", "")
|
|
||||||
preview = strings.ReplaceAll(preview, "<h3>", "")
|
|
||||||
preview = strings.ReplaceAll(preview, "</h3>", "")
|
|
||||||
preview = strings.ReplaceAll(preview, "<p>", "")
|
|
||||||
preview = strings.ReplaceAll(preview, "</p>", "")
|
|
||||||
preview = strings.ReplaceAll(preview, "<code>", "")
|
|
||||||
preview = strings.ReplaceAll(preview, "</code>", "")
|
|
||||||
preview = strings.ReplaceAll(preview, "<pre>", "")
|
|
||||||
preview = strings.ReplaceAll(preview, "</pre>", "")
|
|
||||||
preview = strings.ReplaceAll(preview, "<strong>", "")
|
|
||||||
preview = strings.ReplaceAll(preview, "</strong>", "")
|
|
||||||
preview = strings.ReplaceAll(preview, "<em>", "")
|
|
||||||
preview = strings.ReplaceAll(preview, "</em>", "")
|
|
||||||
preview = strings.ReplaceAll(preview, "<ul>", "")
|
|
||||||
preview = strings.ReplaceAll(preview, "</ul>", "")
|
|
||||||
preview = strings.ReplaceAll(preview, "<li>", "• ")
|
|
||||||
preview = strings.ReplaceAll(preview, "</li>", "")
|
|
||||||
preview = strings.ReplaceAll(preview, "<ol>", "")
|
|
||||||
preview = strings.ReplaceAll(preview, "</ol>", "")
|
|
||||||
preview = strings.ReplaceAll(preview, "<blockquote>", "")
|
|
||||||
preview = strings.ReplaceAll(preview, "</blockquote>", "")
|
|
||||||
preview = strings.ReplaceAll(preview, "<br>", "")
|
|
||||||
preview = strings.ReplaceAll(preview, "<br/>", "")
|
|
||||||
preview = strings.ReplaceAll(preview, "<br />", "")
|
|
||||||
|
|
||||||
|
// Берем первые 200 символов как превью
|
||||||
|
preview = string(content)
|
||||||
// Очищаем от лишних пробелов и переносов
|
// Очищаем от лишних пробелов и переносов
|
||||||
preview = strings.ReplaceAll(preview, "\n", " ")
|
preview = strings.ReplaceAll(preview, "\n", " ")
|
||||||
preview = strings.ReplaceAll(preview, "\r", "")
|
preview = strings.ReplaceAll(preview, "\r", "")
|
||||||
@@ -133,11 +134,12 @@ func getResultFiles() ([]FileInfo, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
files = append(files, FileInfo{
|
files = append(files, FileInfo{
|
||||||
Name: entry.Name(),
|
Name: entry.Name(),
|
||||||
Size: formatFileSize(info.Size()),
|
DisplayName: formatFileDisplayName(entry.Name()),
|
||||||
ModTime: info.ModTime().Format("02.01.2006 15:04"),
|
Size: formatFileSize(info.Size()),
|
||||||
Preview: preview,
|
ModTime: info.ModTime().Format("02.01.2006 15:04"),
|
||||||
Content: fullContent,
|
Preview: template.HTML(preview),
|
||||||
|
Content: fullContent,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,36 +169,119 @@ func formatFileSize(size int64) string {
|
|||||||
return fmt.Sprintf("%.1f %cB", float64(size)/float64(div), "KMGTPE"[exp])
|
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 обрабатывает просмотр конкретного файла
|
// handleFileView обрабатывает просмотр конкретного файла
|
||||||
func handleFileView(w http.ResponseWriter, r *http.Request) {
|
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 == "" {
|
if filename == "" {
|
||||||
http.NotFound(w, r)
|
renderNotFound(w, "Файл не указан", getBasePath())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверяем, что файл существует и находится в папке результатов
|
// Проверяем, что файл существует и находится в папке результатов
|
||||||
filePath := filepath.Join(config.AppConfig.ResultFolder, filename)
|
filePath := filepath.Join(config.AppConfig.ResultFolder, filename)
|
||||||
if !strings.HasPrefix(filePath, config.AppConfig.ResultFolder) {
|
if !strings.HasPrefix(filePath, config.AppConfig.ResultFolder) {
|
||||||
http.NotFound(w, r)
|
renderNotFound(w, "Запрошенный файл недоступен", getBasePath())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
content, err := os.ReadFile(filePath)
|
content, err := os.ReadFile(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.NotFound(w, r)
|
renderNotFound(w, "Файл не найден или был удален", getBasePath())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Конвертируем Markdown в HTML
|
// Конвертируем Markdown в HTML
|
||||||
htmlContent := blackfriday.Run(content)
|
htmlContent := blackfriday.Run(content)
|
||||||
|
|
||||||
// Создаем HTML страницу с красивым отображением
|
// Создаем данные для шаблона
|
||||||
htmlPage := fmt.Sprintf(templates.FileViewTemplate, filename, filename, string(htmlContent))
|
data := struct {
|
||||||
|
Filename string
|
||||||
|
Content template.HTML
|
||||||
|
BasePath string
|
||||||
|
}{
|
||||||
|
Filename: filename,
|
||||||
|
Content: template.HTML(htmlContent),
|
||||||
|
BasePath: getBasePath(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Парсим и выполняем шаблон
|
||||||
|
tmpl := templates.FileViewTemplate
|
||||||
|
t, err := template.New("file_view").Parse(tmpl)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Ошибка шаблона", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Устанавливаем заголовки для отображения HTML
|
// Устанавливаем заголовки для отображения HTML
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
w.Write([]byte(htmlPage))
|
t.Execute(w, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleDeleteFile обрабатывает удаление файла
|
// handleDeleteFile обрабатывает удаление файла
|
||||||
@@ -207,22 +292,30 @@ func handleDeleteFile(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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 == "" {
|
if filename == "" {
|
||||||
http.NotFound(w, r)
|
renderNotFound(w, "Файл не указан", getBasePath())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверяем, что файл существует и находится в папке результатов
|
// Проверяем, что файл существует и находится в папке результатов
|
||||||
filePath := filepath.Join(config.AppConfig.ResultFolder, filename)
|
filePath := filepath.Join(config.AppConfig.ResultFolder, filename)
|
||||||
if !strings.HasPrefix(filePath, config.AppConfig.ResultFolder) {
|
if !strings.HasPrefix(filePath, config.AppConfig.ResultFolder) {
|
||||||
http.NotFound(w, r)
|
renderNotFound(w, "Запрошенный файл недоступен", getBasePath())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверяем, что файл существует
|
// Проверяем, что файл существует
|
||||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||||
http.NotFound(w, r)
|
renderNotFound(w, "Файл не найден или уже удален", getBasePath())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
332
serve/serve.go
332
serve/serve.go
@@ -1,59 +1,323 @@
|
|||||||
package serve
|
package serve
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/direct-dev-ru/linux-command-gpt/config"
|
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
// StartResultServer запускает HTTP сервер для просмотра сохраненных результатов
|
// makePath создает путь с учетом BasePath
|
||||||
func StartResultServer(host, port string) error {
|
func makePath(path string) string {
|
||||||
// Регистрируем все маршруты
|
basePath := config.AppConfig.Server.BasePath
|
||||||
registerRoutes()
|
if basePath == "" || basePath == "/" {
|
||||||
|
return path
|
||||||
addr := fmt.Sprintf("%s:%s", host, port)
|
|
||||||
fmt.Printf("Сервер запущен на http://%s\n", addr)
|
|
||||||
fmt.Println("Нажмите Ctrl+C для остановки")
|
|
||||||
|
|
||||||
// Тестовое логирование для проверки debug флага
|
|
||||||
if config.AppConfig.MainFlags.Debug {
|
|
||||||
fmt.Printf("🔍 DEBUG РЕЖИМ ВКЛЮЧЕН - веб-операции будут логироваться\n")
|
|
||||||
} else {
|
|
||||||
fmt.Printf("🔍 DEBUG РЕЖИМ ОТКЛЮЧЕН - веб-операции не будут логироваться\n")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return http.ListenAndServe(addr, nil)
|
// Убираем слэш в конце basePath если есть
|
||||||
|
basePath = strings.TrimSuffix(basePath, "/")
|
||||||
|
|
||||||
|
// Убираем слэш в начале path если есть
|
||||||
|
path = strings.TrimPrefix(path, "/")
|
||||||
|
|
||||||
|
// Если path пустой, возвращаем basePath с слэшем в конце
|
||||||
|
if path == "" {
|
||||||
|
return basePath + "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
return basePath + "/" + path
|
||||||
|
}
|
||||||
|
|
||||||
|
// getBasePath возвращает BasePath для использования в шаблонах
|
||||||
|
func getBasePath() string {
|
||||||
|
basePath := config.AppConfig.Server.BasePath
|
||||||
|
if basePath == "" || basePath == "/" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.TrimSuffix(basePath, "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartResultServer запускает HTTP/HTTPS сервер для просмотра сохраненных результатов
|
||||||
|
func StartResultServer(host, port string) error {
|
||||||
|
// Инициализируем CSRF менеджер
|
||||||
|
if err := InitCSRFManager(); err != nil {
|
||||||
|
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
|
||||||
|
useHTTPS := ssl.ShouldUseHTTPS(host)
|
||||||
|
|
||||||
|
if useHTTPS {
|
||||||
|
// Регистрируем HTTPS маршруты (включая редирект)
|
||||||
|
registerHTTPSRoutes()
|
||||||
|
|
||||||
|
// Создаем директорию для SSL сертификатов
|
||||||
|
sslDir := fmt.Sprintf("%s/server/ssl", config.AppConfig.Server.ConfigFolder)
|
||||||
|
if err := os.MkdirAll(sslDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create SSL directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Загружаем или генерируем SSL сертификат
|
||||||
|
cert, err := ssl.LoadOrGenerateCert(host)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load/generate SSL certificate: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Настраиваем TLS
|
||||||
|
tlsConfig := &tls.Config{
|
||||||
|
Certificates: []tls.Certificate{*cert},
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
MaxVersion: tls.VersionTLS13,
|
||||||
|
// Отключаем проверку клиентских сертификатов
|
||||||
|
ClientAuth: tls.NoClientCert,
|
||||||
|
// Добавляем логирование для отладки
|
||||||
|
GetCertificate: func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||||
|
if config.AppConfig.MainFlags.Debug {
|
||||||
|
fmt.Printf("🔍 TLS запрос от %s (SNI: %s)\n", clientHello.Conn.RemoteAddr(), clientHello.ServerName)
|
||||||
|
}
|
||||||
|
return cert, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем HTTPS сервер
|
||||||
|
server := &http.Server{
|
||||||
|
Addr: addr,
|
||||||
|
TLSConfig: tlsConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("🔒 Сервер запущен на https://%s (SSL включен)\n", addr)
|
||||||
|
fmt.Println("Нажмите Ctrl+C для остановки")
|
||||||
|
|
||||||
|
// Тестовое логирование для проверки debug флага
|
||||||
|
if config.AppConfig.MainFlags.Debug {
|
||||||
|
fmt.Printf("🔍 DEBUG РЕЖИМ ВКЛЮЧЕН - веб-операции будут логироваться\n")
|
||||||
|
} else {
|
||||||
|
fmt.Printf("🔍 DEBUG РЕЖИМ ОТКЛЮЧЕН - веб-операции не будут логироваться\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return server.ListenAndServeTLS("", "")
|
||||||
|
} else {
|
||||||
|
// Регистрируем обычные маршруты для HTTP
|
||||||
|
registerRoutes()
|
||||||
|
|
||||||
|
fmt.Printf("🌐 Сервер запущен на http://%s (HTTP режим)\n", addr)
|
||||||
|
fmt.Println("Нажмите Ctrl+C для остановки")
|
||||||
|
|
||||||
|
// Тестовое логирование для проверки debug флага
|
||||||
|
if config.AppConfig.MainFlags.Debug {
|
||||||
|
fmt.Printf("🔍 DEBUG РЕЖИМ ВКЛЮЧЕН - веб-операции будут логироваться\n")
|
||||||
|
} else {
|
||||||
|
fmt.Printf("🔍 DEBUG РЕЖИМ ОТКЛЮЧЕН - веб-операции не будут логироваться\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.ListenAndServe(addr, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleHTTPSRedirect обрабатывает редирект с HTTP на HTTPS
|
||||||
|
func handleHTTPSRedirect(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Определяем протокол и хост
|
||||||
|
host := r.Host
|
||||||
|
if host == "" {
|
||||||
|
host = r.Header.Get("Host")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Редиректим на HTTPS
|
||||||
|
httpsURL := fmt.Sprintf("https://%s%s", host, r.RequestURI)
|
||||||
|
http.Redirect(w, r, httpsURL, http.StatusMovedPermanently)
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerHTTPSRoutes регистрирует маршруты для HTTPS сервера
|
||||||
|
func registerHTTPSRoutes() {
|
||||||
|
// Регистрируем все маршруты кроме главной страницы
|
||||||
|
registerRoutesExceptHome()
|
||||||
|
|
||||||
|
// Регистрируем главную страницу (строго по BasePath) с проверкой HTTPS
|
||||||
|
http.HandleFunc(makePath("/"), func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Проверяем, пришел ли запрос по HTTP (не HTTPS)
|
||||||
|
if r.TLS == nil {
|
||||||
|
handleHTTPSRedirect(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Обрабатываем только точные пути: 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
|
||||||
|
basePath := config.AppConfig.Server.BasePath
|
||||||
|
if basePath != "" && basePath != "/" {
|
||||||
|
basePath = strings.TrimSuffix(basePath, "/")
|
||||||
|
http.HandleFunc(basePath, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Проверяем, пришел ли запрос по HTTP (не HTTPS)
|
||||||
|
if r.TLS == nil {
|
||||||
|
handleHTTPSRedirect(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Если уже HTTPS, обрабатываем как обычно
|
||||||
|
AuthMiddleware(handleResultsPage)(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Catch-all 404 для любых незарегистрированных путей (только когда BasePath задан)
|
||||||
|
if getBasePath() != "" {
|
||||||
|
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
renderNotFound(w, "Страница не найдена", getBasePath())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerRoutesExceptHome регистрирует все маршруты кроме главной страницы
|
||||||
|
func registerRoutesExceptHome() {
|
||||||
|
// Страница входа (без аутентификации)
|
||||||
|
http.HandleFunc(makePath("/login"), handleLoginPage)
|
||||||
|
|
||||||
|
// API для аутентификации (без аутентификации)
|
||||||
|
http.HandleFunc(makePath("/api/login"), handleLogin)
|
||||||
|
http.HandleFunc(makePath("/api/logout"), handleLogout)
|
||||||
|
http.HandleFunc(makePath("/api/validate-token"), handleValidateToken)
|
||||||
|
|
||||||
|
// Файлы
|
||||||
|
http.HandleFunc(makePath("/file/"), AuthMiddleware(handleFileView))
|
||||||
|
http.HandleFunc(makePath("/delete/"), AuthMiddleware(handleDeleteFile))
|
||||||
|
|
||||||
|
// История запросов
|
||||||
|
http.HandleFunc(makePath("/history"), AuthMiddleware(handleHistoryPage))
|
||||||
|
http.HandleFunc(makePath("/history/view/"), AuthMiddleware(handleHistoryView))
|
||||||
|
http.HandleFunc(makePath("/history/delete/"), AuthMiddleware(handleDeleteHistoryEntry))
|
||||||
|
http.HandleFunc(makePath("/history/clear"), AuthMiddleware(handleClearHistory))
|
||||||
|
|
||||||
|
// Управление промптами
|
||||||
|
http.HandleFunc(makePath("/prompts"), AuthMiddleware(handlePromptsPage))
|
||||||
|
http.HandleFunc(makePath("/prompts/add"), AuthMiddleware(handleAddPrompt))
|
||||||
|
http.HandleFunc(makePath("/prompts/edit/"), AuthMiddleware(handleEditPrompt))
|
||||||
|
http.HandleFunc(makePath("/prompts/edit-verbose/"), AuthMiddleware(handleEditVerbosePrompt))
|
||||||
|
http.HandleFunc(makePath("/prompts/delete/"), AuthMiddleware(handleDeletePrompt))
|
||||||
|
http.HandleFunc(makePath("/prompts/restore/"), AuthMiddleware(handleRestorePrompt))
|
||||||
|
http.HandleFunc(makePath("/prompts/restore-verbose/"), AuthMiddleware(handleRestoreVerbosePrompt))
|
||||||
|
http.HandleFunc(makePath("/prompts/save-lang"), AuthMiddleware(handleSaveLang))
|
||||||
|
|
||||||
|
// Веб-страница для выполнения запросов
|
||||||
|
http.HandleFunc(makePath("/run"), AuthMiddleware(CSRFMiddleware(handleExecutePage)))
|
||||||
|
|
||||||
|
// API для выполнения запросов
|
||||||
|
http.HandleFunc(makePath("/api/execute"), AuthMiddleware(CSRFMiddleware(handleExecute)))
|
||||||
|
// 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 регистрирует все маршруты сервера
|
// registerRoutes регистрирует все маршруты сервера
|
||||||
func registerRoutes() {
|
func registerRoutes() {
|
||||||
// Главная страница и файлы
|
// Страница входа (без аутентификации)
|
||||||
http.HandleFunc("/", handleResultsPage)
|
http.HandleFunc(makePath("/login"), handleLoginPage)
|
||||||
http.HandleFunc("/file/", handleFileView)
|
|
||||||
http.HandleFunc("/delete/", handleDeleteFile)
|
// API для аутентификации (без аутентификации)
|
||||||
|
http.HandleFunc(makePath("/api/login"), handleLogin)
|
||||||
|
http.HandleFunc(makePath("/api/logout"), handleLogout)
|
||||||
|
http.HandleFunc(makePath("/api/validate-token"), handleValidateToken)
|
||||||
|
|
||||||
|
// Главная страница (строго по 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))
|
||||||
|
|
||||||
// История запросов
|
// История запросов
|
||||||
http.HandleFunc("/history", handleHistoryPage)
|
http.HandleFunc(makePath("/history"), AuthMiddleware(handleHistoryPage))
|
||||||
http.HandleFunc("/history/view/", handleHistoryView)
|
http.HandleFunc(makePath("/history/view/"), AuthMiddleware(handleHistoryView))
|
||||||
http.HandleFunc("/history/delete/", handleDeleteHistoryEntry)
|
http.HandleFunc(makePath("/history/delete/"), AuthMiddleware(handleDeleteHistoryEntry))
|
||||||
http.HandleFunc("/history/clear", handleClearHistory)
|
http.HandleFunc(makePath("/history/clear"), AuthMiddleware(handleClearHistory))
|
||||||
|
|
||||||
// Управление промптами
|
// Управление промптами
|
||||||
http.HandleFunc("/prompts", handlePromptsPage)
|
http.HandleFunc(makePath("/prompts"), AuthMiddleware(handlePromptsPage))
|
||||||
http.HandleFunc("/prompts/add", handleAddPrompt)
|
http.HandleFunc(makePath("/prompts/add"), AuthMiddleware(handleAddPrompt))
|
||||||
http.HandleFunc("/prompts/edit/", handleEditPrompt)
|
http.HandleFunc(makePath("/prompts/edit/"), AuthMiddleware(handleEditPrompt))
|
||||||
http.HandleFunc("/prompts/delete/", handleDeletePrompt)
|
http.HandleFunc(makePath("/prompts/edit-verbose/"), AuthMiddleware(handleEditVerbosePrompt))
|
||||||
http.HandleFunc("/prompts/restore/", handleRestorePrompt)
|
http.HandleFunc(makePath("/prompts/delete/"), AuthMiddleware(handleDeletePrompt))
|
||||||
http.HandleFunc("/prompts/restore-verbose/", handleRestoreVerbosePrompt)
|
http.HandleFunc(makePath("/prompts/restore/"), AuthMiddleware(handleRestorePrompt))
|
||||||
http.HandleFunc("/prompts/save-lang", handleSaveLang)
|
http.HandleFunc(makePath("/prompts/restore-verbose/"), AuthMiddleware(handleRestoreVerbosePrompt))
|
||||||
|
http.HandleFunc(makePath("/prompts/save-lang"), AuthMiddleware(handleSaveLang))
|
||||||
|
|
||||||
// Веб-страница для выполнения запросов
|
// Веб-страница для выполнения запросов
|
||||||
http.HandleFunc("/run", handleExecutePage)
|
http.HandleFunc(makePath("/run"), AuthMiddleware(CSRFMiddleware(handleExecutePage)))
|
||||||
|
|
||||||
// API для выполнения запросов
|
// API для выполнения запросов
|
||||||
http.HandleFunc("/api/execute", handleExecute)
|
http.HandleFunc(makePath("/api/execute"), AuthMiddleware(CSRFMiddleware(handleExecute)))
|
||||||
// API для сохранения результатов и истории
|
// API для сохранения результатов и истории
|
||||||
http.HandleFunc("/api/save-result", handleSaveResult)
|
http.HandleFunc(makePath("/api/save-result"), AuthMiddleware(CSRFMiddleware(handleSaveResult)))
|
||||||
http.HandleFunc("/api/add-to-history", handleAddToHistory)
|
http.HandleFunc(makePath("/api/add-to-history"), AuthMiddleware(CSRFMiddleware(handleAddToHistory)))
|
||||||
|
|
||||||
|
// Регистрируем главную страницу без слэша в конце для BasePath
|
||||||
|
basePath := config.AppConfig.Server.BasePath
|
||||||
|
if basePath != "" && basePath != "/" {
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,13 +72,22 @@ var ExecutePageCSSTemplate = template.Must(template.New("execute_css").Parse(`
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
.header h1 {
|
.header h1 {
|
||||||
|
margin: 0;
|
||||||
font-size: 2.5em;
|
font-size: 2.5em;
|
||||||
margin-bottom: 10px;
|
font-weight: 300;
|
||||||
}
|
}
|
||||||
.header p {
|
.header p {
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.config-info {
|
||||||
|
margin: 5px 0 0 0 !important;
|
||||||
|
opacity: 0.7 !important;
|
||||||
|
font-size: 0.9em !important;
|
||||||
|
font-style: italic;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
.content {
|
.content {
|
||||||
padding: 30px;
|
padding: 30px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ var ExecutePageTemplate = template.Must(template.New("execute").Parse(`<!DOCTYPE
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>{{.Title}} - Linux Command GPT</title>
|
<title>{{.Title}} - {{.AppName}}</title>
|
||||||
<style>
|
<style>
|
||||||
{{template "execute_css" .}}
|
{{template "execute_css" .}}
|
||||||
</style>
|
</style>
|
||||||
@@ -17,16 +17,19 @@ var ExecutePageTemplate = template.Must(template.New("execute").Parse(`<!DOCTYPE
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h1>{{.Header}}</h1>
|
<h1>{{.Header}}</h1>
|
||||||
<p>Выполнение запросов к Linux Command GPT через веб-интерфейс</p>
|
<p>Выполнение запросов к {{.AppName}} через веб-интерфейс</p>
|
||||||
|
<p class="config-info">({{.ProviderType}} • {{.Model}} • {{.Host}})</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="nav-buttons">
|
<div class="nav-buttons">
|
||||||
<a href="/" class="nav-btn">🏠 Главная</a>
|
<a href="{{.BasePath}}/" class="nav-btn">🏠 Главная</a>
|
||||||
<a href="/history" class="nav-btn">📝 История</a>
|
<a href="{{.BasePath}}/history" class="nav-btn">📝 История</a>
|
||||||
<a href="/prompts" class="nav-btn">⚙️ Промпты</a>
|
<a href="{{.BasePath}}/prompts" class="nav-btn">⚙️ Промпты</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form method="POST" id="executeForm">
|
<form method="POST" id="executeForm">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
||||||
|
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="system_id">🤖 Системный промпт:</label>
|
<label for="system_id">🤖 Системный промпт:</label>
|
||||||
|
|||||||
@@ -11,6 +11,15 @@ var ExecutePageScriptsTemplate = template.Must(template.New("execute_scripts").P
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Валидация длины полей
|
||||||
|
const prompt = document.getElementById('prompt').value;
|
||||||
|
const maxUserMessageLength = {{.MaxUserMessageLength}};
|
||||||
|
if (prompt.length > maxUserMessageLength) {
|
||||||
|
alert('Пользовательское сообщение слишком длинное: максимум ' + maxUserMessageLength + ' символов');
|
||||||
|
e.preventDefault();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
this.dataset.submitting = 'true';
|
this.dataset.submitting = 'true';
|
||||||
const submitBtn = document.getElementById('submitBtn');
|
const submitBtn = document.getElementById('submitBtn');
|
||||||
const loading = document.getElementById('loading');
|
const loading = document.getElementById('loading');
|
||||||
@@ -80,6 +89,7 @@ var ExecutePageScriptsTemplate = template.Must(template.New("execute_scripts").P
|
|||||||
function saveResult() {
|
function saveResult() {
|
||||||
const resultDataField = document.getElementById('resultData');
|
const resultDataField = document.getElementById('resultData');
|
||||||
const prompt = document.getElementById('prompt').value;
|
const prompt = document.getElementById('prompt').value;
|
||||||
|
const csrfToken = document.querySelector('input[name="csrf_token"]').value;
|
||||||
|
|
||||||
if (!resultDataField.value || !prompt.trim()) {
|
if (!resultDataField.value || !prompt.trim()) {
|
||||||
alert('Нет данных для сохранения');
|
alert('Нет данных для сохранения');
|
||||||
@@ -95,10 +105,11 @@ var ExecutePageScriptsTemplate = template.Must(template.New("execute_scripts").P
|
|||||||
model: resultData.model || 'Unknown'
|
model: resultData.model || 'Unknown'
|
||||||
};
|
};
|
||||||
|
|
||||||
fetch('/api/save-result', {
|
fetch('{{.BasePath}}/api/save-result', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-Token': csrfToken,
|
||||||
},
|
},
|
||||||
body: JSON.stringify(requestData)
|
body: JSON.stringify(requestData)
|
||||||
})
|
})
|
||||||
@@ -125,6 +136,7 @@ var ExecutePageScriptsTemplate = template.Must(template.New("execute_scripts").P
|
|||||||
const resultDataField = document.getElementById('resultData');
|
const resultDataField = document.getElementById('resultData');
|
||||||
const prompt = document.getElementById('prompt').value;
|
const prompt = document.getElementById('prompt').value;
|
||||||
const systemId = document.getElementById('system_id').value;
|
const systemId = document.getElementById('system_id').value;
|
||||||
|
const csrfToken = document.querySelector('input[name="csrf_token"]').value;
|
||||||
|
|
||||||
if (!resultDataField.value || !prompt.trim()) {
|
if (!resultDataField.value || !prompt.trim()) {
|
||||||
alert('Нет данных для сохранения в историю');
|
alert('Нет данных для сохранения в историю');
|
||||||
@@ -143,10 +155,11 @@ var ExecutePageScriptsTemplate = template.Must(template.New("execute_scripts").P
|
|||||||
system: systemName
|
system: systemName
|
||||||
};
|
};
|
||||||
|
|
||||||
fetch('/api/add-to-history', {
|
fetch('{{.BasePath}}/api/add-to-history', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-Token': csrfToken,
|
||||||
},
|
},
|
||||||
body: JSON.stringify(requestData)
|
body: JSON.stringify(requestData)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ const FileViewTemplate = `
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>%s - LCG Results</title>
|
<title>{{.Filename}} - LCG Results</title>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
background: linear-gradient(135deg, #56ab2f 0%%, #a8e6cf 100%%);
|
background: linear-gradient(135deg, #56ab2f 0%, #a8e6cf 100%);
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
.container {
|
.container {
|
||||||
@@ -25,7 +25,7 @@ const FileViewTemplate = `
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.header {
|
.header {
|
||||||
background: linear-gradient(135deg, #2d5016 0%%, #4a7c59 100%%);
|
background: linear-gradient(135deg, #2d5016 0%, #4a7c59 100%);
|
||||||
color: white;
|
color: white;
|
||||||
padding: 20px 30px;
|
padding: 20px 30px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -125,11 +125,11 @@ const FileViewTemplate = `
|
|||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h1>📄 %s</h1>
|
<h1>📄 {{.Filename}}</h1>
|
||||||
<a href="/" class="back-btn">← Назад к списку</a>
|
<a href="{{.BasePath}}/" class="back-btn">← Назад к списку</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
%s
|
{{.Content}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -111,19 +111,26 @@ const HistoryPageTemplate = `
|
|||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
color: #2d5016;
|
color: #2d5016;
|
||||||
border-left: 3px solid #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 {
|
.delete-btn {
|
||||||
background: #e74c3c;
|
background: transparent;
|
||||||
color: white;
|
color: #ef9a9a; /* бледно-красный */
|
||||||
border: none;
|
border: none;
|
||||||
padding: 6px 12px;
|
padding: 2px 6px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 0.8em;
|
font-size: 18px;
|
||||||
transition: background 0.3s ease;
|
line-height: 1;
|
||||||
|
transition: color 0.2s ease, transform 0.2s ease;
|
||||||
}
|
}
|
||||||
.delete-btn:hover {
|
.delete-btn:hover {
|
||||||
background: #c0392b;
|
color: rgb(171, 27, 24); /* ярче при ховере */
|
||||||
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
.empty-state {
|
.empty-state {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -143,6 +150,7 @@ const HistoryPageTemplate = `
|
|||||||
.history-header { flex-direction: column; align-items: flex-start; gap: 8px; }
|
.history-header { flex-direction: column; align-items: flex-start; gap: 8px; }
|
||||||
.history-item { padding: 15px; }
|
.history-item { padding: 15px; }
|
||||||
.history-response { font-size: 0.85em; }
|
.history-response { font-size: 0.85em; }
|
||||||
|
.search-container input { font-size: 16px; width: 96% !important; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
@@ -154,13 +162,13 @@ const HistoryPageTemplate = `
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h1>📝 История запросов</h1>
|
<h1>📝 История запросов</h1>
|
||||||
<p>Управление историей запросов Linux Command GPT</p>
|
<p>Управление историей запросов {{.AppName}}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="nav-buttons">
|
<div class="nav-buttons">
|
||||||
<a href="/" class="nav-btn">🏠 Главная</a>
|
<a href="{{.BasePath}}/" class="nav-btn">🏠 Главная</a>
|
||||||
<a href="/run" class="nav-btn">🚀 Выполнение</a>
|
<a href="{{.BasePath}}/run" class="nav-btn">🚀 Выполнение</a>
|
||||||
<a href="/prompts" class="nav-btn">⚙️ Промпты</a>
|
<a href="{{.BasePath}}/prompts" class="nav-btn">⚙️ Промпты</a>
|
||||||
<button class="nav-btn clear-btn" onclick="clearHistory()">🗑️ Очистить всю историю</button>
|
<button class="nav-btn clear-btn" onclick="clearHistory()">🗑️ Очистить всю историю</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -179,7 +187,7 @@ const HistoryPageTemplate = `
|
|||||||
<span class="history-index">#{{.Index}}</span>
|
<span class="history-index">#{{.Index}}</span>
|
||||||
<span class="history-timestamp">{{.Timestamp}}</span>
|
<span class="history-timestamp">{{.Timestamp}}</span>
|
||||||
</div>
|
</div>
|
||||||
<button class="delete-btn" onclick="event.stopPropagation(); deleteHistoryEntry({{.Index}})">🗑️ Удалить</button>
|
<button class="delete-btn" onclick="event.stopPropagation(); deleteHistoryEntry({{.Index}})">✖</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="history-command">{{.Command}}</div>
|
<div class="history-command">{{.Command}}</div>
|
||||||
<div class="history-response">{{.Response}}</div>
|
<div class="history-response">{{.Response}}</div>
|
||||||
@@ -196,12 +204,12 @@ const HistoryPageTemplate = `
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
function viewHistoryEntry(index) {
|
function viewHistoryEntry(index) {
|
||||||
window.location.href = '/history/view/' + index;
|
window.location.href = '{{.BasePath}}/history/view/' + index;
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteHistoryEntry(index) {
|
function deleteHistoryEntry(index) {
|
||||||
if (confirm('Вы уверены, что хотите удалить запись #' + index + '?')) {
|
if (confirm('Вы уверены, что хотите удалить запись #' + index + '?')) {
|
||||||
fetch('/history/delete/' + index, {
|
fetch('{{.BasePath}}/history/delete/' + index, {
|
||||||
method: 'DELETE'
|
method: 'DELETE'
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
@@ -220,7 +228,7 @@ const HistoryPageTemplate = `
|
|||||||
|
|
||||||
function clearHistory() {
|
function clearHistory() {
|
||||||
if (confirm('Вы уверены, что хотите очистить всю историю?\\n\\nЭто действие нельзя отменить.')) {
|
if (confirm('Вы уверены, что хотите очистить всю историю?\\n\\nЭто действие нельзя отменить.')) {
|
||||||
fetch('/history/clear', {
|
fetch('{{.BasePath}}/history/clear', {
|
||||||
method: 'DELETE'
|
method: 'DELETE'
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ const HistoryViewTemplate = `
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Запись #%d - LCG History</title>
|
<title>Запись #{{.Index}} - LCG History</title>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
background: linear-gradient(135deg, #56ab2f 0%%, #a8e6cf 100%%);
|
background: linear-gradient(135deg, #56ab2f 0%, #a8e6cf 100%);
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
.container {
|
.container {
|
||||||
@@ -25,7 +25,7 @@ const HistoryViewTemplate = `
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.header {
|
.header {
|
||||||
background: linear-gradient(135deg, #2d5016 0%%, #4a7c59 100%%);
|
background: linear-gradient(135deg, #2d5016 0%, #4a7c59 100%);
|
||||||
color: white;
|
color: white;
|
||||||
padding: 20px 30px;
|
padding: 20px 30px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -212,7 +212,7 @@ const HistoryViewTemplate = `
|
|||||||
.back-btn { padding: 6px 12px; font-size: 0.9em; }
|
.back-btn { padding: 6px 12px; font-size: 0.9em; }
|
||||||
.content { padding: 20px; }
|
.content { padding: 20px; }
|
||||||
.actions { flex-direction: column; }
|
.actions { flex-direction: column; }
|
||||||
.action-btn { width: 100%; text-align: center; }
|
.action-btn { text-align: center; }
|
||||||
.history-response-content { font-size: 0.9em; }
|
.history-response-content { font-size: 0.9em; }
|
||||||
}
|
}
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
@@ -223,34 +223,34 @@ const HistoryViewTemplate = `
|
|||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h1>📝 Запись #%d</h1>
|
<h1>📝 Запись #{{.Index}}</h1>
|
||||||
<a href="/history" class="back-btn">← Назад к истории</a>
|
<a href="{{.BasePath}}/history" class="back-btn">← Назад к истории</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="history-meta">
|
<div class="history-meta">
|
||||||
<div class="history-meta-item">
|
<div class="history-meta-item">
|
||||||
<span class="history-meta-label">📅 Время:</span> %s
|
<span class="history-meta-label">📅 Время:</span> {{.Timestamp}}
|
||||||
</div>
|
</div>
|
||||||
<div class="history-meta-item">
|
<div class="history-meta-item">
|
||||||
<span class="history-meta-label">🔢 Индекс:</span> #%d
|
<span class="history-meta-label">🔢 Индекс:</span> #{{.Index}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="history-command">
|
<div class="history-command">
|
||||||
<h3>💬 Запрос пользователя:</h3>
|
<h3>💬 Запрос пользователя:</h3>
|
||||||
<div class="history-command-text">%s</div>
|
<div class="history-command-text">{{.Command}}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="history-response">
|
<div class="history-response">
|
||||||
<h3>🤖 Ответ Модели:</h3>
|
<h3>🤖 Ответ Модели:</h3>
|
||||||
<div class="history-response-content">%s</div>
|
<div class="history-response-content">{{.Response}}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
%s
|
{{.ExplanationHTML}}
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<a href="/history" class="action-btn">📝 К истории</a>
|
<a href="{{.BasePath}}/history" class="action-btn">📝 К истории</a>
|
||||||
<button class="action-btn delete-btn" onclick="deleteHistoryEntry(%d)">🗑️ Удалить запись</button>
|
<button class="action-btn delete-btn" onclick="deleteHistoryEntry({{.Index}})">🗑️ Удалить запись</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -258,12 +258,12 @@ const HistoryViewTemplate = `
|
|||||||
<script>
|
<script>
|
||||||
function deleteHistoryEntry(index) {
|
function deleteHistoryEntry(index) {
|
||||||
if (confirm('Вы уверены, что хотите удалить запись #' + index + '?')) {
|
if (confirm('Вы уверены, что хотите удалить запись #' + index + '?')) {
|
||||||
fetch('/history/delete/' + index, {
|
fetch('{{.BasePath}}/history/delete/' + index, {
|
||||||
method: 'DELETE'
|
method: 'DELETE'
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
window.location.href = '/history';
|
window.location.href = '{{.BasePath}}/history';
|
||||||
} else {
|
} else {
|
||||||
alert('Ошибка при удалении записи');
|
alert('Ошибка при удалении записи');
|
||||||
}
|
}
|
||||||
|
|||||||
323
serve/templates/login.go
Normal file
323
serve/templates/login.go
Normal file
@@ -0,0 +1,323 @@
|
|||||||
|
package templates
|
||||||
|
|
||||||
|
// LoginPageTemplate шаблон страницы авторизации
|
||||||
|
const LoginPageTemplate = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{{.Title}}</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: linear-gradient(-45deg, #667eea, #764ba2, #f093fb, #f5576c, #4facfe, #00f2fe);
|
||||||
|
background-size: 400% 400%;
|
||||||
|
animation: gradientShift 15s ease infinite;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes gradientShift {
|
||||||
|
0% { background-position: 0% 50%; }
|
||||||
|
50% { background-position: 100% 50%; }
|
||||||
|
100% { background-position: 0% 50%; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Плавающие элементы */
|
||||||
|
.floating-elements {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-element {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0.1;
|
||||||
|
animation: float 20s infinite linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-element:nth-child(1) { left: 10%; animation-delay: 0s; animation-duration: 25s; }
|
||||||
|
.floating-element:nth-child(2) { left: 20%; animation-delay: 2s; animation-duration: 30s; }
|
||||||
|
.floating-element:nth-child(3) { left: 30%; animation-delay: 4s; animation-duration: 20s; }
|
||||||
|
.floating-element:nth-child(4) { left: 40%; animation-delay: 6s; animation-duration: 35s; }
|
||||||
|
.floating-element:nth-child(5) { left: 50%; animation-delay: 8s; animation-duration: 28s; }
|
||||||
|
.floating-element:nth-child(6) { left: 60%; animation-delay: 10s; animation-duration: 22s; }
|
||||||
|
.floating-element:nth-child(7) { left: 70%; animation-delay: 12s; animation-duration: 32s; }
|
||||||
|
.floating-element:nth-child(8) { left: 80%; animation-delay: 14s; animation-duration: 26s; }
|
||||||
|
.floating-element:nth-child(9) { left: 90%; animation-delay: 16s; animation-duration: 24s; }
|
||||||
|
|
||||||
|
@keyframes float {
|
||||||
|
0% { transform: translateY(100vh) rotate(0deg); opacity: 0; }
|
||||||
|
10% { opacity: 0.1; }
|
||||||
|
90% { opacity: 0.1; }
|
||||||
|
100% { transform: translateY(-100px) rotate(360deg); opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.lock-icon {
|
||||||
|
font-size: 2rem;
|
||||||
|
color: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-icon {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shield-icon {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
color: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.star-icon {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-container {
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.2);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header h1 {
|
||||||
|
color: #333;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header p {
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 2px solid #e1e5e9;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.2);
|
||||||
|
background: rgba(255, 255, 255, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||||
|
transition: left 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 10px 25px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button:hover::before {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button:active {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.success {
|
||||||
|
background-color: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.error {
|
||||||
|
background-color: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
display: none;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
border: 2px solid #f3f3f3;
|
||||||
|
border-top: 2px solid #667eea;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Плавающие элементы фона -->
|
||||||
|
<div class="floating-elements">
|
||||||
|
<div class="floating-element lock-icon">🔒</div>
|
||||||
|
<div class="floating-element key-icon">🔑</div>
|
||||||
|
<div class="floating-element shield-icon">🛡️</div>
|
||||||
|
<div class="floating-element star-icon">⭐</div>
|
||||||
|
<div class="floating-element lock-icon">🔐</div>
|
||||||
|
<div class="floating-element key-icon">🗝️</div>
|
||||||
|
<div class="floating-element shield-icon">🔒</div>
|
||||||
|
<div class="floating-element star-icon">✨</div>
|
||||||
|
<div class="floating-element lock-icon">🔒</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="login-container">
|
||||||
|
<div class="login-header">
|
||||||
|
<h1>🔐 Авторизация</h1>
|
||||||
|
<p>Войдите в систему для доступа к LCG</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="loginForm">
|
||||||
|
<input type="hidden" id="csrf_token" name="csrf_token" value="{{.CSRFToken}}">
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username">Имя пользователя:</label>
|
||||||
|
<input type="text" id="username" name="username" required placeholder="Введите имя пользователя">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Пароль:</label>
|
||||||
|
<input type="password" id="password" name="password" required placeholder="Введите пароль">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="login-button">Войти</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="loading" id="loading">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<p>Проверка авторизации...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="message"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.getElementById('loginForm').addEventListener('submit', async function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const form = e.target;
|
||||||
|
const formData = new FormData(form);
|
||||||
|
const username = formData.get('username');
|
||||||
|
const password = formData.get('password');
|
||||||
|
|
||||||
|
// Показываем загрузку
|
||||||
|
document.getElementById('loading').style.display = 'block';
|
||||||
|
document.getElementById('message').innerHTML = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const csrfToken = document.getElementById('csrf_token').value;
|
||||||
|
const response = await fetch('{{.BasePath}}/api/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-Token': csrfToken
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
csrf_token: csrfToken
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
// Успешная авторизация, перенаправляем на главную страницу
|
||||||
|
window.location.href = '{{.BasePath}}/';
|
||||||
|
} else {
|
||||||
|
// Ошибка авторизации
|
||||||
|
showMessage(data.error || 'Ошибка авторизации', 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showMessage('Ошибка соединения с сервером', 'error');
|
||||||
|
} finally {
|
||||||
|
document.getElementById('loading').style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function showMessage(text, type) {
|
||||||
|
const messageDiv = document.getElementById('message');
|
||||||
|
messageDiv.innerHTML = '<div class="message ' + type + '">' + text + '</div>';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
157
serve/templates/not_found.go
Normal file
157
serve/templates/not_found.go
Normal 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>
|
||||||
|
`
|
||||||
@@ -235,13 +235,13 @@ const PromptsPageTemplate = `
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h1>⚙️ Системные промпты</h1>
|
<h1>⚙️ Системные промпты</h1>
|
||||||
<p>Управление системными промптами Linux Command GPT</p>
|
<p>Управление системными промптами {{.AppName}}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="nav-buttons">
|
<div class="nav-buttons">
|
||||||
<a href="/" class="nav-btn">🏠 Главная</a>
|
<a href="{{.BasePath}}/" class="nav-btn">🏠 Главная</a>
|
||||||
<a href="/run" class="nav-btn">🚀 Выполнение</a>
|
<a href="{{.BasePath}}/run" class="nav-btn">🚀 Выполнение</a>
|
||||||
<a href="/history" class="nav-btn">📝 История</a>
|
<a href="{{.BasePath}}/history" class="nav-btn">📝 История</a>
|
||||||
<button class="nav-btn add-btn" onclick="showAddForm()">➕ Добавить промпт</button>
|
<button class="nav-btn add-btn" onclick="showAddForm()">➕ Добавить промпт</button>
|
||||||
<div class="lang-switcher">
|
<div class="lang-switcher">
|
||||||
<button class="lang-btn {{if eq .Lang "ru"}}active{{end}}" onclick="switchLang('ru')">🇷🇺 RU</button>
|
<button class="lang-btn {{if eq .Lang "ru"}}active{{end}}" onclick="switchLang('ru')">🇷🇺 RU</button>
|
||||||
@@ -391,7 +391,7 @@ const PromptsPageTemplate = `
|
|||||||
|
|
||||||
function saveCurrentPrompts(lang) {
|
function saveCurrentPrompts(lang) {
|
||||||
// Отправляем запрос для сохранения текущих промптов с новым языком
|
// Отправляем запрос для сохранения текущих промптов с новым языком
|
||||||
fetch('/prompts/save-lang', {
|
fetch('{{.BasePath}}/prompts/save-lang', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -407,12 +407,17 @@ const PromptsPageTemplate = `
|
|||||||
|
|
||||||
function editVerbosePrompt(mode, content) {
|
function editVerbosePrompt(mode, content) {
|
||||||
// Редактирование промпта подробности
|
// Редактирование промпта подробности
|
||||||
alert('Редактирование промптов подробности будет реализовано');
|
document.getElementById('formTitle').textContent = 'Редактировать промпт подробности (' + mode + ')';
|
||||||
|
document.getElementById('promptId').value = mode;
|
||||||
|
document.getElementById('promptName').value = mode;
|
||||||
|
document.getElementById('promptDescription').value = 'Промпт для режима ' + mode;
|
||||||
|
document.getElementById('promptContent').value = content;
|
||||||
|
document.getElementById('promptForm').style.display = 'block';
|
||||||
}
|
}
|
||||||
|
|
||||||
function deletePrompt(id) {
|
function deletePrompt(id) {
|
||||||
if (confirm('Вы уверены, что хотите удалить промпт #' + id + '?')) {
|
if (confirm('Вы уверены, что хотите удалить промпт #' + id + '?')) {
|
||||||
fetch('/prompts/delete/' + id, {
|
fetch('{{.BasePath}}/prompts/delete/' + id, {
|
||||||
method: 'DELETE'
|
method: 'DELETE'
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
@@ -432,10 +437,42 @@ const PromptsPageTemplate = `
|
|||||||
document.getElementById('promptFormData').addEventListener('submit', function(e) {
|
document.getElementById('promptFormData').addEventListener('submit', function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Валидация длины полей
|
||||||
|
const name = document.getElementById('promptName').value;
|
||||||
|
const description = document.getElementById('promptDescription').value;
|
||||||
|
const content = document.getElementById('promptContent').value;
|
||||||
|
|
||||||
|
const maxContentLength = {{.MaxSystemPromptLength}};
|
||||||
|
const maxNameLength = {{.MaxPromptNameLength}};
|
||||||
|
const maxDescLength = {{.MaxPromptDescLength}};
|
||||||
|
|
||||||
|
if (content.length > maxContentLength) {
|
||||||
|
alert('Содержимое промпта слишком длинное: максимум ' + maxContentLength + ' символов');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (name.length > maxNameLength) {
|
||||||
|
alert('Название промпта слишком длинное: максимум ' + maxNameLength + ' символов');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (description.length > maxDescLength) {
|
||||||
|
alert('Описание промпта слишком длинное: максимум ' + maxDescLength + ' символов');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const formData = new FormData(this);
|
const formData = new FormData(this);
|
||||||
const id = formData.get('id');
|
const id = formData.get('id');
|
||||||
const url = id ? '/prompts/edit/' + id : '/prompts/add';
|
|
||||||
const method = id ? 'PUT' : 'POST';
|
// Определяем, это системный промпт или промпт подробности
|
||||||
|
const isVerbosePrompt = ['v', 'vv', 'vvv'].includes(id);
|
||||||
|
|
||||||
|
let url, method;
|
||||||
|
if (isVerbosePrompt) {
|
||||||
|
url = '{{.BasePath}}/prompts/edit-verbose/' + id;
|
||||||
|
method = 'PUT';
|
||||||
|
} else {
|
||||||
|
url = id ? '{{.BasePath}}/prompts/edit/' + id : '{{.BasePath}}/prompts/add';
|
||||||
|
method = id ? 'PUT' : 'POST';
|
||||||
|
}
|
||||||
|
|
||||||
fetch(url, {
|
fetch(url, {
|
||||||
method: method,
|
method: method,
|
||||||
@@ -464,7 +501,7 @@ const PromptsPageTemplate = `
|
|||||||
// Функция восстановления системного промпта
|
// Функция восстановления системного промпта
|
||||||
function restorePrompt(id) {
|
function restorePrompt(id) {
|
||||||
if (confirm('Восстановить промпт к значению по умолчанию?')) {
|
if (confirm('Восстановить промпт к значению по умолчанию?')) {
|
||||||
fetch('/prompts/restore/' + id, {
|
fetch('{{.BasePath}}/prompts/restore/' + id, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -489,7 +526,7 @@ const PromptsPageTemplate = `
|
|||||||
// Функция восстановления verbose промпта
|
// Функция восстановления verbose промпта
|
||||||
function restoreVerbosePrompt(mode) {
|
function restoreVerbosePrompt(mode) {
|
||||||
if (confirm('Восстановить промпт к значению по умолчанию?')) {
|
if (confirm('Восстановить промпт к значению по умолчанию?')) {
|
||||||
fetch('/prompts/restore-verbose/' + mode, {
|
fetch('{{.BasePath}}/prompts/restore-verbose/' + mode, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const ResultsPageTemplate = `
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>LCG Results - Linux Command GPT</title>
|
<title>{{.AppAbbreviation}} Результаты - {{.AppName}}</title>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
@@ -73,8 +73,10 @@ const ResultsPageTemplate = `
|
|||||||
}
|
}
|
||||||
.files-grid {
|
.files-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
|
align-items: stretch;
|
||||||
|
grid-auto-rows: auto;
|
||||||
}
|
}
|
||||||
.file-card {
|
.file-card {
|
||||||
background: white;
|
background: white;
|
||||||
@@ -91,32 +93,36 @@ const ResultsPageTemplate = `
|
|||||||
}
|
}
|
||||||
.file-card-content {
|
.file-card-content {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
padding-left: 28px;
|
||||||
}
|
}
|
||||||
.file-actions {
|
.file-actions {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 10px;
|
top: 10px;
|
||||||
right: 10px;
|
left: 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
.delete-btn {
|
.delete-btn {
|
||||||
background: #e74c3c;
|
background: transparent;
|
||||||
color: white;
|
color: #ef9a9a; /* бледно-красный */
|
||||||
border: none;
|
border: none;
|
||||||
padding: 6px 12px;
|
padding: 4px 8px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 0.8em;
|
font-size: 18px;
|
||||||
transition: background 0.3s ease;
|
line-height: 1;
|
||||||
|
transition: color 0.2s ease, transform 0.2s ease;
|
||||||
}
|
}
|
||||||
.delete-btn:hover {
|
.delete-btn:hover {
|
||||||
background: #c0392b;
|
color:rgb(171, 27, 24); /* чуть ярче при ховере */
|
||||||
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
.file-name {
|
.file-name {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #333;
|
color: #333;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
|
padding-right: 10px;
|
||||||
}
|
}
|
||||||
.file-info {
|
.file-info {
|
||||||
color: #666;
|
color: #666;
|
||||||
@@ -165,16 +171,25 @@ const ResultsPageTemplate = `
|
|||||||
body { padding: 10px; }
|
body { padding: 10px; }
|
||||||
.container { margin: 0; border-radius: 8px; box-shadow: 0 10px 20px rgba(0,0,0,0.1); }
|
.container { margin: 0; border-radius: 8px; box-shadow: 0 10px 20px rgba(0,0,0,0.1); }
|
||||||
.header { padding: 20px; }
|
.header { padding: 20px; }
|
||||||
.header h1 { font-size: 2em; }
|
.header h1 { font-size: 1.9em; }
|
||||||
.content { padding: 20px; }
|
.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; }
|
.stats { grid-template-columns: 1fr 1fr; }
|
||||||
.nav-buttons { flex-direction: column; gap: 8px; }
|
.nav-buttons { flex-direction: column; gap: 8px; }
|
||||||
.nav-btn, .nav-button { text-align: center; padding: 12px 16px; font-size: 14px; }
|
.nav-btn, .nav-button { text-align: center; padding: 12px 16px; font-size: 14px; }
|
||||||
.search-container input { font-size: 16px; }
|
.search-container input { font-size: 16px; width: 96% !important; }
|
||||||
}
|
}
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.header h1 { font-size: 1.8em; }
|
.header h1 { font-size: 1.6em; }
|
||||||
|
.content { padding: 16px; }
|
||||||
.stats { grid-template-columns: 1fr; }
|
.stats { grid-template-columns: 1fr; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -182,15 +197,15 @@ const ResultsPageTemplate = `
|
|||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h1>🚀 LCG Results</h1>
|
<h1>🚀 {{.AppAbbreviation}} - {{.AppName}}</h1>
|
||||||
<p>Просмотр сохраненных результатов Linux Command GPT</p>
|
<p>Просмотр сохраненных результатов {{.AppName}}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="nav-buttons">
|
<div class="nav-buttons">
|
||||||
<button class="nav-btn" onclick="location.reload()">🔄 Обновить</button>
|
<button class="nav-btn" onclick="location.reload()">🔄 Обновить</button>
|
||||||
<a href="/run" class="nav-btn">🚀 Выполнение</a>
|
<a href="{{.BasePath}}/run" class="nav-btn">🚀 Выполнение</a>
|
||||||
<a href="/history" class="nav-btn">📝 История</a>
|
<a href="{{.BasePath}}/history" class="nav-btn">📝 История</a>
|
||||||
<a href="/prompts" class="nav-btn">⚙️ Промпты</a>
|
<a href="{{.BasePath}}/prompts" class="nav-btn">⚙️ Промпты</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Поиск -->
|
<!-- Поиск -->
|
||||||
@@ -216,10 +231,10 @@ const ResultsPageTemplate = `
|
|||||||
{{range .Files}}
|
{{range .Files}}
|
||||||
<div class="file-card" data-content="{{.Content}}">
|
<div class="file-card" data-content="{{.Content}}">
|
||||||
<div class="file-actions">
|
<div class="file-actions">
|
||||||
<button class="delete-btn" onclick="deleteFile('{{.Name}}')" title="Удалить файл">🗑️</button>
|
<button class="delete-btn" onclick="deleteFile('{{.Name}}')" title="Удалить файл">✖</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="file-card-content" onclick="window.location.href='/file/{{.Name}}'">
|
<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">
|
<div class="file-info">
|
||||||
📅 {{.ModTime}} | 📏 {{.Size}}
|
📅 {{.ModTime}} | 📏 {{.Size}}
|
||||||
</div>
|
</div>
|
||||||
@@ -240,7 +255,7 @@ const ResultsPageTemplate = `
|
|||||||
<script>
|
<script>
|
||||||
function deleteFile(filename) {
|
function deleteFile(filename) {
|
||||||
if (confirm('Вы уверены, что хотите удалить файл "' + filename + '"?\\n\\nЭто действие нельзя отменить.')) {
|
if (confirm('Вы уверены, что хотите удалить файл "' + filename + '"?\\n\\nЭто действие нельзя отменить.')) {
|
||||||
fetch('/delete/' + encodeURIComponent(filename), {
|
fetch('{{.BasePath}}/delete/' + encodeURIComponent(filename), {
|
||||||
method: 'DELETE'
|
method: 'DELETE'
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
|
|||||||
16
shell-code/docker-proxy-max.sh
Normal file
16
shell-code/docker-proxy-max.sh
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#! /usr/bin/bash
|
||||||
|
|
||||||
|
VERSION=$1
|
||||||
|
if [ -z "$VERSION" ]; then
|
||||||
|
VERSION=latest
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker pull kuznetcovay/lcg:"${VERSION}"
|
||||||
|
|
||||||
|
docker run -p 8080:8080 \
|
||||||
|
-e LCG_PROVIDER=proxy \
|
||||||
|
-e LCG_HOST=https://direct-dev.ru \
|
||||||
|
-e LCG_MODEL=GigaChat-2-Max \
|
||||||
|
-e LCG_JWT_TOKEN="$(go-ansible-vault --key "$(cat ~/.config/gak)" \
|
||||||
|
-i ~/.config/jwt.direct-dev.ru get -m JWT_TOKEN -q)" \
|
||||||
|
kuznetcovay/lcg:"${VERSION}"
|
||||||
7
shell-code/run-proxy-max.sh
Normal file
7
shell-code/run-proxy-max.sh
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#! /usr/bin/bash
|
||||||
|
|
||||||
|
LCG_PROVIDER=proxy LCG_HOST=https://direct-dev.ru \
|
||||||
|
LCG_MODEL=GigaChat-2-Max \
|
||||||
|
LCG_JWT_TOKEN=$(go-ansible-vault --key $(cat ~/.config/gak) -i ~/.config/jwt.direct-dev.ru get -m JWT_TOKEN -q) \
|
||||||
|
go run . $1 $2 $3 $4 $5 $6 $7 $8 $9
|
||||||
|
|
||||||
7
shell-code/run-proxy.sh
Normal file
7
shell-code/run-proxy.sh
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#! /usr/bin/bash
|
||||||
|
|
||||||
|
LCG_PROVIDER=proxy LCG_HOST=https://direct-dev.ru \
|
||||||
|
LCG_MODEL=GigaChat-2 \
|
||||||
|
LCG_JWT_TOKEN=$(go-ansible-vault --key $(cat ~/.config/gak) -i ~/.config/jwt.direct-dev.ru get -m JWT_TOKEN -q) \
|
||||||
|
go run . $1 $2 $3 $4 $5 $6 $7 $8 $9
|
||||||
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
#!/usr/bin/bash
|
|
||||||
|
|
||||||
# shellcheck disable=SC2034
|
|
||||||
LCG_PROVIDER=proxy LCG_HOST=http://localhost:8080 LCG_MODEL=GigaChat-2-Max LCG_JWT_TOKEN=$(go-ansible-vault -a -i shell-code/jwt.admin.token get -m 'JWT_TOKEN' -q) go run . $1 $2 $3 $4 $5 $6 $7 $8 $9
|
|
||||||
|
|
||||||
LCG_PROVIDER=proxy LCG_HOST=https://direct-dev.ru LCG_MODEL=GigaChat-2-Max LCG_JWT_TOKEN=$(go-ansible-vault --key $(cat ~/.config/gak) -i ~/.config/jwt.direct-dev.ru get -m 'JWT_TOKEN' -q) go run . [your question here]
|
|
||||||
162
ssl/ssl.go
Normal file
162
ssl/ssl.go
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
package ssl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GenerateSelfSignedCert генерирует самоподписанный сертификат
|
||||||
|
func GenerateSelfSignedCert(host string) (*tls.Certificate, error) {
|
||||||
|
// Создаем приватный ключ
|
||||||
|
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to generate private key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем сертификат
|
||||||
|
template := x509.Certificate{
|
||||||
|
SerialNumber: big.NewInt(1),
|
||||||
|
Subject: pkix.Name{
|
||||||
|
Organization: []string{"LCG Server"},
|
||||||
|
Country: []string{"RU"},
|
||||||
|
Province: []string{""},
|
||||||
|
Locality: []string{""},
|
||||||
|
StreetAddress: []string{""},
|
||||||
|
PostalCode: []string{""},
|
||||||
|
},
|
||||||
|
NotBefore: time.Now(),
|
||||||
|
NotAfter: time.Now().Add(365 * 24 * time.Hour), // 1 год
|
||||||
|
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||||
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||||
|
IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback},
|
||||||
|
DNSNames: []string{"localhost", host},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Подписываем сертификат
|
||||||
|
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create certificate: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем TLS сертификат
|
||||||
|
cert := &tls.Certificate{
|
||||||
|
Certificate: [][]byte{certDER},
|
||||||
|
PrivateKey: privateKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
return cert, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveCertToFile сохраняет сертификат и ключ в файлы
|
||||||
|
func SaveCertToFile(cert *tls.Certificate, certFile, keyFile string) error {
|
||||||
|
// Создаем директорию если не существует
|
||||||
|
certDir := filepath.Dir(certFile)
|
||||||
|
if err := os.MkdirAll(certDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create cert directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохраняем сертификат
|
||||||
|
certOut, err := os.Create(certFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open cert file: %v", err)
|
||||||
|
}
|
||||||
|
defer certOut.Close()
|
||||||
|
|
||||||
|
if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Certificate[0]}); err != nil {
|
||||||
|
return fmt.Errorf("failed to encode cert: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохраняем приватный ключ
|
||||||
|
keyOut, err := os.Create(keyFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open key file: %v", err)
|
||||||
|
}
|
||||||
|
defer keyOut.Close()
|
||||||
|
|
||||||
|
privateKeyDER, err := x509.MarshalPKCS8PrivateKey(cert.PrivateKey)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal private key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := pem.Encode(keyOut, &pem.Block{Type: "PRIVATE KEY", Bytes: privateKeyDER}); err != nil {
|
||||||
|
return fmt.Errorf("failed to encode private key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadOrGenerateCert загружает существующий сертификат или генерирует новый
|
||||||
|
func LoadOrGenerateCert(host string) (*tls.Certificate, error) {
|
||||||
|
// Определяем пути к файлам сертификата
|
||||||
|
certFile := config.AppConfig.Server.SSLCertFile
|
||||||
|
keyFile := config.AppConfig.Server.SSLKeyFile
|
||||||
|
|
||||||
|
// Если пути не указаны, используем стандартные
|
||||||
|
if certFile == "" {
|
||||||
|
certFile = filepath.Join(config.AppConfig.Server.ConfigFolder, "server", "ssl", "cert.pem")
|
||||||
|
}
|
||||||
|
if keyFile == "" {
|
||||||
|
keyFile = filepath.Join(config.AppConfig.Server.ConfigFolder, "server", "ssl", "key.pem")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем существующие файлы
|
||||||
|
if _, err := os.Stat(certFile); err == nil {
|
||||||
|
if _, err := os.Stat(keyFile); err == nil {
|
||||||
|
// Загружаем существующий сертификат
|
||||||
|
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||||
|
if err == nil {
|
||||||
|
return &cert, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Генерируем новый сертификат
|
||||||
|
cert, err := GenerateSelfSignedCert(host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохраняем сертификат
|
||||||
|
if err := SaveCertToFile(cert, certFile, keyFile); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return cert, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSecureHost проверяет, является ли хост безопасным для HTTP
|
||||||
|
func IsSecureHost(host string) bool {
|
||||||
|
secureHosts := []string{"localhost", "127.0.0.1", "::1"}
|
||||||
|
return slices.Contains(secureHosts, host)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShouldUseHTTPS определяет, нужно ли использовать HTTPS
|
||||||
|
func ShouldUseHTTPS(host string) bool {
|
||||||
|
|
||||||
|
// Если явно разрешен HTTP, используем HTTP
|
||||||
|
if config.AppConfig.Server.AllowHTTP {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если хост не localhost/127.0.0.1, принуждаем к HTTPS
|
||||||
|
if !IsSecureHost(host) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// По умолчанию для localhost используем HTTP
|
||||||
|
return false
|
||||||
|
}
|
||||||
158
test_csrf.sh
Executable file
158
test_csrf.sh
Executable file
@@ -0,0 +1,158 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 🛡️ CSRF Protection Test Script
|
||||||
|
# Тестирует CSRF защиту LCG приложения
|
||||||
|
|
||||||
|
echo "🛡️ Тестирование CSRF защиты LCG"
|
||||||
|
echo "=================================="
|
||||||
|
|
||||||
|
# Цвета для вывода
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Функция для вывода результатов
|
||||||
|
print_result() {
|
||||||
|
local test_name="$1"
|
||||||
|
local status="$2"
|
||||||
|
local message="$3"
|
||||||
|
|
||||||
|
if [ "$status" = "PASS" ]; then
|
||||||
|
echo -e "${GREEN}✅ $test_name: PASS${NC} - $message"
|
||||||
|
elif [ "$status" = "FAIL" ]; then
|
||||||
|
echo -e "${RED}❌ $test_name: FAIL${NC} - $message"
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}⚠️ $test_name: $status${NC} - $message"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Проверяем, запущен ли сервер
|
||||||
|
echo -e "${BLUE}🔍 Проверяем доступность сервера...${NC}"
|
||||||
|
if ! curl -s http://localhost:8080/login > /dev/null 2>&1; then
|
||||||
|
echo -e "${RED}❌ Сервер не доступен на localhost:8080${NC}"
|
||||||
|
echo "Запустите сервер командой: LCG_SERVER_REQUIRE_AUTH=true ./lcg serve -p 8080"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${GREEN}✅ Сервер доступен${NC}"
|
||||||
|
|
||||||
|
# Тест 1: Попытка выполнения команды без CSRF токена
|
||||||
|
echo -e "\n${BLUE}🧪 Тест 1: Выполнение команды без CSRF токена${NC}"
|
||||||
|
response=$(curl -s -w "%{http_code}" -X POST http://localhost:8080/api/execute \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"prompt":"whoami","system_id":"1"}' \
|
||||||
|
-o /dev/null)
|
||||||
|
|
||||||
|
if [ "$response" = "403" ]; then
|
||||||
|
print_result "CSRF защита /api/execute" "PASS" "Запрос заблокирован (403 Forbidden)"
|
||||||
|
else
|
||||||
|
print_result "CSRF защита /api/execute" "FAIL" "Запрос прошел (HTTP $response)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Тест 2: Попытка сохранения результата без CSRF токена
|
||||||
|
echo -e "\n${BLUE}🧪 Тест 2: Сохранение результата без CSRF токена${NC}"
|
||||||
|
response=$(curl -s -w "%{http_code}" -X POST http://localhost:8080/api/save-result \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"result":"test result","command":"test command"}' \
|
||||||
|
-o /dev/null)
|
||||||
|
|
||||||
|
if [ "$response" = "403" ]; then
|
||||||
|
print_result "CSRF защита /api/save-result" "PASS" "Запрос заблокирован (403 Forbidden)"
|
||||||
|
else
|
||||||
|
print_result "CSRF защита /api/save-result" "FAIL" "Запрос прошел (HTTP $response)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Тест 3: Попытка добавления в историю без CSRF токена
|
||||||
|
echo -e "\n${BLUE}🧪 Тест 3: Добавление в историю без CSRF токена${NC}"
|
||||||
|
response=$(curl -s -w "%{http_code}" -X POST http://localhost:8080/api/add-to-history \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"prompt":"test prompt","result":"test result"}' \
|
||||||
|
-o /dev/null)
|
||||||
|
|
||||||
|
if [ "$response" = "403" ]; then
|
||||||
|
print_result "CSRF защита /api/add-to-history" "PASS" "Запрос заблокирован (403 Forbidden)"
|
||||||
|
else
|
||||||
|
print_result "CSRF защита /api/add-to-history" "FAIL" "Запрос прошел (HTTP $response)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Тест 4: Проверка GET запросов (должны работать)
|
||||||
|
echo -e "\n${BLUE}🧪 Тест 4: GET запросы (должны работать)${NC}"
|
||||||
|
response=$(curl -s -w "%{http_code}" http://localhost:8080/login -o /dev/null)
|
||||||
|
|
||||||
|
if [ "$response" = "200" ]; then
|
||||||
|
print_result "GET запросы" "PASS" "GET запросы работают (HTTP $response)"
|
||||||
|
else
|
||||||
|
print_result "GET запросы" "FAIL" "GET запросы не работают (HTTP $response)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Тест 5: Проверка наличия CSRF токена на странице входа
|
||||||
|
echo -e "\n${BLUE}🧪 Тест 5: Наличие CSRF токена на странице входа${NC}"
|
||||||
|
csrf_token=$(curl -s http://localhost:8080/login | grep -o 'name="csrf_token"[^>]*value="[^"]*"' | sed 's/.*value="\([^"]*\)".*/\1/')
|
||||||
|
|
||||||
|
if [ -n "$csrf_token" ]; then
|
||||||
|
print_result "CSRF токен на странице входа" "PASS" "Токен найден: ${csrf_token:0:20}..."
|
||||||
|
else
|
||||||
|
print_result "CSRF токен на странице входа" "FAIL" "Токен не найден"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Тест 6: Попытка атаки с поддельным CSRF токеном
|
||||||
|
echo -e "\n${BLUE}🧪 Тест 6: Атака с поддельным CSRF токеном${NC}"
|
||||||
|
response=$(curl -s -w "%{http_code}" -X POST http://localhost:8080/api/execute \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-CSRF-Token: fake_token" \
|
||||||
|
-d '{"prompt":"whoami","system_id":"1"}' \
|
||||||
|
-o /dev/null)
|
||||||
|
|
||||||
|
if [ "$response" = "403" ]; then
|
||||||
|
print_result "CSRF защита от поддельного токена" "PASS" "Поддельный токен заблокирован (403 Forbidden)"
|
||||||
|
else
|
||||||
|
print_result "CSRF защита от поддельного токена" "FAIL" "Поддельный токен принят (HTTP $response)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Итоговый отчет
|
||||||
|
echo -e "\n${BLUE}📊 Итоговый отчет:${NC}"
|
||||||
|
echo "=================================="
|
||||||
|
|
||||||
|
# Подсчитываем результаты
|
||||||
|
total_tests=6
|
||||||
|
passed_tests=0
|
||||||
|
|
||||||
|
# Проверяем каждый тест
|
||||||
|
if curl -s -w "%{http_code}" -X POST http://localhost:8080/api/execute -H "Content-Type: application/json" -d '{"prompt":"test"}' -o /dev/null | grep -q "403"; then
|
||||||
|
((passed_tests++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
if curl -s -w "%{http_code}" -X POST http://localhost:8080/api/save-result -H "Content-Type: application/json" -d '{"result":"test"}' -o /dev/null | grep -q "403"; then
|
||||||
|
((passed_tests++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
if curl -s -w "%{http_code}" -X POST http://localhost:8080/api/add-to-history -H "Content-Type: application/json" -d '{"prompt":"test"}' -o /dev/null | grep -q "403"; then
|
||||||
|
((passed_tests++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
if curl -s -w "%{http_code}" http://localhost:8080/login -o /dev/null | grep -q "200"; then
|
||||||
|
((passed_tests++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
if curl -s http://localhost:8080/login | grep -q 'name="csrf_token"'; then
|
||||||
|
((passed_tests++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
if curl -s -w "%{http_code}" -X POST http://localhost:8080/api/execute -H "Content-Type: application/json" -H "X-CSRF-Token: fake" -d '{"prompt":"test"}' -o /dev/null | grep -q "403"; then
|
||||||
|
((passed_tests++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "Пройдено тестов: ${GREEN}$passed_tests${NC} из ${BLUE}$total_tests${NC}"
|
||||||
|
|
||||||
|
if [ $passed_tests -eq $total_tests ]; then
|
||||||
|
echo -e "${GREEN}🎉 Все тесты пройдены! CSRF защита работает корректно.${NC}"
|
||||||
|
exit 0
|
||||||
|
elif [ $passed_tests -ge 4 ]; then
|
||||||
|
echo -e "${YELLOW}⚠️ Большинство тестов пройдено, но есть проблемы с CSRF защитой.${NC}"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo -e "${RED}❌ Критические проблемы с CSRF защитой!${NC}"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
154
validation/validation.go
Normal file
154
validation/validation.go
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
package validation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ValidationError представляет ошибку валидации
|
||||||
|
type ValidationError struct {
|
||||||
|
Field string
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ValidationError) Error() string {
|
||||||
|
return fmt.Sprintf("%s: %s", e.Field, e.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateSystemPrompt проверяет длину системного промпта
|
||||||
|
func ValidateSystemPrompt(prompt string) error {
|
||||||
|
maxLen := config.AppConfig.Validation.MaxSystemPromptLength
|
||||||
|
if len(prompt) > maxLen {
|
||||||
|
return ValidationError{
|
||||||
|
Field: "system_prompt",
|
||||||
|
Message: fmt.Sprintf("системный промпт слишком длинный: %d символов (максимум %d)", len(prompt), maxLen),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateUserMessage проверяет длину пользовательского сообщения
|
||||||
|
func ValidateUserMessage(message string) error {
|
||||||
|
maxLen := config.AppConfig.Validation.MaxUserMessageLength
|
||||||
|
if len(message) > maxLen {
|
||||||
|
return ValidationError{
|
||||||
|
Field: "user_message",
|
||||||
|
Message: fmt.Sprintf("пользовательское сообщение слишком длинное: %d символов (максимум %d)", len(message), maxLen),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidatePromptAndMessage проверяет и системный промпт, и пользовательское сообщение
|
||||||
|
func ValidatePromptAndMessage(systemPrompt, userMessage string) error {
|
||||||
|
if err := ValidateSystemPrompt(systemPrompt); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := ValidateUserMessage(userMessage); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TruncateSystemPrompt обрезает системный промпт до максимальной длины
|
||||||
|
func TruncateSystemPrompt(prompt string) string {
|
||||||
|
maxLen := config.AppConfig.Validation.MaxSystemPromptLength
|
||||||
|
if len(prompt) <= maxLen {
|
||||||
|
return prompt
|
||||||
|
}
|
||||||
|
return prompt[:maxLen]
|
||||||
|
}
|
||||||
|
|
||||||
|
// TruncateUserMessage обрезает пользовательское сообщение до максимальной длины
|
||||||
|
func TruncateUserMessage(message string) string {
|
||||||
|
maxLen := config.AppConfig.Validation.MaxUserMessageLength
|
||||||
|
if len(message) <= maxLen {
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
return message[:maxLen]
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSystemPromptLength возвращает длину системного промпта
|
||||||
|
func GetSystemPromptLength(prompt string) int {
|
||||||
|
return len(prompt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserMessageLength возвращает длину пользовательского сообщения
|
||||||
|
func GetUserMessageLength(message string) int {
|
||||||
|
return len(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormatLengthInfo форматирует информацию о длине для отображения
|
||||||
|
func FormatLengthInfo(systemPrompt, userMessage string) string {
|
||||||
|
systemLen := GetSystemPromptLength(systemPrompt)
|
||||||
|
userLen := GetUserMessageLength(userMessage)
|
||||||
|
maxSystemLen := config.AppConfig.Validation.MaxSystemPromptLength
|
||||||
|
maxUserLen := config.AppConfig.Validation.MaxUserMessageLength
|
||||||
|
|
||||||
|
var warnings []string
|
||||||
|
|
||||||
|
if systemLen > maxSystemLen {
|
||||||
|
warnings = append(warnings, fmt.Sprintf("⚠️ Системный промпт превышает лимит: %d/%d символов", systemLen, maxSystemLen))
|
||||||
|
}
|
||||||
|
|
||||||
|
if userLen > maxUserLen {
|
||||||
|
warnings = append(warnings, fmt.Sprintf("⚠️ Пользовательское сообщение превышает лимит: %d/%d символов", userLen, maxUserLen))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(warnings) == 0 {
|
||||||
|
return fmt.Sprintf("✅ Длины в пределах нормы: системный промпт %d/%d, сообщение %d/%d",
|
||||||
|
systemLen, maxSystemLen, userLen, maxUserLen)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(warnings, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidatePromptName проверяет длину названия промпта
|
||||||
|
func ValidatePromptName(name string) error {
|
||||||
|
maxLen := config.AppConfig.Validation.MaxPromptNameLength
|
||||||
|
if len(name) > maxLen {
|
||||||
|
return ValidationError{
|
||||||
|
Field: "prompt_name",
|
||||||
|
Message: fmt.Sprintf("название промпта слишком длинное: %d символов (максимум %d)", len(name), maxLen),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidatePromptDescription проверяет длину описания промпта
|
||||||
|
func ValidatePromptDescription(description string) error {
|
||||||
|
maxLen := config.AppConfig.Validation.MaxPromptDescLength
|
||||||
|
if len(description) > maxLen {
|
||||||
|
return ValidationError{
|
||||||
|
Field: "prompt_description",
|
||||||
|
Message: fmt.Sprintf("описание промпта слишком длинное: %d символов (максимум %d)", len(description), maxLen),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateCommand проверяет длину команды
|
||||||
|
func ValidateCommand(command string) error {
|
||||||
|
maxLen := config.AppConfig.Validation.MaxCommandLength
|
||||||
|
if len(command) > maxLen {
|
||||||
|
return ValidationError{
|
||||||
|
Field: "command",
|
||||||
|
Message: fmt.Sprintf("команда слишком длинная: %d символов (максимум %d)", len(command), maxLen),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateExplanation проверяет длину объяснения
|
||||||
|
func ValidateExplanation(explanation string) error {
|
||||||
|
maxLen := config.AppConfig.Validation.MaxExplanationLength
|
||||||
|
if len(explanation) > maxLen {
|
||||||
|
return ValidationError{
|
||||||
|
Field: "explanation",
|
||||||
|
Message: fmt.Sprintf("объяснение слишком длинное: %d символов (максимум %d)", len(explanation), maxLen),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user