From e1bd79db8c580ecf6e818e5aadd47e32967eb989 Mon Sep 17 00:00:00 2001 From: Anton Kuznetcov Date: Thu, 23 Oct 2025 11:41:03 +0600 Subject: [PATCH] add https server functionality - befor auth functionality implementation --- .goreleaser.yaml | 8 +- CONFIG_COMMAND.md | 232 ++++++++++++++++++++++ RELEASE_GUIDE.md | 337 ++++++++++++++++++++++++++++++++ ROADMAP.md | 118 +++++++++++ SECURITY_FEATURES.md | 199 +++++++++++++++++++ VALIDATION_CONFIG.md | 205 +++++++++++++++++++ VERBOSE_PROMPT_EDITING.md | 63 ++++++ config/config.go | 67 ++++++- main.go | 187 ++++++++++++++++-- serve/api.go | 37 ++++ serve/execute.go | 22 +++ serve/execute_page.go | 45 +++-- serve/history.go | 35 ++-- serve/prompts.go | 118 ++++++++++- serve/results.go | 20 +- serve/serve.go | 143 ++++++++++++-- serve/templates/execute.js.go | 9 + serve/templates/file.go | 10 +- serve/templates/history.go | 1 + serve/templates/history_view.go | 22 +-- serve/templates/prompts.go | 43 +++- serve/templates/results.go | 2 +- shell-code/run-proxy-max.sh | 7 + shell-code/run-proxy.sh | 7 + shell-code/run-with-proxy.sh | 6 - ssl/ssl.go | 164 ++++++++++++++++ validation/validation.go | 154 +++++++++++++++ 27 files changed, 2164 insertions(+), 97 deletions(-) create mode 100644 CONFIG_COMMAND.md create mode 100644 RELEASE_GUIDE.md create mode 100644 ROADMAP.md create mode 100644 SECURITY_FEATURES.md create mode 100644 VALIDATION_CONFIG.md create mode 100644 VERBOSE_PROMPT_EDITING.md create mode 100644 shell-code/run-proxy-max.sh create mode 100644 shell-code/run-proxy.sh delete mode 100644 shell-code/run-with-proxy.sh create mode 100644 ssl/ssl.go create mode 100644 validation/validation.go diff --git a/.goreleaser.yaml b/.goreleaser.yaml index d53627b..8dcc93e 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -16,11 +16,15 @@ before: - go generate ./... builds: - - env: + - binary: lcg + env: - CGO_ENABLED=0 + goarch: + - amd64 + - arm64 + - arm goos: - linux - - windows - darwin archives: diff --git a/CONFIG_COMMAND.md b/CONFIG_COMMAND.md new file mode 100644 index 0000000..1b35f76 --- /dev/null +++ b/CONFIG_COMMAND.md @@ -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` показывает актуальное состояние конфигурации после применения всех переменных окружения и значений по умолчанию. diff --git a/RELEASE_GUIDE.md b/RELEASE_GUIDE.md new file mode 100644 index 0000000..b66d78b --- /dev/null +++ b/RELEASE_GUIDE.md @@ -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`. Для других проектов может потребоваться адаптация конфигурации. diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..6298b45 --- /dev/null +++ b/ROADMAP.md @@ -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 diff --git a/SECURITY_FEATURES.md b/SECURITY_FEATURES.md new file mode 100644 index 0000000..1617967 --- /dev/null +++ b/SECURITY_FEATURES.md @@ -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` - эта документация + +--- + +**Результат**: Приложение теперь автоматически обеспечивает безопасность соединения в зависимости от конфигурации хоста! diff --git a/VALIDATION_CONFIG.md b/VALIDATION_CONFIG.md new file mode 100644 index 0000000..e1f2d9d --- /dev/null +++ b/VALIDATION_CONFIG.md @@ -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). diff --git a/VERBOSE_PROMPT_EDITING.md b/VERBOSE_PROMPT_EDITING.md new file mode 100644 index 0000000..d67f3b0 --- /dev/null +++ b/VERBOSE_PROMPT_EDITING.md @@ -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. Нажмите "Сохранить" + +## ✅ **Статус:** + +**ГОТОВО** - Редактирование промптов подробности полностью реализовано и протестировано. diff --git a/config/config.go b/config/config.go index cfc3fe4..32f889b 100644 --- a/config/config.go +++ b/config/config.go @@ -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), }, } } diff --git a/main.go b/main.go index 9d38065..f40d270 100644 --- a/main.go +++ b/main.go @@ -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)) +} diff --git a/serve/api.go b/serve/api.go index d121974..73f66b3 100644 --- a/serve/api.go +++ b/serve/api.go @@ -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 { diff --git a/serve/execute.go b/serve/execute.go index 17af41f..d74b09b 100644 --- a/serve/execute.go +++ b/serve/execute.go @@ -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 } diff --git a/serve/execute_page.go b/serve/execute_page.go index 537d75b..01177b3 100644 --- a/serve/execute_page.go +++ b/serve/execute_page.go @@ -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") diff --git a/serve/history.go b/serve/history.go index f62e11c..e35dc25 100644 --- a/serve/history.go +++ b/serve/history.go @@ -151,18 +151,29 @@ func handleHistoryView(w http.ResponseWriter, r *http.Request) { `, 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) } diff --git a/serve/prompts.go b/serve/prompts.go index 8b576a6..9537601 100644 --- a/serve/prompts.go +++ b/serve/prompts.go @@ -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" { diff --git a/serve/results.go b/serve/results.go index b238e21..8766a5c 100644 --- a/serve/results.go +++ b/serve/results.go @@ -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 обрабатывает удаление файла diff --git a/serve/serve.go b/serve/serve.go index aa2dbe0..6b85e70 100644 --- a/serve/serve.go +++ b/serve/serve.go @@ -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) diff --git a/serve/templates/execute.js.go b/serve/templates/execute.js.go index 7131893..feccbb2 100644 --- a/serve/templates/execute.js.go +++ b/serve/templates/execute.js.go @@ -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'); diff --git a/serve/templates/file.go b/serve/templates/file.go index 9879c38..efd4908 100644 --- a/serve/templates/file.go +++ b/serve/templates/file.go @@ -7,13 +7,13 @@ const FileViewTemplate = ` - %s - LCG Results + {{.Filename}} - LCG Results