add https server functionality - befor auth functionality implementation

This commit is contained in:
2025-10-23 11:41:03 +06:00
parent 3e1cb1e078
commit e1bd79db8c
27 changed files with 2164 additions and 97 deletions

View File

@@ -16,11 +16,15 @@ before:
- go generate ./...
builds:
- env:
- binary: lcg
env:
- CGO_ENABLED=0
goarch:
- amd64
- arm64
- arm
goos:
- linux
- windows
- darwin
archives:

232
CONFIG_COMMAND.md Normal file
View 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": 1000,
"max_user_message_length": 2000,
"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** - максимальная длина системного промпта
- **max_user_message_length** - максимальная длина пользовательского сообщения
- **max_prompt_name_length** - максимальная длина названия промпта
- **max_prompt_desc_length** - максимальная длина описания промпта
- **max_command_length** - максимальная длина команды/ответа
- **max_explanation_length** - максимальная длина объяснения
## 🔒 Безопасность
При выводе полной конфигурации чувствительные данные маскируются:
- **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` показывает актуальное состояние конфигурации после применения всех переменных окружения и значений по умолчанию.

337
RELEASE_GUIDE.md Normal file
View 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`. Для других проектов может потребоваться адаптация конфигурации.

118
ROADMAP.md Normal file
View 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
SECURITY_FEATURES.md Normal file
View 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` - эта документация
---
**Результат**: Приложение теперь автоматически обеспечивает безопасность соединения в зависимости от конфигурации хоста!

205
VALIDATION_CONFIG.md Normal file
View File

@@ -0,0 +1,205 @@
# 🔧 Конфигурация валидации длины полей
## 📋 Переменные окружения
Все настройки валидации можно настроить через переменные окружения:
### Основные лимиты
| Переменная | Описание | По умолчанию |
|------------|----------|--------------|
| `LCG_MAX_SYSTEM_PROMPT_LENGTH` | Максимальная длина системного промпта | 1000 |
| `LCG_MAX_USER_MESSAGE_LENGTH` | Максимальная длина пользовательского сообщения | 2000 |
| `LCG_MAX_PROMPT_NAME_LENGTH` | Максимальная длина названия промпта | 200 |
| `LCG_MAX_PROMPT_DESC_LENGTH` | Максимальная длина описания промпта | 500 |
| `LCG_MAX_COMMAND_LENGTH` | Максимальная длина команды/ответа | 2000 |
| `LCG_MAX_EXPLANATION_LENGTH` | Максимальная длина объяснения | 2000 |
## 🚀 Примеры использования
### Установка через переменные окружения
```bash
# Увеличить лимит системного промпта до 2к символов
export LCG_MAX_SYSTEM_PROMPT_LENGTH=2000
# Уменьшить лимит пользовательского сообщения до 1к символов
export LCG_MAX_USER_MESSAGE_LENGTH=1000
# Увеличить лимит названия промпта до 500 символов
export LCG_MAX_PROMPT_NAME_LENGTH=500
```
### Установка в .env файле
```bash
# .env файл
LCG_MAX_SYSTEM_PROMPT_LENGTH=2000
LCG_MAX_USER_MESSAGE_LENGTH=1500
LCG_MAX_PROMPT_NAME_LENGTH=300
LCG_MAX_PROMPT_DESC_LENGTH=1000
LCG_MAX_COMMAND_LENGTH=3000
LCG_MAX_EXPLANATION_LENGTH=5000
```
### Установка в 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=1500
Environment=LCG_MAX_PROMPT_NAME_LENGTH=300
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=1500
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=1500
- LCG_MAX_PROMPT_NAME_LENGTH=300
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: системный промпт слишком длинный: 1500 символов (максимум 1000)
❌ Ошибка: user_message: пользовательское сообщение слишком длинное: 2500 символов (максимум 2000)
❌ Ошибка: prompt_name: название промпта слишком длинное: 300 символов (максимум 200)
```
## 🔄 Миграция с жестко заданных значений
Если ранее использовались жестко заданные значения в коде, теперь они автоматически заменяются на значения из конфигурации:
```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=2000
export LCG_MAX_PROMPT_NAME_LENGTH=200
export LCG_MAX_PROMPT_DESC_LENGTH=500
```
### Для продакшена
```bash
export LCG_MAX_SYSTEM_PROMPT_LENGTH=1000
export LCG_MAX_USER_MESSAGE_LENGTH=1500
export LCG_MAX_PROMPT_NAME_LENGTH=100
export LCG_MAX_PROMPT_DESC_LENGTH=300
```
### Для высоконагруженных систем
```bash
export LCG_MAX_SYSTEM_PROMPT_LENGTH=500
export LCG_MAX_USER_MESSAGE_LENGTH=1000
export LCG_MAX_PROMPT_NAME_LENGTH=50
export LCG_MAX_PROMPT_DESC_LENGTH=200
```
---
**Примечание**: Все значения настраиваются через переменные окружения и применяются ко всем частям приложения (консоль, веб-интерфейс, API).

63
VERBOSE_PROMPT_EDITING.md Normal file
View 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. Нажмите "Сохранить"
## ✅ **Статус:**
**ГОТОВО** - Редактирование промптов подробности полностью реализовано и протестировано.

View File

@@ -3,6 +3,7 @@ package config
import (
"os"
"path"
"strconv"
"strings"
)
@@ -25,6 +26,7 @@ type Config struct {
AllowExecution bool
MainFlags MainFlags
Server ServerConfig
Validation ValidationConfig
}
type MainFlags struct {
@@ -37,8 +39,21 @@ type MainFlags struct {
}
type ServerConfig struct {
Port string
Host string
Port string
Host string
ConfigFolder string
AllowHTTP bool
SSLCertFile string
SSLKeyFile string
}
type ValidationConfig struct {
MaxSystemPromptLength int
MaxUserMessageLength int
MaxPromptNameLength int
MaxPromptDescLength int
MaxCommandLength int
MaxExplanationLength int
}
func getEnv(key, defaultValue string) string {
@@ -48,6 +63,38 @@ func getEnv(key, defaultValue string) string {
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"}
for _, secureHost := range secureHosts {
if host == secureHost {
return true
}
}
return false
}
func Load() Config {
cwd, _ := os.Getwd()
@@ -79,8 +126,20 @@ func Load() Config {
NoHistoryEnv: getEnv("LCG_NO_HISTORY", ""),
AllowExecution: isAllowExecutionEnabled(),
Server: ServerConfig{
Port: getEnv("LCG_SERVER_PORT", "8080"),
Host: getEnv("LCG_SERVER_HOST", "localhost"),
Port: getEnv("LCG_SERVER_PORT", "8080"),
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", ""),
},
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),
},
}
}

187
main.go
View File

@@ -2,6 +2,7 @@ package main
import (
_ "embed"
"encoding/json"
"fmt"
"math"
"os"
@@ -18,6 +19,7 @@ import (
"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/serve"
"github.com/direct-dev-ru/linux-command-gpt/validation"
"github.com/urfave/cli/v2"
)
@@ -308,24 +310,20 @@ func getCommands() []*cli.Command {
Name: "config",
Aliases: []string{"co"}, // Изменено с "c" на "co"
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 {
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***"
}())
if c.Bool("full") {
// Выводим полную конфигурацию в JSON формате
showFullConfig()
} else {
// Выводим краткую конфигурацию
showShortConfig()
}
return nil
},
@@ -552,10 +550,35 @@ func getCommands() []*cli.Command {
config.AppConfig.MainFlags.Debug = true
}
printColored(fmt.Sprintf("🌐 Запускаю HTTP сервер на %s:%s\n", host, port), colorCyan)
// Обновляем конфигурацию сервера с новыми параметрами
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("🌐 Запускаю %s сервер на %s:%s\n", strings.ToUpper(protocol), host, port), colorCyan)
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"
}
url := fmt.Sprintf("%s://%s:%s", protocol, browserHost, port)
if openBrowser {
printColored("🌍 Открываю браузер...\n", colorGreen)
@@ -576,6 +599,18 @@ func getCommands() []*cli.Command {
}
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 информацию если включен флаг
if config.AppConfig.MainFlags.Debug {
printDebugInfo(file, system, commandInput, timeout)
@@ -884,3 +919,117 @@ func openBrowserURL(url string) error {
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"}
for _, secureHost := range secureHosts {
if host == secureHost {
return true
}
}
return false
}
// 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"`
ProxyUrl string `json:"proxy_url"`
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,
ProxyUrl: config.AppConfig.ProxyUrl,
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,
}
// Выводим JSON с отступами
jsonData, err := json.MarshalIndent(safeConfig, "", " ")
if err != nil {
fmt.Printf("Ошибка сериализации конфигурации: %v\n", err)
return
}
fmt.Println(string(jsonData))
}

View File

@@ -10,6 +10,7 @@ import (
"time"
"github.com/direct-dev-ru/linux-command-gpt/config"
"github.com/direct-dev-ru/linux-command-gpt/validation"
)
// SaveResultRequest представляет запрос на сохранение результата
@@ -62,6 +63,20 @@ func handleSaveResult(w http.ResponseWriter, r *http.Request) {
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 {
apiJsonResponse(w, SaveResultResponse{
@@ -124,6 +139,28 @@ func handleAddToHistory(w http.ResponseWriter, r *http.Request) {
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)
if err != nil {

View File

@@ -9,6 +9,7 @@ import (
"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/validation"
)
// ExecuteRequest представляет запрос на выполнение
@@ -58,9 +59,20 @@ func handleExecute(w http.ResponseWriter, r *http.Request) {
return
}
// Валидация длины пользовательского сообщения
if err := validation.ValidateUserMessage(req.Prompt); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Определяем системный промпт
systemPrompt := ""
if req.SystemText != "" {
// Валидация длины пользовательского системного промпта
if err := validation.ValidateSystemPrompt(req.SystemText); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
systemPrompt = req.SystemText
} else if req.SystemID > 0 && req.SystemID <= 5 {
// Получаем системный промпт по ID
@@ -70,9 +82,19 @@ func handleExecute(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Failed to get system prompt", http.StatusInternalServerError)
return
}
// Валидация длины системного промпта из базы
if err := validation.ValidateSystemPrompt(prompt.Content); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
systemPrompt = prompt.Content
} else {
// Используем промпт по умолчанию
// Валидация длины системного промпта по умолчанию
if err := validation.ValidateSystemPrompt(config.AppConfig.Prompt); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
systemPrompt = config.AppConfig.Prompt
}

View File

@@ -10,6 +10,7 @@ import (
"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/serve/templates"
"github.com/direct-dev-ru/linux-command-gpt/validation"
"github.com/russross/blackfriday/v2"
)
@@ -22,6 +23,8 @@ type ExecutePageData struct {
ResultSection template.HTML
VerboseButtons template.HTML
ActionButtons template.HTML
// Поля конфигурации для валидации
MaxUserMessageLength int
}
// SystemPromptOption представляет опцию системного промпта
@@ -74,13 +77,14 @@ func showExecuteForm(w http.ResponseWriter) {
}
data := ExecutePageData{
Title: "Выполнение запроса",
Header: "Выполнение запроса",
CurrentPrompt: "",
SystemOptions: systemOptions,
ResultSection: template.HTML(""),
VerboseButtons: template.HTML(""),
ActionButtons: template.HTML(""),
Title: "Выполнение запроса",
Header: "Выполнение запроса",
CurrentPrompt: "",
SystemOptions: systemOptions,
ResultSection: template.HTML(""),
VerboseButtons: template.HTML(""),
ActionButtons: template.HTML(""),
MaxUserMessageLength: config.AppConfig.Validation.MaxUserMessageLength,
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
@@ -102,6 +106,12 @@ func handleExecuteRequest(w http.ResponseWriter, r *http.Request) {
return
}
// Валидация длины пользовательского сообщения
if err := validation.ValidateUserMessage(prompt); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
systemID := 1
if systemIDStr != "" {
if id, err := strconv.Atoi(systemIDStr); err == nil && id >= 1 && id <= 5 {
@@ -116,6 +126,12 @@ func handleExecuteRequest(w http.ResponseWriter, r *http.Request) {
return
}
// Валидация длины системного промпта
if err := validation.ValidateSystemPrompt(systemPrompt.Content); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Создаем GPT клиент
gpt3 := gpt.NewGpt3(
config.AppConfig.ProviderType,
@@ -179,13 +195,14 @@ func handleExecuteRequest(w http.ResponseWriter, r *http.Request) {
}
data := ExecutePageData{
Title: "Результат выполнения",
Header: "Результат выполнения",
CurrentPrompt: prompt,
SystemOptions: systemOptions,
ResultSection: template.HTML(formatResultSection(result)),
VerboseButtons: template.HTML(formatVerboseButtons(result)),
ActionButtons: template.HTML(formatActionButtons(result)),
Title: "Результат выполнения",
Header: "Результат выполнения",
CurrentPrompt: prompt,
SystemOptions: systemOptions,
ResultSection: template.HTML(formatResultSection(result)),
VerboseButtons: template.HTML(formatVerboseButtons(result)),
ActionButtons: template.HTML(formatActionButtons(result)),
MaxUserMessageLength: config.AppConfig.Validation.MaxUserMessageLength,
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")

View File

@@ -151,18 +151,29 @@ func handleHistoryView(w http.ResponseWriter, r *http.Request) {
</div>`, string(explanationHTML))
}
// Создаем HTML страницу
htmlPage := fmt.Sprintf(templates.HistoryViewTemplate,
index, // title
index, // header
targetEntry.Timestamp.Format("02.01.2006 15:04:05"), // timestamp
index, // meta index
targetEntry.Command, // command
targetEntry.Response, // response
explanationSection, // explanation (if exists)
index, // delete button index
)
// Создаем данные для шаблона
data := struct {
Index int
Timestamp string
Command string
Response string
ExplanationHTML template.HTML
}{
Index: index,
Timestamp: targetEntry.Timestamp.Format("02.01.2006 15:04:05"),
Command: targetEntry.Command,
Response: targetEntry.Response,
ExplanationHTML: template.HTML(explanationSection),
}
// Парсим и выполняем шаблон
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.Write([]byte(htmlPage))
t.Execute(w, data)
}

View File

@@ -9,8 +9,10 @@ import (
"strconv"
"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/serve/templates"
"github.com/direct-dev-ru/linux-command-gpt/validation"
)
// VerbosePrompt структура для промптов подробности
@@ -82,13 +84,19 @@ func handlePromptsPage(w http.ResponseWriter, r *http.Request) {
verbosePrompts := getVerbosePromptsFromFile(pm.Prompts, lang)
data := struct {
Prompts []PromptWithDefault
VerbosePrompts []VerbosePrompt
Lang string
Prompts []PromptWithDefault
VerbosePrompts []VerbosePrompt
Lang string
MaxSystemPromptLength int
MaxPromptNameLength int
MaxPromptDescLength int
}{
Prompts: promptsWithDefault,
VerbosePrompts: verbosePrompts,
Lang: lang,
Prompts: promptsWithDefault,
VerbosePrompts: verbosePrompts,
Lang: lang,
MaxSystemPromptLength: config.AppConfig.Validation.MaxSystemPromptLength,
MaxPromptNameLength: config.AppConfig.Validation.MaxPromptNameLength,
MaxPromptDescLength: config.AppConfig.Validation.MaxPromptDescLength,
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
@@ -124,6 +132,20 @@ func handleAddPrompt(w http.ResponseWriter, r *http.Request) {
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 {
http.Error(w, fmt.Sprintf("Ошибка добавления промпта: %v", err), http.StatusInternalServerError)
@@ -171,6 +193,20 @@ func handleEditPrompt(w http.ResponseWriter, r *http.Request) {
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 {
http.Error(w, fmt.Sprintf("Ошибка обновления промпта: %v", err), http.StatusInternalServerError)
@@ -181,6 +217,76 @@ func handleEditPrompt(w http.ResponseWriter, r *http.Request) {
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 обрабатывает удаление промпта
func handleDeletePrompt(w http.ResponseWriter, r *http.Request) {
if r.Method != "DELETE" {

View File

@@ -191,12 +191,26 @@ func handleFileView(w http.ResponseWriter, r *http.Request) {
// Конвертируем Markdown в HTML
htmlContent := blackfriday.Run(content)
// Создаем HTML страницу с красивым отображением
htmlPage := fmt.Sprintf(templates.FileViewTemplate, filename, filename, string(htmlContent))
// Создаем данные для шаблона
data := struct {
Filename string
Content template.HTML
}{
Filename: filename,
Content: template.HTML(htmlContent),
}
// Парсим и выполняем шаблон
tmpl := templates.FileViewTemplate
t, err := template.New("file_view").Parse(tmpl)
if err != nil {
http.Error(w, "Ошибка шаблона", http.StatusInternalServerError)
return
}
// Устанавливаем заголовки для отображения HTML
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(htmlPage))
t.Execute(w, data)
}
// handleDeleteFile обрабатывает удаление файла

View File

@@ -1,29 +1,149 @@
package serve
import (
"crypto/tls"
"fmt"
"net/http"
"os"
"github.com/direct-dev-ru/linux-command-gpt/config"
"github.com/direct-dev-ru/linux-command-gpt/ssl"
)
// StartResultServer запускает HTTP сервер для просмотра сохраненных результатов
// StartResultServer запускает HTTP/HTTPS сервер для просмотра сохраненных результатов
func StartResultServer(host, port string) error {
// Регистрируем все маршруты
registerRoutes()
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")
// Проверяем, нужно ли использовать 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 {
fmt.Printf("🔍 DEBUG РЕЖИМ ОТКЛЮЧЕН - веб-операции не будут логироваться\n")
// Регистрируем обычные маршруты для 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")
}
return http.ListenAndServe(addr, nil)
// Редиректим на HTTPS
httpsURL := fmt.Sprintf("https://%s%s", host, r.RequestURI)
http.Redirect(w, r, httpsURL, http.StatusMovedPermanently)
}
// registerHTTPSRoutes регистрирует маршруты для HTTPS сервера
func registerHTTPSRoutes() {
// Регистрируем все маршруты кроме главной страницы
registerRoutesExceptHome()
// Регистрируем главную страницу с проверкой HTTPS
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// Проверяем, пришел ли запрос по HTTP (не HTTPS)
if r.TLS == nil {
handleHTTPSRedirect(w, r)
return
}
// Если уже HTTPS, обрабатываем как обычно
handleResultsPage(w, r)
})
}
// registerRoutesExceptHome регистрирует все маршруты кроме главной страницы
func registerRoutesExceptHome() {
// Файлы
http.HandleFunc("/file/", handleFileView)
http.HandleFunc("/delete/", handleDeleteFile)
// История запросов
http.HandleFunc("/history", handleHistoryPage)
http.HandleFunc("/history/view/", handleHistoryView)
http.HandleFunc("/history/delete/", handleDeleteHistoryEntry)
http.HandleFunc("/history/clear", handleClearHistory)
// Управление промптами
http.HandleFunc("/prompts", handlePromptsPage)
http.HandleFunc("/prompts/add", handleAddPrompt)
http.HandleFunc("/prompts/edit/", handleEditPrompt)
http.HandleFunc("/prompts/edit-verbose/", handleEditVerbosePrompt)
http.HandleFunc("/prompts/delete/", handleDeletePrompt)
http.HandleFunc("/prompts/restore/", handleRestorePrompt)
http.HandleFunc("/prompts/restore-verbose/", handleRestoreVerbosePrompt)
http.HandleFunc("/prompts/save-lang", handleSaveLang)
// Веб-страница для выполнения запросов
http.HandleFunc("/run", handleExecutePage)
// API для выполнения запросов
http.HandleFunc("/api/execute", handleExecute)
// API для сохранения результатов и истории
http.HandleFunc("/api/save-result", handleSaveResult)
http.HandleFunc("/api/add-to-history", handleAddToHistory)
}
// registerRoutes регистрирует все маршруты сервера
@@ -43,6 +163,7 @@ func registerRoutes() {
http.HandleFunc("/prompts", handlePromptsPage)
http.HandleFunc("/prompts/add", handleAddPrompt)
http.HandleFunc("/prompts/edit/", handleEditPrompt)
http.HandleFunc("/prompts/edit-verbose/", handleEditVerbosePrompt)
http.HandleFunc("/prompts/delete/", handleDeletePrompt)
http.HandleFunc("/prompts/restore/", handleRestorePrompt)
http.HandleFunc("/prompts/restore-verbose/", handleRestoreVerbosePrompt)

View File

@@ -11,6 +11,15 @@ var ExecutePageScriptsTemplate = template.Must(template.New("execute_scripts").P
e.preventDefault();
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';
const submitBtn = document.getElementById('submitBtn');
const loading = document.getElementById('loading');

View File

@@ -7,13 +7,13 @@ const FileViewTemplate = `
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>%s - LCG Results</title>
<title>{{.Filename}} - LCG Results</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 20px;
background: linear-gradient(135deg, #56ab2f 0%%, #a8e6cf 100%%);
background: linear-gradient(135deg, #56ab2f 0%, #a8e6cf 100%);
min-height: 100vh;
}
.container {
@@ -25,7 +25,7 @@ const FileViewTemplate = `
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #2d5016 0%%, #4a7c59 100%%);
background: linear-gradient(135deg, #2d5016 0%, #4a7c59 100%);
color: white;
padding: 20px 30px;
display: flex;
@@ -125,11 +125,11 @@ const FileViewTemplate = `
<body>
<div class="container">
<div class="header">
<h1>📄 %s</h1>
<h1>📄 {{.Filename}}</h1>
<a href="/" class="back-btn">← Назад к списку</a>
</div>
<div class="content">
%s
{{.Content}}
</div>
</div>
</body>

View File

@@ -143,6 +143,7 @@ const HistoryPageTemplate = `
.history-header { flex-direction: column; align-items: flex-start; gap: 8px; }
.history-item { padding: 15px; }
.history-response { font-size: 0.85em; }
.search-container input { font-size: 16px; width: 96% !important; }
}
@media (max-width: 480px) {

View File

@@ -7,13 +7,13 @@ const HistoryViewTemplate = `
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Запись #%d - LCG History</title>
<title>Запись #{{.Index}} - LCG History</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 20px;
background: linear-gradient(135deg, #56ab2f 0%%, #a8e6cf 100%%);
background: linear-gradient(135deg, #56ab2f 0%, #a8e6cf 100%);
min-height: 100vh;
}
.container {
@@ -25,7 +25,7 @@ const HistoryViewTemplate = `
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #2d5016 0%%, #4a7c59 100%%);
background: linear-gradient(135deg, #2d5016 0%, #4a7c59 100%);
color: white;
padding: 20px 30px;
display: flex;
@@ -212,7 +212,7 @@ const HistoryViewTemplate = `
.back-btn { padding: 6px 12px; font-size: 0.9em; }
.content { padding: 20px; }
.actions { flex-direction: column; }
.action-btn { width: 100%; text-align: center; }
.action-btn { text-align: center; }
.history-response-content { font-size: 0.9em; }
}
@media (max-width: 480px) {
@@ -223,34 +223,34 @@ const HistoryViewTemplate = `
<body>
<div class="container">
<div class="header">
<h1>📝 Запись #%d</h1>
<h1>📝 Запись #{{.Index}}</h1>
<a href="/history" class="back-btn">← Назад к истории</a>
</div>
<div class="content">
<div class="history-meta">
<div class="history-meta-item">
<span class="history-meta-label">📅 Время:</span> %s
<span class="history-meta-label">📅 Время:</span> {{.Timestamp}}
</div>
<div class="history-meta-item">
<span class="history-meta-label">🔢 Индекс:</span> #%d
<span class="history-meta-label">🔢 Индекс:</span> #{{.Index}}
</div>
</div>
<div class="history-command">
<h3>💬 Запрос пользователя:</h3>
<div class="history-command-text">%s</div>
<div class="history-command-text">{{.Command}}</div>
</div>
<div class="history-response">
<h3>🤖 Ответ Модели:</h3>
<div class="history-response-content">%s</div>
<div class="history-response-content">{{.Response}}</div>
</div>
%s
{{.ExplanationHTML}}
<div class="actions">
<a href="/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>

View File

@@ -407,7 +407,12 @@ const PromptsPageTemplate = `
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) {
@@ -432,10 +437,42 @@ const PromptsPageTemplate = `
document.getElementById('promptFormData').addEventListener('submit', function(e) {
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 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 = '/prompts/edit-verbose/' + id;
method = 'PUT';
} else {
url = id ? '/prompts/edit/' + id : '/prompts/add';
method = id ? 'PUT' : 'POST';
}
fetch(url, {
method: method,

View File

@@ -171,7 +171,7 @@ const ResultsPageTemplate = `
.stats { grid-template-columns: 1fr 1fr; }
.nav-buttons { flex-direction: column; gap: 8px; }
.nav-btn, .nav-button { text-align: center; padding: 12px 16px; font-size: 14px; }
.search-container input { font-size: 16px; }
.search-container input { font-size: 16px; width: 96% !important; }
}
@media (max-width: 480px) {
.header h1 { font-size: 1.8em; }

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

View File

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

164
ssl/ssl.go Normal file
View File

@@ -0,0 +1,164 @@
package ssl
import (
"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"}
for _, secureHost := range secureHosts {
if host == secureHost {
return true
}
}
return false
}
// ShouldUseHTTPS определяет, нужно ли использовать HTTPS
func ShouldUseHTTPS(host string) bool {
// Если хост не localhost/127.0.0.1, принуждаем к HTTPS
if !IsSecureHost(host) {
return true
}
// Если явно разрешен HTTP, используем HTTP
if config.AppConfig.Server.AllowHTTP {
return false
}
// По умолчанию для localhost используем HTTP
return false
}

154
validation/validation.go Normal file
View 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
}