mirror of
https://github.com/Direct-Dev-Ru/go-lcg.git
synced 2025-11-15 17:20:00 +00:00
add https server functionality - befor auth functionality implementation
This commit is contained in:
@@ -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
232
CONFIG_COMMAND.md
Normal file
@@ -0,0 +1,232 @@
|
||||
# 🔧 Команда config - Управление конфигурацией
|
||||
|
||||
## 📋 Описание
|
||||
|
||||
Команда `config` позволяет просматривать текущую конфигурацию приложения, включая все настройки, переменные окружения и значения по умолчанию.
|
||||
|
||||
## 🚀 Использование
|
||||
|
||||
### Краткий вывод конфигурации (по умолчанию)
|
||||
|
||||
```bash
|
||||
lcg config
|
||||
# или
|
||||
lcg co
|
||||
```
|
||||
|
||||
**Вывод:**
|
||||
|
||||
``` text
|
||||
Provider: ollama
|
||||
Host: http://192.168.87.108:11434/
|
||||
Model: hf.co/yandex/YandexGPT-5-Lite-8B-instruct-GGUF:Q4_K_M
|
||||
Prompt: Reply with linux command and nothing else. Output with plain response - no need formatting. No need explanation. No need code blocks. No need ` symbols.
|
||||
Timeout: 300 seconds
|
||||
```
|
||||
|
||||
### Полный вывод конфигурации
|
||||
|
||||
```bash
|
||||
lcg config --full
|
||||
# или
|
||||
lcg config -f
|
||||
# или
|
||||
lcg co --full
|
||||
# или
|
||||
lcg co -f
|
||||
```
|
||||
|
||||
**Вывод (JSON формат):**
|
||||
|
||||
```json
|
||||
{
|
||||
"cwd": "/home/user/projects/golang/linux-command-gpt",
|
||||
"host": "http://192.168.87.108:11434/",
|
||||
"proxy_url": "/api/v1/protected/sberchat/chat",
|
||||
"completions": "api/chat",
|
||||
"model": "hf.co/yandex/YandexGPT-5-Lite-8B-instruct-GGUF:Q4_K_M",
|
||||
"prompt": "Reply with linux command and nothing else. Output with plain response - no need formatting. No need explanation. No need code blocks. No need ` symbols.",
|
||||
"api_key_file": ".openai_api_key",
|
||||
"result_folder": "/home/user/.config/lcg/gpt_results",
|
||||
"prompt_folder": "/home/user/.config/lcg/gpt_sys_prompts",
|
||||
"provider_type": "ollama",
|
||||
"jwt_token": "***not set***",
|
||||
"prompt_id": "1",
|
||||
"timeout": "300",
|
||||
"result_history": "/home/user/.config/lcg/gpt_results/lcg_history.json",
|
||||
"no_history_env": "",
|
||||
"allow_execution": false,
|
||||
"main_flags": {
|
||||
"file": "",
|
||||
"no_history": false,
|
||||
"sys": "",
|
||||
"prompt_id": 0,
|
||||
"timeout": 0,
|
||||
"debug": false
|
||||
},
|
||||
"server": {
|
||||
"port": "8080",
|
||||
"host": "localhost"
|
||||
},
|
||||
"validation": {
|
||||
"max_system_prompt_length": 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
337
RELEASE_GUIDE.md
Normal file
@@ -0,0 +1,337 @@
|
||||
# 🚀 Гайд по созданию релизов с помощью GoReleaser
|
||||
|
||||
Этот документ описывает процесс создания релизов для проекта `linux-command-gpt` с использованием GoReleaser.
|
||||
|
||||
## 📋 Содержание
|
||||
|
||||
- [Установка GoReleaser](#установка-goreleaser)
|
||||
- [Конфигурация](#конфигурация)
|
||||
- [Процесс создания релиза](#процесс-создания-релиза)
|
||||
- [Автоматизация](#автоматизация)
|
||||
- [Устранение проблем](#устранение-проблем)
|
||||
|
||||
## 🔧 Установка GoReleaser
|
||||
|
||||
### Linux/macOS
|
||||
|
||||
```bash
|
||||
# Скачать и установить последнюю версию
|
||||
curl -sL https://git.io/goreleaser | bash
|
||||
|
||||
# Или через Homebrew (macOS)
|
||||
brew install goreleaser
|
||||
|
||||
# Или через Go
|
||||
go install github.com/goreleaser/goreleaser@latest
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
```powershell
|
||||
# Через Chocolatey
|
||||
choco install goreleaser
|
||||
|
||||
# Или скачать с GitHub Releases
|
||||
# https://github.com/goreleaser/goreleaser/releases
|
||||
```
|
||||
|
||||
## ⚙️ Конфигурация
|
||||
|
||||
### Файл `.goreleaser.yaml`
|
||||
|
||||
В проекте используется следующая конфигурация GoReleaser:
|
||||
|
||||
```yaml
|
||||
version: 2
|
||||
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
- go generate ./...
|
||||
|
||||
builds:
|
||||
- binary: lcg
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
- arm
|
||||
goos:
|
||||
- linux
|
||||
- darwin
|
||||
|
||||
archives:
|
||||
- formats: [tar.gz]
|
||||
name_template: >-
|
||||
{{ .ProjectName }}_
|
||||
{{- title .Os }}_
|
||||
{{- if eq .Arch "amd64" }}x86_64
|
||||
{{- else if eq .Arch "386" }}i386
|
||||
{{- else }}{{ .Arch }}{{ end }}
|
||||
{{- if .Arm }}v{{ .Arm }}{{ end }}
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
formats: [zip]
|
||||
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- "^docs:"
|
||||
- "^test:"
|
||||
|
||||
release:
|
||||
footer: >-
|
||||
---
|
||||
Released by [GoReleaser](https://github.com/goreleaser/goreleaser).
|
||||
```
|
||||
|
||||
### Ключевые настройки
|
||||
|
||||
- **builds**: Сборка для Linux, macOS, Windows (amd64, arm64, arm)
|
||||
- **archives**: Создание архивов tar.gz для Unix и zip для Windows
|
||||
- **changelog**: Автоматическое создание changelog из git commits
|
||||
- **release**: Настройки GitHub релиза
|
||||
|
||||
## 🚀 Процесс создания релиза
|
||||
|
||||
### 1. Подготовка
|
||||
|
||||
```bash
|
||||
# Убедитесь, что все изменения закоммичены
|
||||
git status
|
||||
|
||||
# Обновите версию в VERSION.txt
|
||||
echo "v2.0.2" > VERSION.txt
|
||||
|
||||
# Создайте тег
|
||||
git tag v2.0.2
|
||||
git push origin v2.0.2
|
||||
```
|
||||
|
||||
### 2. Настройка переменных окружения
|
||||
|
||||
```bash
|
||||
# Установите GitHub токен
|
||||
export GITHUB_TOKEN="your_github_token_here"
|
||||
|
||||
# Или создайте файл .env
|
||||
echo "GITHUB_TOKEN=your_github_token_here" > .env
|
||||
```
|
||||
|
||||
### 3. Создание релиза
|
||||
|
||||
#### Полный релиз
|
||||
|
||||
```bash
|
||||
# Создать релиз с загрузкой на GitHub
|
||||
goreleaser release
|
||||
|
||||
# Создать релиз без загрузки (только локально)
|
||||
goreleaser release --clean
|
||||
```
|
||||
|
||||
#### Тестовый релиз (snapshot)
|
||||
|
||||
```bash
|
||||
# Создать тестовую сборку
|
||||
goreleaser release --snapshot
|
||||
|
||||
# Тестовая сборка без загрузки
|
||||
goreleaser release --snapshot --clean
|
||||
```
|
||||
|
||||
### 4. Проверка результатов
|
||||
|
||||
После выполнения команды GoReleaser создаст:
|
||||
|
||||
- **Архивы**: `dist/` - готовые архивы для всех платформ
|
||||
- **Чексуммы**: `dist/checksums.txt` - контрольные суммы файлов
|
||||
- **GitHub релиз**: Автоматически созданный релиз на GitHub
|
||||
|
||||
## 🤖 Автоматизация
|
||||
|
||||
### GitHub Actions
|
||||
|
||||
Создайте файл `.github/workflows/release.yml`:
|
||||
|
||||
```yaml
|
||||
name: Release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.21'
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v4
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: latest
|
||||
args: release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
```
|
||||
|
||||
### Локальные скрипты
|
||||
|
||||
В проекте есть готовые скрипты:
|
||||
|
||||
```bash
|
||||
# Предварительная подготовка
|
||||
./shell-code/pre-release.sh
|
||||
|
||||
# Создание релиза
|
||||
./shell-code/release.sh
|
||||
```
|
||||
|
||||
## 📁 Структура релиза
|
||||
|
||||
После создания релиза в директории `dist/` будут созданы:
|
||||
|
||||
```
|
||||
dist/
|
||||
├── artifacts.json # Метаданные артефактов
|
||||
├── CHANGELOG.md # Автоматически созданный changelog
|
||||
├── config.yaml # Конфигурация GoReleaser
|
||||
├── digests.txt # Хеши файлов
|
||||
├── go-lcg_2.0.1_checksums.txt
|
||||
├── go-lcg_Darwin_arm64.tar.gz
|
||||
├── go-lcg_Darwin_x86_64.tar.gz
|
||||
├── go-lcg_Linux_arm64.tar.gz
|
||||
├── go-lcg_Linux_i386.tar.gz
|
||||
├── go-lcg_Linux_x86_64.tar.gz
|
||||
├── go-lcg_Windows_arm64.zip
|
||||
├── go-lcg_Windows_i386.zip
|
||||
├── go-lcg_Windows_x86_64.zip
|
||||
└── metadata.json # Метаданные релиза
|
||||
```
|
||||
|
||||
## 🔍 Устранение проблем
|
||||
|
||||
### Правильные флаги GoReleaser
|
||||
|
||||
**Важно**: В современных версиях GoReleaser флаг `--skip-publish` больше не поддерживается. Используйте:
|
||||
|
||||
- `--clean` - очищает директорию `dist/` перед сборкой
|
||||
- `--snapshot` - создает тестовую сборку без создания тега
|
||||
- `--debug` - подробный вывод для отладки
|
||||
- `--skip-validate` - пропускает валидацию конфигурации
|
||||
|
||||
### Частые ошибки
|
||||
|
||||
#### 1. Ошибка аутентификации GitHub
|
||||
|
||||
```
|
||||
Error: failed to get GitHub token: missing github token
|
||||
```
|
||||
|
||||
**Решение**: Установите `GITHUB_TOKEN` в переменные окружения.
|
||||
|
||||
#### 2. Ошибка создания тега
|
||||
|
||||
```
|
||||
Error: git tag v1.0.0 already exists
|
||||
```
|
||||
|
||||
**Решение**: Удалите существующий тег или используйте другую версию.
|
||||
|
||||
#### 3. Ошибка сборки
|
||||
|
||||
```
|
||||
Error: failed to build for linux/amd64
|
||||
```
|
||||
|
||||
**Решение**: Проверьте, что код компилируется локально:
|
||||
|
||||
```bash
|
||||
go build -o lcg .
|
||||
```
|
||||
|
||||
### Отладка
|
||||
|
||||
```bash
|
||||
# Подробный вывод
|
||||
goreleaser release --debug
|
||||
|
||||
# Проверка конфигурации
|
||||
goreleaser check
|
||||
|
||||
# Только сборка без релиза
|
||||
goreleaser build
|
||||
|
||||
# Создание релиза без публикации (только локальная сборка)
|
||||
goreleaser release --clean
|
||||
|
||||
# Создание snapshot релиза без публикации
|
||||
goreleaser release --snapshot --clean
|
||||
```
|
||||
|
||||
## 📝 Лучшие практики
|
||||
|
||||
### 1. Версионирование
|
||||
|
||||
- Используйте семантическое версионирование (SemVer)
|
||||
- Обновляйте `VERSION.txt` перед созданием релиза
|
||||
- Создавайте теги в формате `v1.0.0`
|
||||
|
||||
### 2. Changelog
|
||||
|
||||
- Пишите понятные commit messages
|
||||
- Используйте conventional commits для автоматического changelog
|
||||
- Исключайте технические коммиты из changelog
|
||||
|
||||
### 3. Тестирование
|
||||
|
||||
- Всегда тестируйте snapshot релизы перед полным релизом
|
||||
- Проверяйте сборки на разных платформах
|
||||
- Тестируйте установку из релиза
|
||||
|
||||
### 4. Безопасность
|
||||
|
||||
- Никогда не коммитьте токены в репозиторий
|
||||
- Используйте GitHub Secrets для CI/CD
|
||||
- Регулярно обновляйте токены доступа
|
||||
|
||||
## 🎯 Пример полного процесса
|
||||
|
||||
```bash
|
||||
# 1. Подготовка
|
||||
git checkout main
|
||||
git pull origin main
|
||||
|
||||
# 2. Обновление версии
|
||||
echo "v2.0.2" > VERSION.txt
|
||||
git add VERSION.txt
|
||||
git commit -m "chore: bump version to v2.0.2"
|
||||
|
||||
# 3. Создание тега
|
||||
git tag v2.0.2
|
||||
git push origin v2.0.2
|
||||
|
||||
# 4. Создание релиза
|
||||
export GITHUB_TOKEN="your_token"
|
||||
goreleaser release
|
||||
|
||||
# 5. Проверка
|
||||
ls -la dist/
|
||||
```
|
||||
|
||||
## 📚 Дополнительные ресурсы
|
||||
|
||||
- [Официальная документация GoReleaser](https://goreleaser.com/)
|
||||
- [Примеры конфигураций](https://github.com/goreleaser/goreleaser/tree/main/examples)
|
||||
- [GitHub Actions для GoReleaser](https://github.com/goreleaser/goreleaser-action)
|
||||
|
||||
---
|
||||
|
||||
**Примечание**: Этот гайд создан специально для проекта `linux-command-gpt`. Для других проектов может потребоваться адаптация конфигурации.
|
||||
118
ROADMAP.md
Normal file
118
ROADMAP.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# Дорожная карта развития (функциональность и безопасность)
|
||||
|
||||
Документ описывает план развития проекта на ближайшие релизы с фокусом на улучшение функциональности и усиление безопасности.
|
||||
|
||||
## Принципы
|
||||
|
||||
- Безопасность по умолчанию: новые возможности включают безопасные дефолты, опционально ослабляются.
|
||||
- Обратная совместимость: не ломать существующие сценарии CLI и API.
|
||||
- Прозрачность: чёткие Changelog, версии по SemVer, миграции и откаты.
|
||||
- Качество: тесты, линтеры, аудит зависимостей, автоматизация релизов.
|
||||
|
||||
## Вехи и цели
|
||||
|
||||
### v2.1.0 — Формализация API и UX улучшения
|
||||
|
||||
- REST API
|
||||
- Описать `POST /execute` в OpenAPI (swagger.yaml/json) и приложить в репозитории.
|
||||
- Валидация входа по схеме: обязательные поля, ограничения длины, лимит размера тела.
|
||||
- Явные коды ошибок и структура ответа (коды/сообщения).
|
||||
- Безопасность API (первый этап)
|
||||
- Дополнить защиту: ограничение размера тела (например, 64KB), тайм-ауты на чтение/запись.
|
||||
- Rate limit (встроенный простой токен-бакет, по IP). Конфиг через env.
|
||||
- Логирование попыток доступа и ошибок API (с редактированием PII).
|
||||
- Веб-интерфейс
|
||||
- Улучшения мобильной версии (доступность, контраст, a11y-метки).
|
||||
- Переключатель темы (light/dark), сохранение предпочтений.
|
||||
- Промпты
|
||||
- Экспорт/импорт системных промптов (JSON) из UI/CLI.
|
||||
- Превью при редактировании промптов в UI.
|
||||
- Документация
|
||||
- `API_GUIDE.md`: синхронизировать с OpenAPI.
|
||||
- `USAGE_GUIDE.md`: добавить раздел «Ограничения API и лимиты».
|
||||
|
||||
### v2.2.0 — Усиление безопасности и управление доступом
|
||||
|
||||
- Аутентификация/Авторизация для веб-сервера
|
||||
- Ввести токен доступа для API: `LCG_SERVER_TOKEN` (Bearer), отключаемо.
|
||||
- Сессии UI (опционально): cookie HttpOnly + SameSite=strict, CSRF-защита форм.
|
||||
- CORS: явный список разрешённых Origin через `LCG_CORS_ORIGINS`.
|
||||
- Транспорт и заголовки безопасности
|
||||
- Рекомендации по TLS терминации (пример конфигов nginx/caddy) в `serve/README.md`.
|
||||
- Security headers: CSP, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, HSTS (при HTTPS).
|
||||
- Хранилище и история
|
||||
- Опциональное шифрование истории на диске (`LCG_HISTORY_ENCRYPTION_KEY_FILE`).
|
||||
- Права на файлы истории и результатов: 0600, директории 0700.
|
||||
- Настраиваемая ретенция истории (дни/размер). Авто-очистка.
|
||||
- Наблюдаемость
|
||||
- Аудит-лог действий UI/API с маскированием чувствительных данных.
|
||||
- Включаемые/отключаемые метрики (prometheus endpoint — опционально, по отдельному порту/токену).
|
||||
|
||||
### v2.3.0 — Расширяемость и производительность
|
||||
|
||||
- Плагины провайдеров
|
||||
- Интерфейс адаптеров (провайдеры LLM), регистрация через конфиг/билд-теги.
|
||||
- Образцы адаптеров и гайды по разработке.
|
||||
- Производительность
|
||||
- Пулы HTTP-клиентов, connection reuse, упреждающие таймауты на уровне контекста.
|
||||
- Кэширование результатов подробных объяснений (опционально, по ключу запроса).
|
||||
- Расширения API
|
||||
- Пакетная обработка запросов (batch) с квотой.
|
||||
- Пагинация и фильтрация для `/history` (если будет публичный REST).
|
||||
- Дистрибуция
|
||||
- Улучшения .goreleaser: публикация SBOM, подписи (cosign), детерминированные сборки.
|
||||
- Готовые пакеты: deb/rpm, инструкции для brew/scoop (по возможности).
|
||||
|
||||
## Backlog (кандидаты)
|
||||
|
||||
- Потоковая генерация (stream) и WebSocket-канал (при наличии поддержки у провайдеров).
|
||||
- Оффлайн-режим/кэширование моделей для локальных провайдеров.
|
||||
- Расширенный поиск по результатам/истории, теги и сохранённые фильтры.
|
||||
- Резервное копирование и восстановление каталога результатов/истории.
|
||||
- Улучшение доступности (a11y), горячие клавиши, локализация интерфейса.
|
||||
|
||||
## Техническое качество
|
||||
|
||||
- Обновление стека
|
||||
- Обновить Go (минимум 1.20+), пересобрать и протестировать совместимость.
|
||||
- Регулярные обновления зависимостей и проверка уязвимостей (`govulncheck`).
|
||||
- Линтеры и проверка качества
|
||||
- Включить `golangci-lint`, `staticcheck`, `gosec` в CI.
|
||||
- Форматирование и единый стиль, pre-commit хуки.
|
||||
- Тесты
|
||||
- Unit-тесты на `serve/*` (маршруты, валидация входных данных, заголовки).
|
||||
- Интеграционные тесты API `/execute` (позитив/негатив, лимиты, токены).
|
||||
- Фаззинг критичных функций парсинга/валидации.
|
||||
- CI/CD
|
||||
- GitHub Actions: сборка, тесты, линты, релизы. Генерация чек-сумм, подписи, SBOM.
|
||||
- Автоматическая публикация релизов и проверок артефактов.
|
||||
|
||||
## Конфигурация (новые/уточняемые переменные)
|
||||
|
||||
- `LCG_SERVER_TOKEN` — токен доступа для API (Bearer). Отключаемый режим.
|
||||
- `LCG_RATE_LIMIT` — глобальные лимиты (например, `60/m`, `5/s`).
|
||||
- `LCG_CORS_ORIGINS` — список разрешённых Origin.
|
||||
- `LCG_HISTORY_ENCRYPTION_KEY_FILE` — путь к ключу для шифрования истории (опц.).
|
||||
- `LCG_MAX_BODY_BYTES` — максимальный размер тела запроса, байты (по умолчанию 65536).
|
||||
- `LCG_BROWSER_PATH` — путь к браузеру для `--browser`.
|
||||
|
||||
## Политика релизов
|
||||
|
||||
- SemVer: MINOR — функционал без ломаний, PATCH — багфиксы/мелкие улучшения.
|
||||
- Каждый релиз: обновлённый `CHANGELOG.txt`, теги `vX.Y.Z`, двуязычная документация (RU/EN при возможности).
|
||||
- Security Advisories: отдельный раздел/ISSUE шаблон для отчётов об уязвимостях.
|
||||
|
||||
## Критерии приемки (примеры)
|
||||
|
||||
- v2.1.0: OpenAPI спецификация доступна, API валидируется по схеме, лимит размера тела и таймауты соблюдаются, добавлены тесты в CI.
|
||||
- v2.2.0: Доступ к API с токеном включаем/отключаем через env; активированы security-заголовки; есть базовые правила CORS; аудит-лог включается флагом.
|
||||
- v2.3.0: Пулы клиентов, бенчмарки показывают улучшение p95 латентности, есть механизм подключения новых провайдеров.
|
||||
|
||||
## Риски и смягчение
|
||||
|
||||
- Ломание совместимости при усилении безопасности → режим совместимости через env/флаги.
|
||||
- Рост сложности конфигурации → шаблоны конфигов и «рецепты» в README/serve/README.md.
|
||||
- Производительные регрессии из-за валидации/лимитов → профилирование и кэширование на горячих путях.
|
||||
|
||||
---
|
||||
Последнее обновление: 2025-10-22
|
||||
199
SECURITY_FEATURES.md
Normal file
199
SECURITY_FEATURES.md
Normal file
@@ -0,0 +1,199 @@
|
||||
# 🔒 Функции безопасности LCG
|
||||
|
||||
## 🛡️ Автоматическое принуждение к HTTPS
|
||||
|
||||
### Логика безопасности
|
||||
|
||||
Приложение автоматически определяет, нужно ли использовать HTTPS:
|
||||
|
||||
1. **Небезопасные хосты** (не localhost/127.0.0.1) → **принудительно HTTPS**
|
||||
2. **Безопасные хосты** (localhost/127.0.0.1) → HTTP (если не указано иное)
|
||||
3. **Переменная `LCG_SERVER_ALLOW_HTTP=true`** → разрешает HTTP для любых хостов
|
||||
|
||||
### Примеры
|
||||
|
||||
```bash
|
||||
# Небезопасно - принудительно HTTPS
|
||||
LCG_SERVER_HOST=192.168.1.100 lcg serve
|
||||
# Результат: https://192.168.1.100:8080
|
||||
|
||||
# Безопасно - HTTP по умолчанию
|
||||
LCG_SERVER_HOST=localhost lcg serve
|
||||
# Результат: http://localhost:8080
|
||||
|
||||
# Принудительно HTTP для любого хоста
|
||||
LCG_SERVER_HOST=192.168.1.100 LCG_SERVER_ALLOW_HTTP=true lcg serve
|
||||
# Результат: http://192.168.1.100:8080
|
||||
```
|
||||
|
||||
## 🔐 SSL/TLS сертификаты
|
||||
|
||||
### Автоматическая генерация
|
||||
|
||||
Приложение автоматически генерирует самоподписанный сертификат если:
|
||||
|
||||
1. Не указаны переменные `LCG_SERVER_SSL_CERT_FILE` и `LCG_SERVER_SSL_KEY_FILE`
|
||||
2. Не найдены файлы в `~/.config/lcg/server/ssl/cert.pem` и `~/.config/lcg/server/ssl/key.pem`
|
||||
|
||||
### Расположение сертификатов
|
||||
|
||||
``` text
|
||||
~/.config/lcg/
|
||||
├── config/
|
||||
│ └── server/
|
||||
│ └── ssl/
|
||||
│ ├── cert.pem # Сертификат
|
||||
│ └── key.pem # Приватный ключ
|
||||
```
|
||||
|
||||
### Переменные окружения
|
||||
|
||||
| Переменная | Описание | По умолчанию |
|
||||
|------------|----------|--------------|
|
||||
| `LCG_CONFIG_FOLDER` | Папка конфигурации | `~/.config/lcg/config` |
|
||||
| `LCG_SERVER_ALLOW_HTTP` | Разрешить HTTP для любых хостов | `false` |
|
||||
| `LCG_SERVER_SSL_CERT_FILE` | Путь к сертификату | `""` (авто) |
|
||||
| `LCG_SERVER_SSL_KEY_FILE` | Путь к ключу | `""` (авто) |
|
||||
|
||||
## 🚀 Примеры использования
|
||||
|
||||
### Безопасный режим (по умолчанию)
|
||||
|
||||
```bash
|
||||
# Локальный сервер - HTTP
|
||||
lcg serve
|
||||
|
||||
# Внешний сервер - принудительно HTTPS
|
||||
LCG_SERVER_HOST=192.168.1.100 lcg serve
|
||||
```
|
||||
|
||||
### Настройка SSL сертификатов
|
||||
|
||||
```bash
|
||||
# Использовать собственные сертификаты
|
||||
LCG_SERVER_SSL_CERT_FILE=/path/to/cert.pem \
|
||||
LCG_SERVER_SSL_KEY_FILE=/path/to/key.pem \
|
||||
lcg serve
|
||||
|
||||
# Разрешить HTTP для внешних хостов
|
||||
LCG_SERVER_HOST=192.168.1.100 \
|
||||
LCG_SERVER_ALLOW_HTTP=true \
|
||||
lcg serve
|
||||
```
|
||||
|
||||
### Docker контейнер
|
||||
|
||||
```dockerfile
|
||||
FROM golang:1.21-alpine AS builder
|
||||
# ... build steps ...
|
||||
|
||||
FROM alpine:latest
|
||||
COPY --from=builder /app/lcg /usr/local/bin/
|
||||
ENV LCG_SERVER_HOST=0.0.0.0
|
||||
ENV LCG_SERVER_ALLOW_HTTP=false
|
||||
CMD ["lcg", "serve"]
|
||||
```
|
||||
|
||||
### Systemd сервис
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=LCG Server
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=lcg
|
||||
WorkingDirectory=/opt/lcg
|
||||
ExecStart=/opt/lcg/lcg serve
|
||||
Environment=LCG_SERVER_HOST=0.0.0.0
|
||||
Environment=LCG_SERVER_ALLOW_HTTP=false
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
## 🔧 Технические детали
|
||||
|
||||
### Генерация сертификата
|
||||
|
||||
Самоподписанный сертификат генерируется с:
|
||||
|
||||
- **Размер ключа**: 2048 бит RSA
|
||||
- **Срок действия**: 1 год
|
||||
- **Поддерживаемые хосты**: localhost, 127.0.0.1, указанный хост
|
||||
- **Использование**: Server Authentication
|
||||
|
||||
### Безопасные хосты
|
||||
|
||||
Следующие хосты считаются безопасными для HTTP:
|
||||
|
||||
- `localhost`
|
||||
- `127.0.0.1`
|
||||
- `::1` (IPv6 localhost)
|
||||
|
||||
### Проверка безопасности
|
||||
|
||||
```go
|
||||
// Проверка хоста
|
||||
if !ssl.IsSecureHost(host) {
|
||||
// Принудительно HTTPS
|
||||
useHTTPS = true
|
||||
}
|
||||
|
||||
// Проверка разрешения HTTP
|
||||
if config.AppConfig.Server.AllowHTTP {
|
||||
useHTTPS = false
|
||||
}
|
||||
```
|
||||
|
||||
## 🛠️ Отладка
|
||||
|
||||
### Проверка конфигурации
|
||||
|
||||
```bash
|
||||
# Показать текущую конфигурацию
|
||||
lcg config --full | jq '.server'
|
||||
|
||||
# Проверить SSL сертификаты
|
||||
ls -la ~/.config/lcg/config/server/ssl/
|
||||
|
||||
# Проверить переменные окружения
|
||||
env | grep LCG_SERVER
|
||||
```
|
||||
|
||||
### Логи безопасности
|
||||
|
||||
```bash
|
||||
# Запуск с отладкой
|
||||
LCG_SERVER_HOST=192.168.1.100 lcg serve --debug
|
||||
|
||||
# Проверка SSL
|
||||
openssl x509 -in ~/.config/lcg/config/server/ssl/cert.pem -text -noout
|
||||
```
|
||||
|
||||
## ⚠️ Важные замечания
|
||||
|
||||
### Безопасность
|
||||
|
||||
1. **Самоподписанные сертификаты** - браузеры будут показывать предупреждение
|
||||
2. **Продакшен** - используйте настоящие SSL сертификаты от CA
|
||||
3. **Сетевой доступ** - HTTPS защищает трафик, но не аутентификацию
|
||||
|
||||
### Производительность
|
||||
|
||||
1. **HTTPS** - небольшая нагрузка на CPU для шифрования
|
||||
2. **Сертификаты** - генерируются один раз, затем кэшируются
|
||||
3. **Память** - сертификаты загружаются в память при запуске
|
||||
|
||||
## 📚 Связанные файлы
|
||||
|
||||
- `config/config.go` - конфигурация безопасности
|
||||
- `ssl/ssl.go` - генерация и управление сертификатами
|
||||
- `serve/serve.go` - HTTP/HTTPS сервер
|
||||
- `SECURITY_FEATURES.md` - эта документация
|
||||
|
||||
---
|
||||
|
||||
**Результат**: Приложение теперь автоматически обеспечивает безопасность соединения в зависимости от конфигурации хоста!
|
||||
205
VALIDATION_CONFIG.md
Normal file
205
VALIDATION_CONFIG.md
Normal 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
63
VERBOSE_PROMPT_EDITING.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# Редактирование промптов подробности
|
||||
|
||||
## 🎯 Реализованная функциональность
|
||||
|
||||
### ✅ **Что добавлено:**
|
||||
|
||||
1. **Функция редактирования в JavaScript:**
|
||||
- `editVerbosePrompt(mode, content)` - открывает форму редактирования для промптов подробности
|
||||
- Автоматически заполняет поля формы данными промпта
|
||||
- Показывает режим в заголовке формы
|
||||
|
||||
2. **Обработчик на сервере:**
|
||||
- `handleEditVerbosePrompt()` - новый обработчик для маршрута `/prompts/edit-verbose/`
|
||||
- Поддерживает режимы: `v`, `vv`, `vvv`
|
||||
- Валидация всех полей с использованием `validation` пакета
|
||||
- Обновление промптов через `PromptManager`
|
||||
|
||||
3. **Маршрутизация:**
|
||||
- Добавлен маршрут `/prompts/edit-verbose/` в `serve.go`
|
||||
- Поддержка HTTP методов PUT
|
||||
- Интеграция с существующей системой маршрутов
|
||||
|
||||
### 🔧 **Как работает:**
|
||||
|
||||
1. **Пользователь нажимает кнопку "✏️"** на промпте подробности
|
||||
2. **JavaScript вызывает** `editVerbosePrompt(mode, content)`
|
||||
3. **Форма открывается** с заполненными полями
|
||||
4. **При сохранении** отправляется PUT запрос на `/prompts/edit-verbose/{mode}`
|
||||
5. **Сервер обрабатывает** запрос через `handleEditVerbosePrompt()`
|
||||
6. **Промпт обновляется** в файловой системе
|
||||
7. **Страница перезагружается** с обновленными данными
|
||||
|
||||
### 📋 **Поддерживаемые режимы:**
|
||||
|
||||
- **`v`** → ID 6 (базовый verbose)
|
||||
- **`vv`** → ID 7 (средний verbose)
|
||||
- **`vvv`** → ID 8 (максимальный verbose)
|
||||
|
||||
### 🛡️ **Валидация:**
|
||||
|
||||
- **Содержимое:** максимум символов из `LCG_MAX_SYSTEM_PROMPT_LENGTH`
|
||||
- **Название:** максимум символов из `LCG_MAX_PROMPT_NAME_LENGTH`
|
||||
- **Описание:** максимум символов из `LCG_MAX_PROMPT_DESC_LENGTH`
|
||||
|
||||
### 🎨 **UI/UX:**
|
||||
|
||||
- **Единая форма** для редактирования всех типов промптов
|
||||
- **Автоматическое определение** типа промпта (системный/verbose)
|
||||
- **Правильная маршрутизация** запросов
|
||||
- **Валидация на клиенте** и сервере
|
||||
- **Отзывчивый дизайн** для мобильных устройств
|
||||
|
||||
## 🚀 **Использование:**
|
||||
|
||||
1. Откройте страницу `/prompts`
|
||||
2. Перейдите на вкладку "📝 Промпты подробности"
|
||||
3. Нажмите кнопку "✏️" на нужном промпте
|
||||
4. Отредактируйте содержимое
|
||||
5. Нажмите "Сохранить"
|
||||
|
||||
## ✅ **Статус:**
|
||||
|
||||
**ГОТОВО** - Редактирование промптов подробности полностью реализовано и протестировано.
|
||||
@@ -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
187
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))
|
||||
}
|
||||
|
||||
37
serve/api.go
37
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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
118
serve/prompts.go
118
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" {
|
||||
|
||||
@@ -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 обрабатывает удаление файла
|
||||
|
||||
143
serve/serve.go
143
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)
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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; }
|
||||
|
||||
7
shell-code/run-proxy-max.sh
Normal file
7
shell-code/run-proxy-max.sh
Normal file
@@ -0,0 +1,7 @@
|
||||
#! /usr/bin/bash
|
||||
|
||||
LCG_PROVIDER=proxy LCG_HOST=https://direct-dev.ru \
|
||||
LCG_MODEL=GigaChat-2-Max \
|
||||
LCG_JWT_TOKEN=$(go-ansible-vault --key $(cat ~/.config/gak) -i ~/.config/jwt.direct-dev.ru get -m JWT_TOKEN -q) \
|
||||
go run . $1 $2 $3 $4 $5 $6 $7 $8 $9
|
||||
|
||||
7
shell-code/run-proxy.sh
Normal file
7
shell-code/run-proxy.sh
Normal file
@@ -0,0 +1,7 @@
|
||||
#! /usr/bin/bash
|
||||
|
||||
LCG_PROVIDER=proxy LCG_HOST=https://direct-dev.ru \
|
||||
LCG_MODEL=GigaChat-2 \
|
||||
LCG_JWT_TOKEN=$(go-ansible-vault --key $(cat ~/.config/gak) -i ~/.config/jwt.direct-dev.ru get -m JWT_TOKEN -q) \
|
||||
go run . $1 $2 $3 $4 $5 $6 $7 $8 $9
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
|
||||
# shellcheck disable=SC2034
|
||||
LCG_PROVIDER=proxy LCG_HOST=http://localhost:8080 LCG_MODEL=GigaChat-2-Max LCG_JWT_TOKEN=$(go-ansible-vault -a -i shell-code/jwt.admin.token get -m 'JWT_TOKEN' -q) go run . $1 $2 $3 $4 $5 $6 $7 $8 $9
|
||||
|
||||
LCG_PROVIDER=proxy LCG_HOST=https://direct-dev.ru LCG_MODEL=GigaChat-2-Max LCG_JWT_TOKEN=$(go-ansible-vault --key $(cat ~/.config/gak) -i ~/.config/jwt.direct-dev.ru get -m 'JWT_TOKEN' -q) go run . [your question here]
|
||||
164
ssl/ssl.go
Normal file
164
ssl/ssl.go
Normal 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
154
validation/validation.go
Normal file
@@ -0,0 +1,154 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||
)
|
||||
|
||||
// ValidationError представляет ошибку валидации
|
||||
type ValidationError struct {
|
||||
Field string
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e ValidationError) Error() string {
|
||||
return fmt.Sprintf("%s: %s", e.Field, e.Message)
|
||||
}
|
||||
|
||||
// ValidateSystemPrompt проверяет длину системного промпта
|
||||
func ValidateSystemPrompt(prompt string) error {
|
||||
maxLen := config.AppConfig.Validation.MaxSystemPromptLength
|
||||
if len(prompt) > maxLen {
|
||||
return ValidationError{
|
||||
Field: "system_prompt",
|
||||
Message: fmt.Sprintf("системный промпт слишком длинный: %d символов (максимум %d)", len(prompt), maxLen),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateUserMessage проверяет длину пользовательского сообщения
|
||||
func ValidateUserMessage(message string) error {
|
||||
maxLen := config.AppConfig.Validation.MaxUserMessageLength
|
||||
if len(message) > maxLen {
|
||||
return ValidationError{
|
||||
Field: "user_message",
|
||||
Message: fmt.Sprintf("пользовательское сообщение слишком длинное: %d символов (максимум %d)", len(message), maxLen),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidatePromptAndMessage проверяет и системный промпт, и пользовательское сообщение
|
||||
func ValidatePromptAndMessage(systemPrompt, userMessage string) error {
|
||||
if err := ValidateSystemPrompt(systemPrompt); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ValidateUserMessage(userMessage); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TruncateSystemPrompt обрезает системный промпт до максимальной длины
|
||||
func TruncateSystemPrompt(prompt string) string {
|
||||
maxLen := config.AppConfig.Validation.MaxSystemPromptLength
|
||||
if len(prompt) <= maxLen {
|
||||
return prompt
|
||||
}
|
||||
return prompt[:maxLen]
|
||||
}
|
||||
|
||||
// TruncateUserMessage обрезает пользовательское сообщение до максимальной длины
|
||||
func TruncateUserMessage(message string) string {
|
||||
maxLen := config.AppConfig.Validation.MaxUserMessageLength
|
||||
if len(message) <= maxLen {
|
||||
return message
|
||||
}
|
||||
return message[:maxLen]
|
||||
}
|
||||
|
||||
// GetSystemPromptLength возвращает длину системного промпта
|
||||
func GetSystemPromptLength(prompt string) int {
|
||||
return len(prompt)
|
||||
}
|
||||
|
||||
// GetUserMessageLength возвращает длину пользовательского сообщения
|
||||
func GetUserMessageLength(message string) int {
|
||||
return len(message)
|
||||
}
|
||||
|
||||
// FormatLengthInfo форматирует информацию о длине для отображения
|
||||
func FormatLengthInfo(systemPrompt, userMessage string) string {
|
||||
systemLen := GetSystemPromptLength(systemPrompt)
|
||||
userLen := GetUserMessageLength(userMessage)
|
||||
maxSystemLen := config.AppConfig.Validation.MaxSystemPromptLength
|
||||
maxUserLen := config.AppConfig.Validation.MaxUserMessageLength
|
||||
|
||||
var warnings []string
|
||||
|
||||
if systemLen > maxSystemLen {
|
||||
warnings = append(warnings, fmt.Sprintf("⚠️ Системный промпт превышает лимит: %d/%d символов", systemLen, maxSystemLen))
|
||||
}
|
||||
|
||||
if userLen > maxUserLen {
|
||||
warnings = append(warnings, fmt.Sprintf("⚠️ Пользовательское сообщение превышает лимит: %d/%d символов", userLen, maxUserLen))
|
||||
}
|
||||
|
||||
if len(warnings) == 0 {
|
||||
return fmt.Sprintf("✅ Длины в пределах нормы: системный промпт %d/%d, сообщение %d/%d",
|
||||
systemLen, maxSystemLen, userLen, maxUserLen)
|
||||
}
|
||||
|
||||
return strings.Join(warnings, "\n")
|
||||
}
|
||||
|
||||
// ValidatePromptName проверяет длину названия промпта
|
||||
func ValidatePromptName(name string) error {
|
||||
maxLen := config.AppConfig.Validation.MaxPromptNameLength
|
||||
if len(name) > maxLen {
|
||||
return ValidationError{
|
||||
Field: "prompt_name",
|
||||
Message: fmt.Sprintf("название промпта слишком длинное: %d символов (максимум %d)", len(name), maxLen),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidatePromptDescription проверяет длину описания промпта
|
||||
func ValidatePromptDescription(description string) error {
|
||||
maxLen := config.AppConfig.Validation.MaxPromptDescLength
|
||||
if len(description) > maxLen {
|
||||
return ValidationError{
|
||||
Field: "prompt_description",
|
||||
Message: fmt.Sprintf("описание промпта слишком длинное: %d символов (максимум %d)", len(description), maxLen),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateCommand проверяет длину команды
|
||||
func ValidateCommand(command string) error {
|
||||
maxLen := config.AppConfig.Validation.MaxCommandLength
|
||||
if len(command) > maxLen {
|
||||
return ValidationError{
|
||||
Field: "command",
|
||||
Message: fmt.Sprintf("команда слишком длинная: %d символов (максимум %d)", len(command), maxLen),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateExplanation проверяет длину объяснения
|
||||
func ValidateExplanation(explanation string) error {
|
||||
maxLen := config.AppConfig.Validation.MaxExplanationLength
|
||||
if len(explanation) > maxLen {
|
||||
return ValidationError{
|
||||
Field: "explanation",
|
||||
Message: fmt.Sprintf("объяснение слишком длинное: %d символов (максимум %d)", len(explanation), maxLen),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user