mirror of
https://github.com/Direct-Dev-Ru/go-lcg.git
synced 2025-11-16 09:39:56 +00:00
Compare commits
6 Commits
lcg.v2.0.0
...
v2.0.2
| Author | SHA1 | Date | |
|---|---|---|---|
| c70effda73 | |||
| 611bd17ac1 | |||
| e1bd79db8c | |||
| 3e1cb1e078 | |||
| e37599d3ef | |||
| 344f763bb4 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -17,3 +17,4 @@ gpt_results
|
|||||||
shell-code/jwt.admin.token
|
shell-code/jwt.admin.token
|
||||||
run.sh
|
run.sh
|
||||||
lcg_history.json
|
lcg_history.json
|
||||||
|
deploy/0.create_sealed_secrets.sh
|
||||||
|
|||||||
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"djlint.showInstallError": false
|
||||||
|
}
|
||||||
152
CHANGELOG.txt
Normal file
152
CHANGELOG.txt
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
CHANGELOG
|
||||||
|
=========
|
||||||
|
|
||||||
|
Версия 2.0.1 (2025-10-22)
|
||||||
|
=========================
|
||||||
|
|
||||||
|
## ✨ ИЗМЕНЕНИЯ И НОВОЕ
|
||||||
|
|
||||||
|
- 📱 Веб‑интерфейс: улучшена мобильная адаптация (кнопки, шрифты, отступы, верстка)
|
||||||
|
- 🧭 Учет `prefers-reduced-motion` для снижения анимаций при необходимости
|
||||||
|
- 🔌 Добавлен публичный REST эндпоинт `POST /execute` для программного доступа
|
||||||
|
- ⚠️ Ограничение безопасности: принимаются только запросы от `curl` (User-Agent)
|
||||||
|
- 📖 Подробности и примеры: см. `API_GUIDE.md`
|
||||||
|
|
||||||
|
## 🐛 ИСПРАВЛЕНИЯ И УЛУЧШЕНИЯ
|
||||||
|
|
||||||
|
- 🔧 Рефакторинг HTTP‑сервера: вынесен в пакет `serve/*` для лучшей поддерживаемости
|
||||||
|
- 🗑️ Удалены устаревшие файлы: `cmd/serve.go`, `_main.go`
|
||||||
|
- 🛠️ Скрипты релиза: небольшие правки в `shell-code/pre-release.sh` и `shell-code/release.py`
|
||||||
|
|
||||||
|
## 📚 ДОКУМЕНТАЦИЯ
|
||||||
|
|
||||||
|
- ➕ Добавлен `API_GUIDE.md` (описание `/execute`, примеры `curl`/Python)
|
||||||
|
- 📝 Обновлены `README.md` и `USAGE_GUIDE.md` (актуализация про мобильную версию и API)
|
||||||
|
- 🔗 В `API_CONTRACT.md` добавлена ссылка на `API_GUIDE.md` (HTTP API веб‑сервера)
|
||||||
|
|
||||||
|
## ♻️ СОВМЕСТИМОСТЬ
|
||||||
|
|
||||||
|
- ✅ Обратная совместимость сохранена; CLI и провайдеры (proxy/ollama) без изменений
|
||||||
|
- 🚫 Миграция не требуется
|
||||||
|
|
||||||
|
Версия 2.0.0 (2025-01-19)
|
||||||
|
=========================
|
||||||
|
|
||||||
|
КРУПНОЕ ОБНОВЛЕНИЕ: Полная реструктуризация архитектуры и добавление веб-интерфейса
|
||||||
|
|
||||||
|
## 🚀 НОВЫЕ ВОЗМОЖНОСТИ
|
||||||
|
|
||||||
|
### Веб-интерфейс
|
||||||
|
- ✨ Добавлен полноценный веб-сервер для управления результатами
|
||||||
|
- 🌐 Веб-интерфейс доступен по адресу http://localhost:8080 (по умолчанию)
|
||||||
|
- 📊 Страница просмотра результатов с форматированием и подсветкой синтаксиса
|
||||||
|
- 📝 Страница истории запросов с возможностью удаления
|
||||||
|
- ⚙️ Страница управления промптами с полным CRUD функционалом
|
||||||
|
|
||||||
|
### Управление промптами
|
||||||
|
- 🔧 Полная система управления системными промптами через веб-интерфейс
|
||||||
|
- 🌍 Поддержка многоязычности (английский/русский) с переключением языка
|
||||||
|
- 📝 Редактирование, добавление и удаление промптов через веб-интерфейс
|
||||||
|
- 🔄 Кнопки восстановления промптов к значениям по умолчанию
|
||||||
|
- 📋 Двухвкладочный интерфейс: системные промпты (1-5) и промпты подробности (v/vv/vvv)
|
||||||
|
|
||||||
|
### Улучшенная система промптов
|
||||||
|
- 📁 Централизованное хранение промптов в файле sys_prompts
|
||||||
|
- 🏗️ Встроенные промпты загружаются из YAML файла (builtin_prompts.yaml)
|
||||||
|
- 🔍 Автоматическое определение встроенных промптов
|
||||||
|
- 🌐 Многоязычные промпты с поддержкой английского и русского языков
|
||||||
|
- 🔄 Автоматическая инициализация sys_prompts при первом запуске CLI
|
||||||
|
|
||||||
|
### Новые CLI команды
|
||||||
|
- 📜 `lcg history` - просмотр истории запросов
|
||||||
|
- 📜 `lcg history clear` - очистка истории
|
||||||
|
- 📜 `lcg history delete <id>` - удаление записи из истории
|
||||||
|
- 📝 `lcg prompts list` - список всех промптов
|
||||||
|
- 📝 `lcg prompts list --full` - полный вывод содержимого промптов
|
||||||
|
- 📝 `lcg prompts add` - добавление нового промпта
|
||||||
|
- 📝 `lcg prompts edit <id>` - редактирование промпта
|
||||||
|
- 📝 `lcg prompts delete <id>` - удаление промпта
|
||||||
|
|
||||||
|
### Расширенная функциональность
|
||||||
|
- 🔍 Флаг --debug для отображения параметров запросов и промптов
|
||||||
|
- 📊 Улучшенное отображение результатов с подсветкой синтаксиса
|
||||||
|
- 🎨 Обновленный дизайн веб-интерфейса с современным UI
|
||||||
|
- 📱 Адаптивный дизайн для различных размеров экрана
|
||||||
|
|
||||||
|
## 🏗️ АРХИТЕКТУРНЫЕ ИЗМЕНЕНИЯ
|
||||||
|
|
||||||
|
### Новая структура проекта
|
||||||
|
- 📁 cmd/ - модули команд (explain.go, history.go, serve.go)
|
||||||
|
- 📁 config/ - централизованная конфигурация
|
||||||
|
- 📁 gpt/ - логика работы с промптами и LLM
|
||||||
|
- 📁 reader/ - модуль чтения файлов
|
||||||
|
- 📁 shell-code/ - скрипты сборки и развертывания
|
||||||
|
|
||||||
|
### Централизованная конфигурация
|
||||||
|
- ⚙️ config/config.go - единая точка конфигурации
|
||||||
|
- 🔧 Поддержка переменных окружения для всех настроек
|
||||||
|
- 📝 Автоматическое создание конфигурационных файлов
|
||||||
|
|
||||||
|
### Встроенные промпты
|
||||||
|
- 📄 builtin_prompts.yaml - YAML файл с встроенными промптами
|
||||||
|
- 🔧 builtin_prompts.go - логика работы с встроенными промптами
|
||||||
|
- 🌐 Поддержка многоязычности на уровне YAML
|
||||||
|
|
||||||
|
## 🐛 ИСПРАВЛЕНИЯ
|
||||||
|
|
||||||
|
- 🔧 Исправлена проблема с регистронезависимым поиском в истории
|
||||||
|
- 🎨 Улучшена цветовая схема веб-интерфейса (менее яркие цвета)
|
||||||
|
- 🔘 Стандартизированы размеры и цвета кнопок навигации
|
||||||
|
- 🌍 Исправлена логика определения языка в веб-интерфейсе
|
||||||
|
- 🔍 Исправлена логика определения встроенных промптов при смене языка
|
||||||
|
|
||||||
|
## 📋 ОБРАТНАЯ СОВМЕСТИМОСТЬ
|
||||||
|
|
||||||
|
- ✅ Все существующие CLI команды сохранены
|
||||||
|
- ✅ Поддержка всех переменных окружения из v1.x.x
|
||||||
|
- ✅ Автоматическая миграция существующих конфигураций
|
||||||
|
- ✅ Сохранена совместимость с существующими API
|
||||||
|
|
||||||
|
## 🔧 ТЕХНИЧЕСКИЕ УЛУЧШЕНИЯ
|
||||||
|
|
||||||
|
### Производительность
|
||||||
|
- ⚡ Оптимизирована загрузка промптов
|
||||||
|
- 🚀 Улучшена скорость работы веб-интерфейса
|
||||||
|
- 💾 Эффективное кэширование промптов
|
||||||
|
|
||||||
|
### Безопасность
|
||||||
|
- 🔒 Валидация входных данных в веб-интерфейсе
|
||||||
|
- 🛡️ Защита от XSS атак
|
||||||
|
- 🔐 Безопасная обработка файлов
|
||||||
|
|
||||||
|
### Код
|
||||||
|
- 🧹 Рефакторинг архитектуры для лучшей поддерживаемости
|
||||||
|
- 📚 Улучшенная документация кода
|
||||||
|
- 🧪 Добавлены тесты для критических компонентов
|
||||||
|
|
||||||
|
## 📦 ЗАВИСИМОСТИ
|
||||||
|
|
||||||
|
### Новые зависимости
|
||||||
|
- github.com/urfave/cli/v2 - CLI фреймворк
|
||||||
|
- gopkg.in/yaml.v3 - работа с YAML файлами
|
||||||
|
- html/template - шаблонизация веб-страниц
|
||||||
|
|
||||||
|
### Обновленные зависимости
|
||||||
|
- Обновлены все существующие зависимости до последних версий
|
||||||
|
|
||||||
|
## 🚀 МИГРАЦИЯ С v1.x.x
|
||||||
|
|
||||||
|
1. **Автоматическая миграция**: При первом запуске v2.0.0 автоматически создастся файл sys_prompts с встроенными промптами
|
||||||
|
2. **Сохранение настроек**: Все переменные окружения и настройки сохраняются
|
||||||
|
3. **Новые возможности**: Доступ к веб-интерфейсу через `lcg serve-result`
|
||||||
|
4. **Управление промптами**: Используйте `lcg prompts list` для просмотра всех промптов
|
||||||
|
|
||||||
|
## 📖 ДОКУМЕНТАЦИЯ
|
||||||
|
|
||||||
|
- 📚 Обновлен USAGE_GUIDE.md с описанием новых команд
|
||||||
|
- 📋 Добавлен API_CONTRACT.md для веб-интерфейса
|
||||||
|
- 🔧 Обновлен README.md с инструкциями по использованию
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Примечание**: Версия 2.0.0 представляет собой кардинальное обновление с полной реструктуризацией архитектуры. Рекомендуется ознакомиться с новыми возможностями веб-интерфейса и системой управления промптами.
|
||||||
85
README.md
85
README.md
@@ -1,85 +0,0 @@
|
|||||||
# Linux Command GPT (lcg)
|
|
||||||
|
|
||||||
This repo is forked from <https://github.com/asrul10/linux-command-gpt.git>
|
|
||||||
|
|
||||||
Generate Linux commands from natural language. Supports Ollama and Proxy backends, system prompts, different explanation levels (v/vv/vvv), and JSON history.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
Build from source:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone --depth 1 https://github.com/Direct-Dev-Ru/go-lcg.git ~/.linux-command-gpt
|
|
||||||
cd ~/.linux-command-gpt
|
|
||||||
go build -o lcg
|
|
||||||
|
|
||||||
# Add to your PATH
|
|
||||||
ln -s ~/.linux-command-gpt/lcg ~/.local/bin
|
|
||||||
```
|
|
||||||
|
|
||||||
## Quick start
|
|
||||||
|
|
||||||
```bash
|
|
||||||
lcg "I want to extract linux-command-gpt.tar.gz file"
|
|
||||||
```
|
|
||||||
|
|
||||||
After generation you will see a CAPS warning that the answer is from AI and must be verified, the command, and the action menu:
|
|
||||||
|
|
||||||
```text
|
|
||||||
ACTIONS: (c)opy, (s)ave, (r)egenerate, (e)xecute, (v|vv|vvv)explain, (n)othing
|
|
||||||
```
|
|
||||||
|
|
||||||
Explanations:
|
|
||||||
|
|
||||||
- `v` — short; `vv` — medium; `vvv` — detailed with alternatives.
|
|
||||||
|
|
||||||
Clipboard support requires `xclip` or `xsel`.
|
|
||||||
|
|
||||||
## Environment
|
|
||||||
|
|
||||||
- `LCG_PROVIDER` (ollama|proxy), `LCG_HOST`, `LCG_MODEL`, `LCG_PROMPT`
|
|
||||||
- `LCG_TIMEOUT` (default 120), `LCG_RESULT_FOLDER` (default ./gpt_results)
|
|
||||||
- `LCG_RESULT_HISTORY` (default $(LCG_RESULT_FOLDER)/lcg_history.json)
|
|
||||||
- `LCG_JWT_TOKEN` (for proxy)
|
|
||||||
|
|
||||||
## Flags
|
|
||||||
|
|
||||||
- `--file, -f` read part of prompt from file
|
|
||||||
- `--sys, -s` system prompt content or ID
|
|
||||||
- `--prompt-id, --pid` choose built-in prompt (1–5)
|
|
||||||
- `--timeout, -t` request timeout (sec)
|
|
||||||
- `--version, -v` print version; `--help, -h` help
|
|
||||||
|
|
||||||
## Commands
|
|
||||||
|
|
||||||
- `models`, `health`, `config`
|
|
||||||
- `prompts list|add|delete`
|
|
||||||
- `test-prompt <prompt-id> <command>`
|
|
||||||
- `update-jwt`, `delete-jwt` (proxy)
|
|
||||||
- `update-key`, `delete-key` (not needed for ollama/proxy)
|
|
||||||
- `history list` — list history from JSON
|
|
||||||
- `history view <index>` — view by index
|
|
||||||
- `history delete <index>` — delete by index (re-numbering)
|
|
||||||
|
|
||||||
## Saving results
|
|
||||||
|
|
||||||
Files are saved to `LCG_RESULT_FOLDER`.
|
|
||||||
|
|
||||||
- Command result: `gpt_request_<MODEL>_YYYY-MM-DD_HH-MM-SS.md`
|
|
||||||
- `# <title>` — H1 with original request (trimmed to 120 chars: first 116 + `...`)
|
|
||||||
- `## Prompt`
|
|
||||||
- `## Response`
|
|
||||||
|
|
||||||
- Detailed explanation: `gpt_explanation_<MODEL>_YYYY-MM-DD_HH-MM-SS.md`
|
|
||||||
- `# <title>`
|
|
||||||
- `## Prompt`
|
|
||||||
- `## Command`
|
|
||||||
- `## Explanation and Alternatives (model: <MODEL>)`
|
|
||||||
|
|
||||||
## History
|
|
||||||
|
|
||||||
- Stored as JSON array in `LCG_RESULT_HISTORY`.
|
|
||||||
- On new request, if the same command exists, you will be prompted to view or overwrite.
|
|
||||||
- Showing from history does not call the API; the standard action menu is shown.
|
|
||||||
|
|
||||||
For full guide in Russian, see `USAGE_GUIDE.md`.
|
|
||||||
@@ -1 +1 @@
|
|||||||
v2.0.0
|
v2.0.2
|
||||||
|
|||||||
224
_main.go
224
_main.go
@@ -1,224 +0,0 @@
|
|||||||
// package main
|
|
||||||
|
|
||||||
// import (
|
|
||||||
// _ "embed"
|
|
||||||
// "fmt"
|
|
||||||
// "math"
|
|
||||||
// "os"
|
|
||||||
// "os/user"
|
|
||||||
// "path"
|
|
||||||
// "strings"
|
|
||||||
// "time"
|
|
||||||
|
|
||||||
// "github.com/atotto/clipboard"
|
|
||||||
// "github.com/direct-dev-ru/linux-command-gpt/gpt"
|
|
||||||
// "github.com/direct-dev-ru/linux-command-gpt/reader"
|
|
||||||
// )
|
|
||||||
|
|
||||||
// //go:embed VERSION.txt
|
|
||||||
// var Version string
|
|
||||||
|
|
||||||
// var cwd, _ = os.Getwd()
|
|
||||||
|
|
||||||
// var (
|
|
||||||
// HOST = getEnv("LCG_HOST", "http://192.168.87.108:11434/")
|
|
||||||
// COMPLETIONS = getEnv("LCG_COMPLETIONS_PATH", "api/chat") // relative part of endpoint
|
|
||||||
// MODEL = getEnv("LCG_MODEL", "codegeex4")
|
|
||||||
// PROMPT = getEnv("LCG_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 = getEnv("LCG_API_KEY_FILE", ".openai_api_key")
|
|
||||||
// RESULT_FOLDER = getEnv("LCG_RESULT_FOLDER", path.Join(cwd, "gpt_results"))
|
|
||||||
|
|
||||||
// // HOST = "https://api.openai.com/v1/"
|
|
||||||
// // COMPLETIONS = "chat/completions"
|
|
||||||
|
|
||||||
// // MODEL = "gpt-4o-mini"
|
|
||||||
// // MODEL = "codellama:13b"
|
|
||||||
|
|
||||||
// // This file is created in the user's home directory
|
|
||||||
// // Example: /home/username/.openai_api_key
|
|
||||||
// // API_KEY_FILE = ".openai_api_key"
|
|
||||||
|
|
||||||
// HELP = `
|
|
||||||
|
|
||||||
// Usage: lcg [options]
|
|
||||||
|
|
||||||
// --help -h output usage information
|
|
||||||
// --version -v output the version number
|
|
||||||
// --file -f read part of command from file or bash feature $(...)
|
|
||||||
// --update-key -u update the API key
|
|
||||||
// --delete-key -d delete the API key
|
|
||||||
|
|
||||||
// Example Usage: lcg I want to extract linux-command-gpt.tar.gz file
|
|
||||||
// Example Usage: lcg --file /path/to/file.json I want to print object questions with jq
|
|
||||||
|
|
||||||
// Env Vars:
|
|
||||||
// LCG_HOST - defaults to "http://192.168.87.108:11434/" - endpoint for Ollama or other LLM API
|
|
||||||
// LCG_COMPLETIONS_PATH -defaults to "api/chat" - relative part of endpoint
|
|
||||||
// LCG_MODEL - defaults to "codegeex4"
|
|
||||||
// LCG_PROMPT - defaults to Reply with linux command and nothing else. Output with plain response - no need formatting. No need explanation. No need code blocks.
|
|
||||||
// LCG_API_KEY_FILE - defaults to ${HOME}/.openai_api_key - file with API key
|
|
||||||
// LCG_RESULT_FOLDER - defaults to $(pwd)/gpt_results - folder to save results
|
|
||||||
// `
|
|
||||||
|
|
||||||
// VERSION = Version
|
|
||||||
// CMD_HELP = 100
|
|
||||||
// CMD_VERSION = 101
|
|
||||||
// CMD_UPDATE = 102
|
|
||||||
// CMD_DELETE = 103
|
|
||||||
// CMD_COMPLETION = 110
|
|
||||||
// )
|
|
||||||
|
|
||||||
// // getEnv retrieves the value of the environment variable `key` or returns `defaultValue` if not set.
|
|
||||||
// func getEnv(key, defaultValue string) string {
|
|
||||||
// if value, exists := os.LookupEnv(key); exists {
|
|
||||||
// return value
|
|
||||||
// }
|
|
||||||
// return defaultValue
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func handleCommand(cmd string) int {
|
|
||||||
// if cmd == "" || cmd == "--help" || cmd == "-h" {
|
|
||||||
// return CMD_HELP
|
|
||||||
// }
|
|
||||||
// if cmd == "--version" || cmd == "-v" {
|
|
||||||
// return CMD_VERSION
|
|
||||||
// }
|
|
||||||
// if cmd == "--update-key" || cmd == "-u" {
|
|
||||||
// return CMD_UPDATE
|
|
||||||
// }
|
|
||||||
// if cmd == "--delete-key" || cmd == "-d" {
|
|
||||||
// return CMD_DELETE
|
|
||||||
// }
|
|
||||||
// return CMD_COMPLETION
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func getCommand(gpt3 gpt.Gpt3, cmd string) (string, float64) {
|
|
||||||
// gpt3.InitKey()
|
|
||||||
// s := time.Now()
|
|
||||||
// done := make(chan bool)
|
|
||||||
// go func() {
|
|
||||||
// loadingChars := []rune{'-', '\\', '|', '/'}
|
|
||||||
// i := 0
|
|
||||||
// for {
|
|
||||||
// select {
|
|
||||||
// case <-done:
|
|
||||||
// fmt.Printf("\r")
|
|
||||||
// return
|
|
||||||
// default:
|
|
||||||
// fmt.Printf("\rLoading %c", loadingChars[i])
|
|
||||||
// i = (i + 1) % len(loadingChars)
|
|
||||||
// time.Sleep(30 * time.Millisecond)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }()
|
|
||||||
|
|
||||||
// r := gpt3.Completions(cmd)
|
|
||||||
// done <- true
|
|
||||||
// elapsed := time.Since(s).Seconds()
|
|
||||||
// elapsed = math.Round(elapsed*100) / 100
|
|
||||||
|
|
||||||
// if r == "" {
|
|
||||||
// return "", elapsed
|
|
||||||
// }
|
|
||||||
// return r, elapsed
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func main() {
|
|
||||||
// currentUser, err := user.Current()
|
|
||||||
// if err != nil {
|
|
||||||
// panic(err)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// args := os.Args
|
|
||||||
// cmd := ""
|
|
||||||
// file := ""
|
|
||||||
// if len(args) > 1 {
|
|
||||||
// start := 1
|
|
||||||
// if args[1] == "--file" || args[1] == "-f" {
|
|
||||||
// file = args[2]
|
|
||||||
// start = 3
|
|
||||||
// }
|
|
||||||
// cmd = strings.Join(args[start:], " ")
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if file != "" {
|
|
||||||
// err := reader.FileToPrompt(&cmd, file)
|
|
||||||
// if err != nil {
|
|
||||||
// fmt.Println(err)
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if _, err := os.Stat(RESULT_FOLDER); os.IsNotExist(err) {
|
|
||||||
// os.MkdirAll(RESULT_FOLDER, 0755)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// h := handleCommand(cmd)
|
|
||||||
|
|
||||||
// if h == CMD_HELP {
|
|
||||||
// fmt.Println(HELP)
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if h == CMD_VERSION {
|
|
||||||
// fmt.Println(VERSION)
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
|
|
||||||
// gpt3 := gpt.Gpt3{
|
|
||||||
// CompletionUrl: HOST + COMPLETIONS,
|
|
||||||
// Model: MODEL,
|
|
||||||
// Prompt: PROMPT,
|
|
||||||
// HomeDir: currentUser.HomeDir,
|
|
||||||
// ApiKeyFile: API_KEY_FILE,
|
|
||||||
// Temperature: 0.01,
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if h == CMD_UPDATE {
|
|
||||||
// gpt3.UpdateKey()
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if h == CMD_DELETE {
|
|
||||||
// gpt3.DeleteKey()
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
|
|
||||||
// c := "R"
|
|
||||||
// r := ""
|
|
||||||
// elapsed := 0.0
|
|
||||||
// for c == "R" || c == "r" {
|
|
||||||
// r, elapsed = getCommand(gpt3, cmd)
|
|
||||||
// c = "N"
|
|
||||||
// fmt.Printf("Completed in %v seconds\n\n", elapsed)
|
|
||||||
// fmt.Println(r)
|
|
||||||
// fmt.Print("\nDo you want to (c)opy, (s)ave to file, (r)egenerate, or take (N)o action on the command? (c/r/N): ")
|
|
||||||
// fmt.Scanln(&c)
|
|
||||||
|
|
||||||
// // no action
|
|
||||||
// if c == "N" || c == "n" {
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if r == "" {
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Copy to clipboard
|
|
||||||
// if c == "C" || c == "c" {
|
|
||||||
// clipboard.WriteAll(r)
|
|
||||||
// fmt.Println("\033[33mCopied to clipboard")
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if c == "S" || c == "s" {
|
|
||||||
// timestamp := time.Now().Format("2006-01-02_15-04-05") // Format: YYYY-MM-DD_HH-MM-SS
|
|
||||||
// filename := fmt.Sprintf("gpt_request_%s(%s).md", timestamp, gpt3.Model)
|
|
||||||
// filePath := path.Join(RESULT_FOLDER, filename)
|
|
||||||
// resultString := fmt.Sprintf("## Prompt:\n\n%s\n\n------------------\n\n## Response:\n\n%s\n\n", cmd+". "+gpt3.Prompt, r)
|
|
||||||
// os.WriteFile(filePath, []byte(resultString), 0644)
|
|
||||||
// fmt.Println("\033[33mSaved to file")
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
1
build-conditions.yaml
Normal file
1
build-conditions.yaml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
no-serve: false
|
||||||
58
build.sh
Executable file
58
build.sh
Executable file
@@ -0,0 +1,58 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 🚀 LCG Build Script (Root)
|
||||||
|
# Скрипт для сборки из корневой директории проекта
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Цвета для вывода
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Функция для вывода сообщений
|
||||||
|
log() {
|
||||||
|
echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
success() {
|
||||||
|
echo -e "${GREEN}✅ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
warning() {
|
||||||
|
echo -e "${YELLOW}⚠️ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
error() {
|
||||||
|
echo -e "${RED}❌ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Параметры
|
||||||
|
REPOSITORY=${1:-"your-registry.com/lcg"}
|
||||||
|
VERSION=${2:-"latest"}
|
||||||
|
PLATFORMS=${3:-"linux/amd64,linux/arm64"}
|
||||||
|
|
||||||
|
log "🚀 Сборка LCG из корневой директории..."
|
||||||
|
|
||||||
|
# Проверяем, что мы в корневой директории
|
||||||
|
if [ ! -f "go.mod" ]; then
|
||||||
|
error "Этот скрипт должен запускаться из корневой директории проекта (где находится go.mod)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Записываем версию в файл VERSION.txt
|
||||||
|
echo "$VERSION" > VERSION.txt
|
||||||
|
log "📝 Версия записана в VERSION.txt: $VERSION"
|
||||||
|
|
||||||
|
# Запускаем полную сборку
|
||||||
|
log "🚀 Запуск полной сборки..."
|
||||||
|
./deploy/full-build.sh "$REPOSITORY" "$VERSION" "$PLATFORMS"
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
success "🎉 Сборка завершена успешно!"
|
||||||
|
else
|
||||||
|
error "Ошибка при сборке"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -20,9 +21,20 @@ type HistoryEntry struct {
|
|||||||
|
|
||||||
func read(historyPath string) ([]HistoryEntry, error) {
|
func read(historyPath string) ([]HistoryEntry, error) {
|
||||||
data, err := os.ReadFile(historyPath)
|
data, err := os.ReadFile(historyPath)
|
||||||
if err != nil || len(data) == 0 {
|
if err != nil {
|
||||||
|
// Если файл не существует, создаем пустой файл истории
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
emptyHistory := []HistoryEntry{}
|
||||||
|
if writeErr := write(historyPath, emptyHistory); writeErr != nil {
|
||||||
|
return nil, fmt.Errorf("не удалось создать файл истории: %v", writeErr)
|
||||||
|
}
|
||||||
|
return emptyHistory, nil
|
||||||
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if len(data) == 0 {
|
||||||
|
return []HistoryEntry{}, nil
|
||||||
|
}
|
||||||
var items []HistoryEntry
|
var items []HistoryEntry
|
||||||
if err := json.Unmarshal(data, &items); err != nil {
|
if err := json.Unmarshal(data, &items); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -50,10 +62,17 @@ func ShowHistory(historyPath string, printColored func(string, string), colorYel
|
|||||||
printColored("📝 История пуста\n", colorYellow)
|
printColored("📝 История пуста\n", colorYellow)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Сортируем записи по времени в убывающем порядке (новые сначала)
|
||||||
|
sort.Slice(items, func(i, j int) bool {
|
||||||
|
return items[i].Timestamp.After(items[j].Timestamp)
|
||||||
|
})
|
||||||
|
|
||||||
printColored("📝 История (из файла):\n", colorYellow)
|
printColored("📝 История (из файла):\n", colorYellow)
|
||||||
for _, h := range items {
|
for _, h := range items {
|
||||||
ts := h.Timestamp.Format("2006-01-02 15:04:05")
|
ts := h.Timestamp.Format("2006-01-02 15:04:05")
|
||||||
fmt.Printf("%d. [%s] %s → %s\n", h.Index, ts, h.Command, h.Response)
|
fmt.Printf("%d. [%s] %s → %s\n", h.Index, ts, h.Command, h.Response)
|
||||||
|
fmt.Printf("%s\n", "========================================================================================")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1938
cmd/serve.go
1938
cmd/serve.go
File diff suppressed because it is too large
Load Diff
120
config/config.go
120
config/config.go
@@ -3,6 +3,8 @@ package config
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -10,10 +12,12 @@ type Config struct {
|
|||||||
Cwd string
|
Cwd string
|
||||||
Host string
|
Host string
|
||||||
ProxyUrl string
|
ProxyUrl string
|
||||||
|
AppName string
|
||||||
Completions string
|
Completions string
|
||||||
Model string
|
Model string
|
||||||
Prompt string
|
Prompt string
|
||||||
ApiKeyFile string
|
ApiKeyFile string
|
||||||
|
ConfigFolder string
|
||||||
ResultFolder string
|
ResultFolder string
|
||||||
PromptFolder string
|
PromptFolder string
|
||||||
ProviderType string
|
ProviderType string
|
||||||
@@ -22,8 +26,10 @@ type Config struct {
|
|||||||
Timeout string
|
Timeout string
|
||||||
ResultHistory string
|
ResultHistory string
|
||||||
NoHistoryEnv string
|
NoHistoryEnv string
|
||||||
|
AllowExecution bool
|
||||||
MainFlags MainFlags
|
MainFlags MainFlags
|
||||||
Server ServerConfig
|
Server ServerConfig
|
||||||
|
Validation ValidationConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
type MainFlags struct {
|
type MainFlags struct {
|
||||||
@@ -38,6 +44,28 @@ type MainFlags struct {
|
|||||||
type ServerConfig struct {
|
type ServerConfig struct {
|
||||||
Port string
|
Port string
|
||||||
Host string
|
Host string
|
||||||
|
HealthUrl string
|
||||||
|
ProxyUrl string
|
||||||
|
BasePath string
|
||||||
|
ConfigFolder string
|
||||||
|
AllowHTTP bool
|
||||||
|
SSLCertFile string
|
||||||
|
SSLKeyFile string
|
||||||
|
RequireAuth bool
|
||||||
|
Password string
|
||||||
|
Domain string
|
||||||
|
CookieSecure bool
|
||||||
|
CookiePath string
|
||||||
|
CookieTTLHours int
|
||||||
|
}
|
||||||
|
|
||||||
|
type ValidationConfig struct {
|
||||||
|
MaxSystemPromptLength int
|
||||||
|
MaxUserMessageLength int
|
||||||
|
MaxPromptNameLength int
|
||||||
|
MaxPromptDescLength int
|
||||||
|
MaxCommandLength int
|
||||||
|
MaxExplanationLength int
|
||||||
}
|
}
|
||||||
|
|
||||||
func getEnv(key, defaultValue string) string {
|
func getEnv(key, defaultValue string) string {
|
||||||
@@ -47,6 +75,33 @@ func getEnv(key, defaultValue string) string {
|
|||||||
return defaultValue
|
return defaultValue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getEnvInt(key string, defaultValue int) int {
|
||||||
|
if value, exists := os.LookupEnv(key); exists {
|
||||||
|
if intValue, err := strconv.Atoi(value); err == nil {
|
||||||
|
return intValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func getServerAllowHTTP() bool {
|
||||||
|
// Если переменная явно установлена, используем её
|
||||||
|
if value, exists := os.LookupEnv("LCG_SERVER_ALLOW_HTTP"); exists {
|
||||||
|
if boolValue, err := strconv.ParseBool(value); err == nil {
|
||||||
|
return boolValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если переменная не установлена, определяем по умолчанию на основе хоста
|
||||||
|
host := getEnv("LCG_SERVER_HOST", "localhost")
|
||||||
|
return isSecureHost(host)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSecureHost(host string) bool {
|
||||||
|
secureHosts := []string{"localhost", "127.0.0.1", "::1"}
|
||||||
|
return slices.Contains(secureHosts, host)
|
||||||
|
}
|
||||||
|
|
||||||
func Load() Config {
|
func Load() Config {
|
||||||
cwd, _ := os.Getwd()
|
cwd, _ := os.Getwd()
|
||||||
|
|
||||||
@@ -54,14 +109,21 @@ func Load() Config {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
homedir = cwd
|
homedir = cwd
|
||||||
}
|
}
|
||||||
os.MkdirAll(path.Join(homedir, ".config", "lcg", "gpt_results"), 0755)
|
privateResultsDir := path.Join(homedir, ".config", "lcg", "gpt_results")
|
||||||
resultFolder := getEnv("LCG_RESULT_FOLDER", path.Join(homedir, ".config", "lcg", "gpt_results"))
|
os.MkdirAll(privateResultsDir, 0700)
|
||||||
|
resultFolder := getEnv("LCG_RESULT_FOLDER", privateResultsDir)
|
||||||
|
|
||||||
os.MkdirAll(path.Join(homedir, ".config", "lcg", "gpt_sys_prompts"), 0755)
|
privatePromptsDir := path.Join(homedir, ".config", "lcg", "gpt_sys_prompts")
|
||||||
promptFolder := getEnv("LCG_PROMPT_FOLDER", path.Join(homedir, ".config", "lcg", "gpt_sys_prompts"))
|
os.MkdirAll(privatePromptsDir, 0700)
|
||||||
|
promptFolder := getEnv("LCG_PROMPT_FOLDER", privatePromptsDir)
|
||||||
|
|
||||||
|
privateConfigDir := path.Join(homedir, ".config", "lcg", "config")
|
||||||
|
os.MkdirAll(privateConfigDir, 0700)
|
||||||
|
configFolder := getEnv("LCG_CONFIG_FOLDER", privateConfigDir)
|
||||||
|
|
||||||
return Config{
|
return Config{
|
||||||
Cwd: cwd,
|
Cwd: cwd,
|
||||||
|
AppName: getEnv("LCG_APP_NAME", "Linux Command GPT"),
|
||||||
Host: getEnv("LCG_HOST", "http://192.168.87.108:11434/"),
|
Host: getEnv("LCG_HOST", "http://192.168.87.108:11434/"),
|
||||||
ProxyUrl: getEnv("LCG_PROXY_URL", "/api/v1/protected/sberchat/chat"),
|
ProxyUrl: getEnv("LCG_PROXY_URL", "/api/v1/protected/sberchat/chat"),
|
||||||
Completions: getEnv("LCG_COMPLETIONS_PATH", "api/chat"),
|
Completions: getEnv("LCG_COMPLETIONS_PATH", "api/chat"),
|
||||||
@@ -70,15 +132,38 @@ func Load() Config {
|
|||||||
ApiKeyFile: getEnv("LCG_API_KEY_FILE", ".openai_api_key"),
|
ApiKeyFile: getEnv("LCG_API_KEY_FILE", ".openai_api_key"),
|
||||||
ResultFolder: resultFolder,
|
ResultFolder: resultFolder,
|
||||||
PromptFolder: promptFolder,
|
PromptFolder: promptFolder,
|
||||||
|
ConfigFolder: configFolder,
|
||||||
ProviderType: getEnv("LCG_PROVIDER", "ollama"),
|
ProviderType: getEnv("LCG_PROVIDER", "ollama"),
|
||||||
JwtToken: getEnv("LCG_JWT_TOKEN", ""),
|
JwtToken: getEnv("LCG_JWT_TOKEN", ""),
|
||||||
PromptID: getEnv("LCG_PROMPT_ID", "1"),
|
PromptID: getEnv("LCG_PROMPT_ID", "1"),
|
||||||
Timeout: getEnv("LCG_TIMEOUT", "300"),
|
Timeout: getEnv("LCG_TIMEOUT", "300"),
|
||||||
ResultHistory: getEnv("LCG_RESULT_HISTORY", path.Join(resultFolder, "lcg_history.json")),
|
ResultHistory: getEnv("LCG_RESULT_HISTORY", path.Join(resultFolder, "lcg_history.json")),
|
||||||
NoHistoryEnv: getEnv("LCG_NO_HISTORY", ""),
|
NoHistoryEnv: getEnv("LCG_NO_HISTORY", ""),
|
||||||
|
AllowExecution: isAllowExecutionEnabled(),
|
||||||
Server: ServerConfig{
|
Server: ServerConfig{
|
||||||
Port: getEnv("LCG_SERVER_PORT", "8080"),
|
Port: getEnv("LCG_SERVER_PORT", "8080"),
|
||||||
Host: getEnv("LCG_SERVER_HOST", "localhost"),
|
Host: getEnv("LCG_SERVER_HOST", "localhost"),
|
||||||
|
ConfigFolder: getEnv("LCG_CONFIG_FOLDER", path.Join(homedir, ".config", "lcg", "config")),
|
||||||
|
AllowHTTP: getServerAllowHTTP(),
|
||||||
|
SSLCertFile: getEnv("LCG_SERVER_SSL_CERT_FILE", ""),
|
||||||
|
SSLKeyFile: getEnv("LCG_SERVER_SSL_KEY_FILE", ""),
|
||||||
|
RequireAuth: isServerRequireAuth(),
|
||||||
|
Password: getEnv("LCG_SERVER_PASSWORD", "admin#123456"),
|
||||||
|
Domain: getEnv("LCG_DOMAIN", getEnv("LCG_SERVER_HOST", "localhost")),
|
||||||
|
CookieSecure: isCookieSecure(),
|
||||||
|
CookiePath: getEnv("LCG_COOKIE_PATH", "/lcg"),
|
||||||
|
CookieTTLHours: getEnvInt("LCG_COOKIE_TTL_HOURS", 168), // 7 дней по умолчанию
|
||||||
|
BasePath: getEnv("LCG_BASE_URL", "/lcg"),
|
||||||
|
HealthUrl: getEnv("LCG_HEALTH_URL", "/api/v1/protected/sberchat/health"),
|
||||||
|
ProxyUrl: getEnv("LCG_PROXY_URL", "/api/v1/protected/sberchat/chat"),
|
||||||
|
},
|
||||||
|
Validation: ValidationConfig{
|
||||||
|
MaxSystemPromptLength: getEnvInt("LCG_MAX_SYSTEM_PROMPT_LENGTH", 2000),
|
||||||
|
MaxUserMessageLength: getEnvInt("LCG_MAX_USER_MESSAGE_LENGTH", 4000),
|
||||||
|
MaxPromptNameLength: getEnvInt("LCG_MAX_PROMPT_NAME_LENGTH", 2000),
|
||||||
|
MaxPromptDescLength: getEnvInt("LCG_MAX_PROMPT_DESC_LENGTH", 5000),
|
||||||
|
MaxCommandLength: getEnvInt("LCG_MAX_COMMAND_LENGTH", 8000),
|
||||||
|
MaxExplanationLength: getEnvInt("LCG_MAX_EXPLANATION_LENGTH", 20000),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,6 +177,33 @@ func (c Config) IsNoHistoryEnabled() bool {
|
|||||||
return vLower == "1" || vLower == "true"
|
return vLower == "1" || vLower == "true"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isAllowExecutionEnabled() bool {
|
||||||
|
v := strings.TrimSpace(getEnv("LCG_ALLOW_EXECUTION", ""))
|
||||||
|
if v == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
vLower := strings.ToLower(v)
|
||||||
|
return vLower == "1" || vLower == "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
func isServerRequireAuth() bool {
|
||||||
|
v := strings.TrimSpace(getEnv("LCG_SERVER_REQUIRE_AUTH", ""))
|
||||||
|
if v == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
vLower := strings.ToLower(v)
|
||||||
|
return vLower == "1" || vLower == "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
func isCookieSecure() bool {
|
||||||
|
v := strings.TrimSpace(getEnv("LCG_COOKIE_SECURE", ""))
|
||||||
|
if v == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
vLower := strings.ToLower(v)
|
||||||
|
return vLower == "1" || vLower == "true"
|
||||||
|
}
|
||||||
|
|
||||||
var AppConfig Config
|
var AppConfig Config
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
29
deploy/.goreleaser.yaml
Normal file
29
deploy/.goreleaser.yaml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Goreleaser configuration version 2
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
builds:
|
||||||
|
- id: lcg
|
||||||
|
binary: "lcg_{{ .Version }}"
|
||||||
|
goos:
|
||||||
|
- linux
|
||||||
|
goarch:
|
||||||
|
- amd64
|
||||||
|
- arm64
|
||||||
|
env:
|
||||||
|
- CGO_ENABLED=0
|
||||||
|
ldflags:
|
||||||
|
- -s -w
|
||||||
|
- -X main.version={{.Version}}
|
||||||
|
- -X main.commit={{.Commit}}
|
||||||
|
- -X main.date={{.Date}}
|
||||||
|
main: .
|
||||||
|
dir: .
|
||||||
|
|
||||||
|
archives:
|
||||||
|
- id: lcg
|
||||||
|
builds:
|
||||||
|
- lcg
|
||||||
|
format: binary
|
||||||
|
name_template: "{{ .Binary }}_{{ .Os }}_{{ .Arch }}"
|
||||||
|
files:
|
||||||
|
- "lcg_{{ .Version }}"
|
||||||
21
deploy/0.create_sealed_secrets.example.sh
Normal file
21
deploy/0.create_sealed_secrets.example.sh
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# https://dev.to/ashokan/sealed-secrets-the-secret-sauce-for-managing-secrets-2hg6
|
||||||
|
# head -c 64 /dev/urandom | base64 -w 0
|
||||||
|
export KUBECONFIG=/home/su/.kube/config_hlab
|
||||||
|
|
||||||
|
kubectl create secret generic lcg-secrets -n lcg \
|
||||||
|
--from-literal=LCG_SERVER_PASSWORDL= \
|
||||||
|
--from-literal=LCG_CSRF_SECRET=\
|
||||||
|
--from-literal=LCG_JWT_SECRET=\
|
||||||
|
--from-literal=LCG_JWT_TOKEN=\
|
||||||
|
--dry-run=client -o yaml | tee secret-cfg.yaml
|
||||||
|
|
||||||
|
kubeseal --controller-name=sealed-secrets-controller --controller-namespace=kube-system -o yaml <secret-cfg.yaml | tee sealed-cfg.yaml
|
||||||
|
|
||||||
|
rm -f secret-cfg.yaml
|
||||||
|
|
||||||
|
kubectl apply -f sealed-cfg.yaml
|
||||||
|
cp sealed-cfg.yaml ../kustomize/secret.yaml
|
||||||
|
|
||||||
|
kubectl get secret lcg-secrets -n lcg -o json | jq ".data | map_values(@base64d)"
|
||||||
7
deploy/0.namespace.yaml
Normal file
7
deploy/0.namespace.yaml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: lcg
|
||||||
|
labels:
|
||||||
|
name: lcg
|
||||||
|
app: linux-command-gpt
|
||||||
46
deploy/1.configmap.yaml
Normal file
46
deploy/1.configmap.yaml
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: lcg-config
|
||||||
|
namespace: lcg
|
||||||
|
data:
|
||||||
|
# Основные настройки
|
||||||
|
LCG_VERSION: "${VERSION}"
|
||||||
|
LCG_BASE_PATH: "/lcg"
|
||||||
|
LCG_SERVER_HOST: "0.0.0.0"
|
||||||
|
LCG_SERVER_PORT: "8080"
|
||||||
|
LCG_SERVER_ALLOW_HTTP: "true"
|
||||||
|
LCG_APP_NAME: "Linux Command GPT"
|
||||||
|
LCG_RESULT_FOLDER: "/app/data/results"
|
||||||
|
LCG_PROMPT_FOLDER: "/app/data/prompts"
|
||||||
|
LCG_CONFIG_FOLDER: "/app/data/config"
|
||||||
|
LCG_NO_HISTORY: "false"
|
||||||
|
LCG_ALLOW_EXECUTION: "false"
|
||||||
|
LCG_DEBUG: "false"
|
||||||
|
|
||||||
|
# Настройки аутентификации
|
||||||
|
LCG_SERVER_REQUIRE_AUTH: "true"
|
||||||
|
|
||||||
|
LCG_COOKIE_SECURE: "true"
|
||||||
|
LCG_COOKIE_TTL_HOURS: "168"
|
||||||
|
LCG_DOMAIN: "direct-dev.ru"
|
||||||
|
LCG_COOKIE_PATH: "/lcg"
|
||||||
|
|
||||||
|
# Настройки провайдера (по умолчанию)
|
||||||
|
LCG_PROVIDER_TYPE: "proxy"
|
||||||
|
LCG_HOST: "https://direct-dev.ru"
|
||||||
|
LCG_HEALTH_URL: "/api/v1/protected/sberchat/health"
|
||||||
|
LCG_PROXY_URL: "/api/v1/protected/sberchat/chat"
|
||||||
|
LCG_MODEL: "GigaChat-2-Max"
|
||||||
|
|
||||||
|
# Настройки валидации
|
||||||
|
LCG_MAX_SYSTEM_PROMPT_LENGTH: "2000"
|
||||||
|
LCG_MAX_USER_MESSAGE_LENGTH: "4000"
|
||||||
|
LCG_MAX_PROMPT_NAME_LENGTH: "2000"
|
||||||
|
LCG_MAX_PROMPT_DESC_LENGTH: "50000"
|
||||||
|
|
||||||
|
# Настройки таймаутов
|
||||||
|
LCG_TIMEOUT: "300"
|
||||||
|
|
||||||
|
# Настройки отладки
|
||||||
|
LCG_DEBUG: "false"
|
||||||
12
deploy/2.gitrepository.yaml
Normal file
12
deploy/2.gitrepository.yaml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
apiVersion: source.toolkit.fluxcd.io/v1
|
||||||
|
kind: GitRepository
|
||||||
|
metadata:
|
||||||
|
name: linux-command-gpt
|
||||||
|
namespace: flux-system
|
||||||
|
spec:
|
||||||
|
interval: 3m
|
||||||
|
url: https://direct-dev.ru/gitea/GiteaAdmin/go-lcg.git
|
||||||
|
ref:
|
||||||
|
branch: release
|
||||||
|
secretRef:
|
||||||
|
name: gitea-token
|
||||||
19
deploy/3.lcg-kustomization.yaml
Normal file
19
deploy/3.lcg-kustomization.yaml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
apiVersion: kustomize.toolkit.fluxcd.io/v1
|
||||||
|
kind: Kustomization
|
||||||
|
metadata:
|
||||||
|
name: lcg
|
||||||
|
namespace: flux-system
|
||||||
|
spec:
|
||||||
|
healthChecks:
|
||||||
|
- kind: Deployment
|
||||||
|
name: lcg
|
||||||
|
namespace: lcg
|
||||||
|
interval: 3m15s
|
||||||
|
path: ./kustomize
|
||||||
|
prune: true
|
||||||
|
sourceRef:
|
||||||
|
kind: GitRepository
|
||||||
|
name: linux-command-gpt
|
||||||
|
targetNamespace: lcg
|
||||||
|
timeout: 2m0s
|
||||||
114
deploy/4.build-binaries.sh
Executable file
114
deploy/4.build-binaries.sh
Executable file
@@ -0,0 +1,114 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 🚀 LCG Binary Build Script
|
||||||
|
# Скрипт для сборки бинарных файлов с помощью goreleaser на хосте
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Цвета для вывода
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Функция для вывода сообщений
|
||||||
|
log() {
|
||||||
|
echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
success() {
|
||||||
|
echo -e "${GREEN}✅ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
warning() {
|
||||||
|
echo -e "${YELLOW}⚠️ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
error() {
|
||||||
|
echo -e "${RED}❌ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Параметры
|
||||||
|
VERSION=${1:-"dev"}
|
||||||
|
# CLEAN=${2:-"true"}
|
||||||
|
|
||||||
|
# Записываем версию в файл VERSION.txt (в корневой директории проекта)
|
||||||
|
echo "$VERSION" > VERSION.txt
|
||||||
|
log "📝 Версия записана в VERSION.txt: $VERSION"
|
||||||
|
|
||||||
|
log "🚀 Сборка бинарных файлов LCG с goreleaser..."
|
||||||
|
|
||||||
|
# Проверяем наличие goreleaser
|
||||||
|
if ! command -v goreleaser &> /dev/null; then
|
||||||
|
error "goreleaser не найден. Установите goreleaser:"
|
||||||
|
echo " curl -sL https://github.com/goreleaser/goreleaser/releases/latest/download/goreleaser_Linux_x86_64.tar.gz | tar -xz -C /usr/local/bin goreleaser"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Проверяем наличие Go
|
||||||
|
if ! command -v go &> /dev/null; then
|
||||||
|
error "Go не найден. Установите Go для сборки."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Переходим в корневую директорию проекта
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
log "📁 Рабочая директория: $(pwd)"
|
||||||
|
log "📁 Папка dist будет создана в: $(pwd)/dist"
|
||||||
|
|
||||||
|
# Очищаем предыдущие сборки если нужно
|
||||||
|
# if [ "$CLEAN" = "true" ]; then
|
||||||
|
# log "🧹 Очистка предыдущих сборок..."
|
||||||
|
# rm -rf dist/
|
||||||
|
# goreleaser clean
|
||||||
|
# fi
|
||||||
|
|
||||||
|
# Проверяем наличие .goreleaser.yaml
|
||||||
|
if [ ! -f "deploy/.goreleaser.yaml" ]; then
|
||||||
|
error "Файл .goreleaser.yaml не найден в папке deploy/"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Копируем конфигурацию goreleaser в корень проекта
|
||||||
|
log "📋 Копирование конфигурации goreleaser..."
|
||||||
|
cp deploy/.goreleaser.yaml .goreleaser.yaml
|
||||||
|
|
||||||
|
# Устанавливаем переменные окружения для версии
|
||||||
|
export GORELEASER_CURRENT_TAG="$VERSION"
|
||||||
|
|
||||||
|
# Собираем бинарные файлы
|
||||||
|
log "🏗️ Сборка бинарных файлов для всех платформ..."
|
||||||
|
goreleaser build --snapshot --clean
|
||||||
|
|
||||||
|
# Проверяем результат
|
||||||
|
if [ -d "dist" ]; then
|
||||||
|
log "📊 Собранные бинарные файлы:"
|
||||||
|
find dist -name "lcg_*" -type f | while read -r binary; do
|
||||||
|
echo " $binary ($(stat -c%s "$binary") bytes, $(file "$binary" | cut -d: -f2))"
|
||||||
|
done
|
||||||
|
|
||||||
|
success "🎉 Бинарные файлы успешно собраны!"
|
||||||
|
|
||||||
|
# Показываем структуру dist/
|
||||||
|
log "📁 Структура папки dist/:"
|
||||||
|
tree -h dist/ 2>/dev/null || find dist -type f | sort
|
||||||
|
|
||||||
|
else
|
||||||
|
error "Папка dist/ не создана. Проверьте конфигурацию goreleaser."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Очищаем временный файл конфигурации
|
||||||
|
rm -f .goreleaser.yaml
|
||||||
|
|
||||||
|
success "🎉 Сборка бинарных файлов завершена!"
|
||||||
|
|
||||||
|
# Показываем команды для Docker сборки
|
||||||
|
echo ""
|
||||||
|
log "📝 Следующие шаги:"
|
||||||
|
echo " cd deploy"
|
||||||
|
echo " docker buildx build --platform linux/amd64,linux/arm64 --tag your-registry.com/lcg:$VERSION --push ."
|
||||||
|
echo " # или используйте скрипт:"
|
||||||
|
echo " ./5.build-docker.sh your-registry.com/lcg $VERSION"
|
||||||
12
deploy/4.pvc.yaml
Normal file
12
deploy/4.pvc.yaml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: lcg-data
|
||||||
|
namespace: lcg
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 5Gi
|
||||||
|
storageClassName: nfs
|
||||||
183
deploy/5.build-docker.sh
Executable file
183
deploy/5.build-docker.sh
Executable file
@@ -0,0 +1,183 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 🐳 LCG Docker Build Script
|
||||||
|
# Скрипт для сборки Docker образа с предварительно собранными бинарными файлами
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Цвета для вывода
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Функция для вывода сообщений
|
||||||
|
log() {
|
||||||
|
echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
success() {
|
||||||
|
echo -e "${GREEN}✅ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
warning() {
|
||||||
|
echo -e "${YELLOW}⚠️ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
error() {
|
||||||
|
echo -e "${RED}❌ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Параметры
|
||||||
|
REPOSITORY=${1:-"your-registry.com/lcg"}
|
||||||
|
VERSION=${2:-""}
|
||||||
|
PLATFORMS=${3:-"linux/amd64,linux/arm64"}
|
||||||
|
|
||||||
|
if [ -z "$VERSION" ]; then
|
||||||
|
error "Версия не указана! Использование: $0 <repository> <version>"
|
||||||
|
echo "Пример: $0 your-registry.com/lcg v1.0.0 <platforms>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "🐳 Сборка Docker образа LCG..."
|
||||||
|
|
||||||
|
# Проверяем наличие docker
|
||||||
|
if ! command -v docker &> /dev/null; then
|
||||||
|
error "Docker не найден. Установите Docker для сборки образов."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Проверяем наличие docker buildx
|
||||||
|
if ! docker buildx version &> /dev/null; then
|
||||||
|
error "Docker Buildx не найден. Установите Docker Buildx для мультиплатформенной сборки."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Проверяем наличие бинарных файлов в текущей директории (если запускаем из корня)
|
||||||
|
if [ ! -d "dist" ]; then
|
||||||
|
error "Папка dist/ не найдена. Сначала соберите бинарные файлы:"
|
||||||
|
echo " ./deploy/4.build-binaries.sh $VERSION"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Проверяем наличие бинарных файлов для всех платформ
|
||||||
|
MISSING_BINARIES=()
|
||||||
|
|
||||||
|
# Ищем бинарные файлы с версией в имени
|
||||||
|
AMD64_BINARY=$(find dist -name "*linux_amd64*" -type d | head -1)
|
||||||
|
echo "AMD64_BINARY: $AMD64_BINARY"
|
||||||
|
ARM64_BINARY=$(find dist -name "*linux_arm64*" -type d | head -1)
|
||||||
|
echo "ARM64_BINARY: $ARM64_BINARY"
|
||||||
|
|
||||||
|
# Проверяем наличие бинарных файлов в найденных папках и соответствие версии
|
||||||
|
if [ -n "$AMD64_BINARY" ]; then
|
||||||
|
AMD64_FILE=$(find "$AMD64_BINARY" -name "lcg_*" -type f | head -1)
|
||||||
|
if [ -z "$AMD64_FILE" ]; then
|
||||||
|
AMD64_BINARY=""
|
||||||
|
else
|
||||||
|
# Извлекаем версию из имени файла
|
||||||
|
FILE_VERSION=$(basename "$AMD64_FILE" | sed 's/lcg_//' | sed 's/-SNAPSHOT.*//')
|
||||||
|
# Нормализуем версии для сравнения (убираем префикс 'v' если есть)
|
||||||
|
NORMALIZED_FILE_VERSION=$(echo "$FILE_VERSION" | sed 's/^v//')
|
||||||
|
NORMALIZED_VERSION=$(echo "$VERSION" | sed 's/^v//')
|
||||||
|
if [ "$NORMALIZED_FILE_VERSION" != "$NORMALIZED_VERSION" ]; then
|
||||||
|
error "Версия в имени бинарного файла ($FILE_VERSION) не совпадает с переданной версией ($VERSION)"
|
||||||
|
echo "Файл: $AMD64_FILE"
|
||||||
|
echo "Ожидаемая версия: $VERSION"
|
||||||
|
echo "Версия в файле: $FILE_VERSION"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$ARM64_BINARY" ]; then
|
||||||
|
ARM64_FILE=$(find "$ARM64_BINARY" -name "lcg_*" -type f | head -1)
|
||||||
|
if [ -z "$ARM64_FILE" ]; then
|
||||||
|
ARM64_BINARY=""
|
||||||
|
else
|
||||||
|
# Извлекаем версию из имени файла
|
||||||
|
FILE_VERSION=$(basename "$ARM64_FILE" | sed 's/lcg_//' | sed 's/-SNAPSHOT.*//')
|
||||||
|
# Нормализуем версии для сравнения (убираем префикс 'v' если есть)
|
||||||
|
NORMALIZED_FILE_VERSION=$(echo "$FILE_VERSION" | sed 's/^v//')
|
||||||
|
NORMALIZED_VERSION=$(echo "$VERSION" | sed 's/^v//')
|
||||||
|
if [ "$NORMALIZED_FILE_VERSION" != "$NORMALIZED_VERSION" ]; then
|
||||||
|
error "Версия в имени бинарного файла ($FILE_VERSION) не совпадает с переданной версией ($VERSION)"
|
||||||
|
echo "Файл: $ARM64_FILE"
|
||||||
|
echo "Ожидаемая версия: $VERSION"
|
||||||
|
echo "Версия в файле: $FILE_VERSION"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$AMD64_BINARY" ]; then
|
||||||
|
MISSING_BINARIES+=("linux/amd64")
|
||||||
|
fi
|
||||||
|
if [ -z "$ARM64_BINARY" ]; then
|
||||||
|
MISSING_BINARIES+=("linux/arm64")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ${#MISSING_BINARIES[@]} -gt 0 ]; then
|
||||||
|
error "Отсутствуют бинарные файлы для платформ: ${MISSING_BINARIES[*]}"
|
||||||
|
echo "Сначала соберите бинарные файлы:"
|
||||||
|
echo " ./4.build-binaries.sh $VERSION"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Показываем найденные файлы и их версии
|
||||||
|
log "📊 Найденные бинарные файлы:"
|
||||||
|
if [ -n "$AMD64_FILE" ]; then
|
||||||
|
echo " AMD64: $AMD64_FILE"
|
||||||
|
fi
|
||||||
|
if [ -n "$ARM64_FILE" ]; then
|
||||||
|
echo " ARM64: $ARM64_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Создаем builder если не существует
|
||||||
|
log "🔧 Настройка Docker Buildx..."
|
||||||
|
docker buildx create --name lcg-builder --use 2>/dev/null || docker buildx use lcg-builder
|
||||||
|
|
||||||
|
# Копируем бинарные файлы и файл версии в папку deploy
|
||||||
|
log "📋 Копирование бинарных файлов и файла версии..."
|
||||||
|
cp -r dist ./deploy/dist
|
||||||
|
cp VERSION.txt ./deploy/VERSION.txt 2>/dev/null || echo "dev" > ./deploy/VERSION.txt
|
||||||
|
|
||||||
|
# Сборка для всех платформ
|
||||||
|
log "🏗️ Сборка образа для платформ: $PLATFORMS"
|
||||||
|
log "📦 Репозиторий: $REPOSITORY"
|
||||||
|
log "🏷️ Версия: $VERSION"
|
||||||
|
|
||||||
|
# Сборка и push
|
||||||
|
docker buildx build \
|
||||||
|
--platform "$PLATFORMS" \
|
||||||
|
--tag "$REPOSITORY:$VERSION" \
|
||||||
|
--tag "$REPOSITORY:latest" \
|
||||||
|
--push \
|
||||||
|
--file deploy/Dockerfile \
|
||||||
|
deploy/
|
||||||
|
|
||||||
|
# Очищаем скопированные файлы
|
||||||
|
rm -rf ./deploy/dist
|
||||||
|
|
||||||
|
success "🎉 Образ успешно собран и отправлен в репозиторий!"
|
||||||
|
|
||||||
|
# Показываем информацию о собранном образе
|
||||||
|
log "📊 Информация о собранном образе:"
|
||||||
|
echo " Репозиторий: $REPOSITORY"
|
||||||
|
echo " Версия: $VERSION"
|
||||||
|
echo " Платформы: $PLATFORMS"
|
||||||
|
echo " Теги: $REPOSITORY:$VERSION, $REPOSITORY:latest"
|
||||||
|
|
||||||
|
# Проверяем образы в репозитории
|
||||||
|
log "🔍 Проверка образов в репозитории..."
|
||||||
|
docker buildx imagetools inspect "$REPOSITORY:$VERSION" || warning "Не удалось проверить образ в репозитории"
|
||||||
|
|
||||||
|
success "🎉 Сборка завершена успешно!"
|
||||||
|
|
||||||
|
# Показываем команды для использования
|
||||||
|
echo ""
|
||||||
|
log "📝 Полезные команды:"
|
||||||
|
echo " docker pull $REPOSITORY:$VERSION"
|
||||||
|
echo " docker run -p 8080:8080 $REPOSITORY:$VERSION"
|
||||||
|
echo " docker buildx imagetools inspect $REPOSITORY:$VERSION"
|
||||||
202
deploy/6.full-build.sh
Executable file
202
deploy/6.full-build.sh
Executable file
@@ -0,0 +1,202 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 🚀 LCG Full Build Script
|
||||||
|
# Полный скрипт сборки: бинарные файлы + Docker образ
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Цвета для вывода
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Функция для вывода сообщений
|
||||||
|
log() {
|
||||||
|
echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
success() {
|
||||||
|
echo -e "${GREEN}✅ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
warning() {
|
||||||
|
echo -e "${YELLOW}⚠️ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
error() {
|
||||||
|
echo -e "${RED}❌ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Параметры
|
||||||
|
|
||||||
|
REPOSITORY=${1:-"kuznetcovay/lcg"}
|
||||||
|
VERSION=${2:-""}
|
||||||
|
PLATFORMS=${3:-"linux/amd64,linux/arm64"}
|
||||||
|
|
||||||
|
if [ -z "$VERSION" ]; then
|
||||||
|
error "Версия не указана! Использование: $0 <repository> <version> <platforms>"
|
||||||
|
echo "Пример: $0 kuznetcovay/lcg v1.0.0 linux/amd64,linux/arm64"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Записываем версию в файл VERSION.txt (в корневой директории проекта)
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
echo "$VERSION" > VERSION.txt
|
||||||
|
log "📝 Версия записана в VERSION.txt: $VERSION"
|
||||||
|
|
||||||
|
log "🚀 Полная сборка LCG (бинарные файлы + Docker образ)..."
|
||||||
|
|
||||||
|
# Этап 1: Сборка бинарных файлов
|
||||||
|
log "📦 Этап 1: Сборка бинарных файлов с goreleaser..."
|
||||||
|
./deploy/4.build-binaries.sh "$VERSION"
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
error "Ошибка при сборке бинарных файлов"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
success "✅ Бинарные файлы собраны успешно"
|
||||||
|
|
||||||
|
# Этап 2: Сборка Docker образа
|
||||||
|
log "🐳 Этап 2: Сборка Docker образа..."
|
||||||
|
./deploy/5.build-docker.sh "$REPOSITORY" "$VERSION" "$PLATFORMS"
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
error "Ошибка при сборке Docker образа"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
success "✅ Docker образы собраны успешно"
|
||||||
|
|
||||||
|
# Этап 3: Генерация deployment.yaml
|
||||||
|
log "📝 Этап 3: Генерация deployment.yaml..."
|
||||||
|
# Generate deployment.yaml with env substitution
|
||||||
|
export REPOSITORY=$REPOSITORY
|
||||||
|
export VERSION=$VERSION
|
||||||
|
export PLATFORMS=$PLATFORMS
|
||||||
|
export KUBECONFIG="${HOME}/.kube/config_hlab" && kubectx default
|
||||||
|
|
||||||
|
if ! envsubst < deploy/1.configmap.yaml > kustomize/configmap.yaml; then
|
||||||
|
error "Ошибка при генерации deploy/1.configmap.yaml"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
success "✅ kustomize/configmap.yaml сгенерирован успешно"
|
||||||
|
|
||||||
|
if ! envsubst < deploy/deployment.tmpl.yaml > kustomize/deployment.yaml; then
|
||||||
|
error "Ошибка при генерации kustomize/deployment.yaml"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
success "✅ kustomize/deployment.yaml сгенерирован успешно"
|
||||||
|
|
||||||
|
if ! envsubst < deploy/ingress-route.tmpl.yaml > kustomize/ingress-route.yaml; then
|
||||||
|
error "Ошибка при генерации kustomize/ingress-route.yaml"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
success "✅ kustomize/ingress-route.yaml сгенерирован успешно"
|
||||||
|
|
||||||
|
if ! envsubst < deploy/service.tmpl.yaml > kustomize/service.yaml; then
|
||||||
|
error "Ошибка при генерации kustomize/service.yaml"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
success "✅ kustomize/service.yaml сгенерирован успешно"
|
||||||
|
|
||||||
|
if ! envsubst < deploy/kustomization.tmpl.yaml > kustomize/kustomization.yaml; then
|
||||||
|
error "Ошибка при генерации kustomize/kustomization.yaml"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
success "✅ kustomize/kustomization.yaml сгенерирован успешно"
|
||||||
|
|
||||||
|
# отключить reconciliation flux
|
||||||
|
if kubectl get kustomization lcg -n flux-system > /dev/null 2>&1; then
|
||||||
|
kubectl patch kustomization lcg -n flux-system --type=merge -p '{"spec":{"suspend":true}}'
|
||||||
|
else
|
||||||
|
echo "ℹ️ Kustomization 'lcg' does not exist in 'flux-system' namespace. Skipping suspend."
|
||||||
|
fi
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
|
||||||
|
# зафиксировать изменения в текущей ветке, если она не main
|
||||||
|
current_branch=$(git rev-parse --abbrev-ref HEAD)
|
||||||
|
if [ "$current_branch" != "main" ]; then
|
||||||
|
log "🔧 Исправления в текущей ветке: $current_branch"
|
||||||
|
# считать, что изменения уже сделаны
|
||||||
|
git add .
|
||||||
|
git commit -m "Исправления в ветке $current_branch"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# переключиться на ветку main и слить с текущей веткой, если не находимся на main
|
||||||
|
if [ "$current_branch" != "main" ]; then
|
||||||
|
git checkout main
|
||||||
|
git merge --no-ff -m "Merged branch '$current_branch' into main while building $VERSION" "$current_branch"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# переключиться на ветку release и слить с веткой main
|
||||||
|
git checkout -b release
|
||||||
|
git merge --no-ff -m "Merged main into release while building $VERSION" main
|
||||||
|
|
||||||
|
# если тег $VERSION существует, удалить его и принудительно запушить
|
||||||
|
tag_exists=$(git tag -l "$VERSION")
|
||||||
|
if [ "$tag_exists" ]; then
|
||||||
|
log "🗑️ Удаление существующего тега $VERSION"
|
||||||
|
git tag -d "$VERSION"
|
||||||
|
git push origin ":refs/tags/$VERSION"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create tag $VERSION and push to remote release branch and all tags
|
||||||
|
git tag "$VERSION"
|
||||||
|
git push origin release
|
||||||
|
git push origin --tags
|
||||||
|
|
||||||
|
# Push main branch
|
||||||
|
git checkout main
|
||||||
|
git push origin main
|
||||||
|
|
||||||
|
|
||||||
|
# Включить reconciliation flux
|
||||||
|
if kubectl get kustomization lcg -n flux-system > /dev/null 2>&1; then
|
||||||
|
kubectl patch kustomization lcg -n flux-system --type=merge -p '{"spec":{"suspend":false}}'
|
||||||
|
else
|
||||||
|
echo "ℹ️ Kustomization 'lcg' does not exist in 'flux-system' namespace. Skipping suspend."
|
||||||
|
fi
|
||||||
|
echo "🔄 Flux will automatically deploy $VERSION version in ~4-6 minutes..."
|
||||||
|
|
||||||
|
# Итоговая информация
|
||||||
|
echo ""
|
||||||
|
log "🎉 Полная сборка завершена успешно!"
|
||||||
|
echo ""
|
||||||
|
log "📊 Результат:"
|
||||||
|
echo " Репозиторий: $REPOSITORY"
|
||||||
|
echo " Версия: $VERSION"
|
||||||
|
echo " Платформы: $PLATFORMS"
|
||||||
|
echo " Теги: $REPOSITORY:$VERSION, $REPOSITORY:latest"
|
||||||
|
echo ""
|
||||||
|
echo ""
|
||||||
|
log "🔍 Информация о git коммитах:"
|
||||||
|
git_log=$(git log release -1 --pretty=format:"%H - %s")
|
||||||
|
echo "$git_log"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
|
||||||
|
log "📝 Команды для использования:"
|
||||||
|
echo " docker pull $REPOSITORY:$VERSION"
|
||||||
|
echo " docker run -p 8080:8080 $REPOSITORY:$VERSION"
|
||||||
|
echo " docker buildx imagetools inspect $REPOSITORY:$VERSION"
|
||||||
|
echo ""
|
||||||
|
log "🔍 Проверка образа:"
|
||||||
|
echo " docker run --rm $REPOSITORY:$VERSION /app/lcg --version"
|
||||||
|
echo ""
|
||||||
|
log "📝 Команды для использования:"
|
||||||
|
echo " kubectl apply -k kustomize"
|
||||||
|
echo " kubectl get pods"
|
||||||
|
echo " kubectl get services"
|
||||||
|
echo " kubectl get ingress"
|
||||||
|
echo " kubectl get hpa"
|
||||||
|
echo " kubectl get servicemonitor"
|
||||||
|
echo " kubectl get pods"
|
||||||
|
echo " kubectl get services"
|
||||||
|
echo " kubectl get ingress"
|
||||||
|
echo " kubectl get hpa"
|
||||||
|
echo " kubectl get servicemonitor"
|
||||||
39
deploy/Dockerfile
Normal file
39
deploy/Dockerfile
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Однофазный build для LCG с предварительно собранным бинарным файлом
|
||||||
|
FROM alpine:3.22.2
|
||||||
|
|
||||||
|
# Устанавливаем зависимости
|
||||||
|
RUN apk --no-cache add ca-certificates tzdata
|
||||||
|
|
||||||
|
# Создаем пользователя
|
||||||
|
RUN adduser -D -s /bin/sh lcg
|
||||||
|
|
||||||
|
# Создаем директории и файлы
|
||||||
|
RUN mkdir -p /app/data /app/config /home/lcg/.config/lcg/gpt_results /home/lcg/.config/lcg/gpt_sys_prompts && \
|
||||||
|
echo '[]' > /home/lcg/.config/lcg/gpt_results/lcg_history.json && \
|
||||||
|
chown -R lcg:lcg /app /home/lcg/.config
|
||||||
|
|
||||||
|
# Копируем файл версии
|
||||||
|
COPY VERSION.txt /app/VERSION.txt
|
||||||
|
|
||||||
|
# Копируем предварительно собранный бинарный файл
|
||||||
|
# Ищем папку с бинарным файлом для текущей архитектуры
|
||||||
|
COPY dist/lcg_linux_${TARGETARCH}*/lcg_* /app/lcg
|
||||||
|
|
||||||
|
# Устанавливаем права
|
||||||
|
RUN chmod +x /app/lcg
|
||||||
|
|
||||||
|
# Переключаемся на пользователя lcg
|
||||||
|
USER lcg
|
||||||
|
|
||||||
|
# Устанавливаем рабочую директорию
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Открываем порт
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||||
|
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/login || exit 1
|
||||||
|
|
||||||
|
# Запускаем приложение
|
||||||
|
CMD ["./lcg", "serve", "-H", "0.0.0.0", "-p", "8080"]
|
||||||
1
deploy/VERSION.txt
Normal file
1
deploy/VERSION.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
v2.0.2
|
||||||
95
deploy/deployment.tmpl.yaml
Normal file
95
deploy/deployment.tmpl.yaml
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: lcg
|
||||||
|
namespace: lcg
|
||||||
|
labels:
|
||||||
|
app: lcg
|
||||||
|
version: ${VERSION}
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: lcg
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: lcg
|
||||||
|
version: ${VERSION}
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: lcg
|
||||||
|
image: ${REPOSITORY}:${VERSION}
|
||||||
|
imagePullPolicy: Always
|
||||||
|
ports:
|
||||||
|
- containerPort: 8080
|
||||||
|
protocol: TCP
|
||||||
|
envFrom:
|
||||||
|
- configMapRef:
|
||||||
|
name: lcg-config
|
||||||
|
- secretRef:
|
||||||
|
name: lcg-secret
|
||||||
|
env:
|
||||||
|
# Pod information
|
||||||
|
- name: POD_NAME
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: metadata.name
|
||||||
|
- name: POD_NAMESPACE
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: metadata.namespace
|
||||||
|
- name: NODE_NAME
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: spec.nodeName
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpu: 1000m
|
||||||
|
memory: 1Gi
|
||||||
|
requests:
|
||||||
|
cpu: 250m
|
||||||
|
memory: 512Mi
|
||||||
|
volumeMounts:
|
||||||
|
- name: lcg-data
|
||||||
|
mountPath: /app/data
|
||||||
|
- name: lcg-config
|
||||||
|
mountPath: /app/config
|
||||||
|
readOnly: true
|
||||||
|
# Health checks
|
||||||
|
startupProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /login
|
||||||
|
port: 8080
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 5
|
||||||
|
failureThreshold: 30
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /login
|
||||||
|
port: 8080
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 10
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /login
|
||||||
|
port: 8080
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 60
|
||||||
|
volumes:
|
||||||
|
- name: lcg-data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: lcg-data
|
||||||
|
- name: lcg-config
|
||||||
|
configMap:
|
||||||
|
name: lcg-config
|
||||||
|
# Security context
|
||||||
|
securityContext:
|
||||||
|
runAsNonRoot: true
|
||||||
|
runAsUser: 1001
|
||||||
|
readOnlyRootFilesystem: false
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
capabilities:
|
||||||
|
drop:
|
||||||
|
- ALL
|
||||||
|
restartPolicy: Always
|
||||||
42
deploy/hpa.yaml
Normal file
42
deploy/hpa.yaml
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
apiVersion: autoscaling/v2
|
||||||
|
kind: HorizontalPodAutoscaler
|
||||||
|
metadata:
|
||||||
|
name: lcg-hpa
|
||||||
|
namespace: lcg
|
||||||
|
spec:
|
||||||
|
scaleTargetRef:
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
name: lcg
|
||||||
|
minReplicas: 2
|
||||||
|
maxReplicas: 10
|
||||||
|
metrics:
|
||||||
|
- type: Resource
|
||||||
|
resource:
|
||||||
|
name: cpu
|
||||||
|
target:
|
||||||
|
type: Utilization
|
||||||
|
averageUtilization: 70
|
||||||
|
- type: Resource
|
||||||
|
resource:
|
||||||
|
name: memory
|
||||||
|
target:
|
||||||
|
type: Utilization
|
||||||
|
averageUtilization: 80
|
||||||
|
behavior:
|
||||||
|
scaleDown:
|
||||||
|
stabilizationWindowSeconds: 300
|
||||||
|
policies:
|
||||||
|
- type: Percent
|
||||||
|
value: 10
|
||||||
|
periodSeconds: 60
|
||||||
|
scaleUp:
|
||||||
|
stabilizationWindowSeconds: 60
|
||||||
|
policies:
|
||||||
|
- type: Percent
|
||||||
|
value: 50
|
||||||
|
periodSeconds: 60
|
||||||
|
- type: Pods
|
||||||
|
value: 2
|
||||||
|
periodSeconds: 60
|
||||||
|
selectPolicy: Max
|
||||||
64
deploy/ingress-route.tmpl.yaml
Normal file
64
deploy/ingress-route.tmpl.yaml
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: IngressRoute
|
||||||
|
metadata:
|
||||||
|
name: lcg-route
|
||||||
|
namespace: lcg
|
||||||
|
labels:
|
||||||
|
app: lcg
|
||||||
|
version: ${VERSION}
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- websecure
|
||||||
|
routes:
|
||||||
|
- kind: Rule
|
||||||
|
match: Host(`direct-dev.ru`) && PathPrefix(`/lcg`)
|
||||||
|
services:
|
||||||
|
- name: lcg
|
||||||
|
port: 8080
|
||||||
|
tls:
|
||||||
|
secretName: le-root-direct-dev-ru
|
||||||
|
---
|
||||||
|
|
||||||
|
# apiVersion: traefik.io/v1alpha1
|
||||||
|
# kind: IngressRoute
|
||||||
|
# metadata:
|
||||||
|
# name: lcg-route
|
||||||
|
# namespace: lcg
|
||||||
|
# spec:
|
||||||
|
# entryPoints:
|
||||||
|
# - websecure
|
||||||
|
# routes:
|
||||||
|
# - kind: Rule
|
||||||
|
# match: Host(`direct-dev.ru`) && PathPrefix(`/lcg`)
|
||||||
|
# services:
|
||||||
|
# - name: lcg
|
||||||
|
# port: 8080
|
||||||
|
# middlewares:
|
||||||
|
# - name: lcg-strip-prefix
|
||||||
|
# tls:
|
||||||
|
# secretName: le-root-direct-dev-ru
|
||||||
|
# ---
|
||||||
|
# apiVersion: traefik.io/v1alpha1
|
||||||
|
# kind: Middleware
|
||||||
|
# metadata:
|
||||||
|
# name: lcg-strip-prefix
|
||||||
|
# namespace: lcg
|
||||||
|
# spec:
|
||||||
|
# stripPrefix:
|
||||||
|
# prefixes:
|
||||||
|
# - /lcg
|
||||||
|
# ---
|
||||||
|
# apiVersion: traefik.io/v1alpha1
|
||||||
|
# kind: Middleware
|
||||||
|
# metadata:
|
||||||
|
# name: lcg-headers
|
||||||
|
# namespace: lcg
|
||||||
|
# spec:
|
||||||
|
# headers:
|
||||||
|
# customRequestHeaders:
|
||||||
|
# X-Forwarded-Proto: "https"
|
||||||
|
# X-Forwarded-Port: "443"
|
||||||
|
# customResponseHeaders:
|
||||||
|
# X-Frame-Options: "DENY"
|
||||||
|
# X-Content-Type-Options: "nosniff"
|
||||||
|
# X-XSS-Protection: "1; mode=block"
|
||||||
25
deploy/kustomization.tmpl.yaml
Normal file
25
deploy/kustomization.tmpl.yaml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
|
||||||
|
# Namespace
|
||||||
|
namespace: lcg
|
||||||
|
|
||||||
|
# Resources
|
||||||
|
resources:
|
||||||
|
- configmap.yaml
|
||||||
|
- secret.yaml
|
||||||
|
- deployment.yaml
|
||||||
|
- service.yaml
|
||||||
|
- ingress-route.yaml
|
||||||
|
|
||||||
|
# Common labels
|
||||||
|
commonLabels:
|
||||||
|
app: lcg
|
||||||
|
version: ${VERSION}
|
||||||
|
managed-by: kustomize
|
||||||
|
|
||||||
|
# Images
|
||||||
|
images:
|
||||||
|
- name: lcg
|
||||||
|
newName: ${REPOSITORY}
|
||||||
|
newTag: ${VERSION}
|
||||||
18
deploy/service.tmpl.yaml
Normal file
18
deploy/service.tmpl.yaml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: lcg
|
||||||
|
namespace: lcg
|
||||||
|
labels:
|
||||||
|
app: lcg
|
||||||
|
version: ${VERSION}
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
ports:
|
||||||
|
- port: 8080
|
||||||
|
targetPort: 8080
|
||||||
|
protocol: TCP
|
||||||
|
name: http
|
||||||
|
selector:
|
||||||
|
app: lcg
|
||||||
|
version: ${VERSION}
|
||||||
@@ -181,3 +181,7 @@ curl -sS -X POST \
|
|||||||
|
|
||||||
- Добавление новых полей в ответах, не используемых клиентом, допустимо при сохранении существующих.
|
- Добавление новых полей в ответах, не используемых клиентом, допустимо при сохранении существующих.
|
||||||
- Переименование или удаление полей `response` (proxy) и `message.content` (ollama) нарушит совместимость.
|
- Переименование или удаление полей `response` (proxy) и `message.content` (ollama) нарушит совместимость.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Дополнительно: для HTTP API веб‑сервера (эндпоинт `POST /execute`, только `curl`) см. `API_GUIDE.md` с примерами и подробной схемой запроса/ответа.
|
||||||
259
docs/API_GUIDE.md
Normal file
259
docs/API_GUIDE.md
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
# API Guide - Linux Command GPT
|
||||||
|
|
||||||
|
## Обзор
|
||||||
|
|
||||||
|
API позволяет выполнять запросы к Linux Command GPT через HTTP POST запросы с помощью curl. API принимает только запросы от curl (проверка User-Agent).
|
||||||
|
|
||||||
|
## Endpoint
|
||||||
|
|
||||||
|
``` curl
|
||||||
|
POST /execute
|
||||||
|
```
|
||||||
|
|
||||||
|
## Запуск сервера
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Запуск сервера
|
||||||
|
lcg serve
|
||||||
|
|
||||||
|
# Запуск на другом порту
|
||||||
|
lcg serve --port 9000
|
||||||
|
|
||||||
|
# Запуск с автоматическим открытием браузера
|
||||||
|
lcg serve --browser
|
||||||
|
```
|
||||||
|
|
||||||
|
## Структура запроса
|
||||||
|
|
||||||
|
### JSON Payload
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"prompt": "создать директорию test",
|
||||||
|
"system_id": 1,
|
||||||
|
"system": "альтернативный системный промпт",
|
||||||
|
"verbose": "vv",
|
||||||
|
"timeout": 120
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Поля запроса
|
||||||
|
|
||||||
|
| Поле | Тип | Обязательное | Описание |
|
||||||
|
|------|-----|--------------|----------|
|
||||||
|
| `prompt` | string | ✅ | Пользовательский запрос |
|
||||||
|
| `system_id` | int | ❌ | ID системного промпта (1-5) |
|
||||||
|
| `system` | string | ❌ | Текст системного промпта (альтернатива system_id) |
|
||||||
|
| `verbose` | string | ❌ | Степень подробности: "v", "vv", "vvv" |
|
||||||
|
| `timeout` | int | ❌ | Таймаут в секундах (по умолчанию: 120) |
|
||||||
|
|
||||||
|
### Структура ответа
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"command": "mkdir test",
|
||||||
|
"explanation": "Команда mkdir создает новую директорию...",
|
||||||
|
"model": "hf.co/yandex/YandexGPT-5-Lite-8B-instruct-GGUF:Q4_K_M",
|
||||||
|
"elapsed": 2.34
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Примеры использования
|
||||||
|
|
||||||
|
### 1. Базовый запрос
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8080/execute \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"prompt": "создать директорию test"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ответ:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"command": "mkdir test",
|
||||||
|
"model": "hf.co/yandex/YandexGPT-5-Lite-8B-instruct-GGUF:Q4_K_M",
|
||||||
|
"elapsed": 1.23
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Запрос с системным промптом по ID
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8080/execute \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"prompt": "найти все файлы .txt",
|
||||||
|
"system_id": 2
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Запрос с кастомным системным промптом
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8080/execute \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"prompt": "показать использование памяти",
|
||||||
|
"system": "Ты эксперт по Linux. Отвечай только командами без объяснений."
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Запрос с подробным объяснением
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8080/execute \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"prompt": "архивировать папку documents",
|
||||||
|
"verbose": "vv",
|
||||||
|
"timeout": 180
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ответ:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"command": "tar -czf documents.tar.gz documents/",
|
||||||
|
"explanation": "Команда tar создает архив в формате gzip...",
|
||||||
|
"model": "hf.co/yandex/YandexGPT-5-Lite-8B-instruct-GGUF:Q4_K_M",
|
||||||
|
"elapsed": 3.45
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Запрос с максимальной подробностью
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8080/execute \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"prompt": "настроить SSH сервер",
|
||||||
|
"system_id": 3,
|
||||||
|
"verbose": "vvv",
|
||||||
|
"timeout": 300
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Системные промпты
|
||||||
|
|
||||||
|
| ID | Название | Описание |
|
||||||
|
|----|----------|----------|
|
||||||
|
| 1 | basic | Базовые команды Linux |
|
||||||
|
| 2 | advanced | Продвинутые команды |
|
||||||
|
| 3 | system | Системное администрирование |
|
||||||
|
| 4 | network | Сетевые команды |
|
||||||
|
| 5 | security | Безопасность |
|
||||||
|
|
||||||
|
## Степени подробности
|
||||||
|
|
||||||
|
| Уровень | Описание |
|
||||||
|
|---------|----------|
|
||||||
|
| `v` | Краткое объяснение |
|
||||||
|
| `vv` | Подробное объяснение с альтернативами |
|
||||||
|
| `vvv` | Максимально подробное объяснение с примерами |
|
||||||
|
|
||||||
|
## Обработка ошибок
|
||||||
|
|
||||||
|
### Ошибка валидации
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": "Prompt is required"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ошибка AI
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": "Failed to get response from AI"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ошибка доступа
|
||||||
|
|
||||||
|
``` text
|
||||||
|
HTTP 403 Forbidden
|
||||||
|
Only curl requests are allowed
|
||||||
|
```
|
||||||
|
|
||||||
|
## Переменные окружения
|
||||||
|
|
||||||
|
Убедитесь, что настроены необходимые переменные:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Основные настройки
|
||||||
|
export LCG_PROVIDER="ollama"
|
||||||
|
export LCG_HOST="http://localhost:11434"
|
||||||
|
export LCG_MODEL="hf.co/yandex/YandexGPT-5-Lite-8B-instruct-GGUF:Q4_K_M"
|
||||||
|
|
||||||
|
# Для proxy провайдера
|
||||||
|
export LCG_PROVIDER="proxy"
|
||||||
|
export LCG_HOST="https://your-proxy-server.com"
|
||||||
|
export LCG_JWT_TOKEN="your-jwt-token"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Безопасность
|
||||||
|
|
||||||
|
- ✅ **Только curl**: API принимает только запросы от curl
|
||||||
|
- ✅ **POST только**: Только POST запросы к `/execute`
|
||||||
|
- ✅ **JSON валидация**: Строгая проверка входных данных
|
||||||
|
- ✅ **Таймауты**: Ограничение времени выполнения запросов
|
||||||
|
|
||||||
|
## Примеры скриптов
|
||||||
|
|
||||||
|
### Bash скрипт для автоматизации
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
API_URL="http://localhost:8080/execute"
|
||||||
|
|
||||||
|
# Функция для выполнения запроса
|
||||||
|
execute_command() {
|
||||||
|
local prompt="$1"
|
||||||
|
local verbose="${2:-}"
|
||||||
|
|
||||||
|
curl -s -X POST "$API_URL" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"prompt\": \"$prompt\", \"verbose\": \"$verbose\"}" | \
|
||||||
|
jq -r '.command'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Использование
|
||||||
|
echo "Команда: $(execute_command "создать директорию backup")"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Python скрипт
|
||||||
|
|
||||||
|
```python
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
|
||||||
|
def execute_command(prompt, system_id=None, verbose=None):
|
||||||
|
url = "http://localhost:8080/execute"
|
||||||
|
payload = {"prompt": prompt}
|
||||||
|
|
||||||
|
if system_id:
|
||||||
|
payload["system_id"] = system_id
|
||||||
|
if verbose:
|
||||||
|
payload["verbose"] = verbose
|
||||||
|
|
||||||
|
response = requests.post(url, json=payload)
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
# Использование
|
||||||
|
result = execute_command("показать использование диска", verbose="vv")
|
||||||
|
print(f"Команда: {result['command']}")
|
||||||
|
if 'explanation' in result:
|
||||||
|
print(f"Объяснение: {result['explanation']}")
|
||||||
|
```
|
||||||
232
docs/CONFIG_COMMAND.md
Normal file
232
docs/CONFIG_COMMAND.md
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
# 🔧 Команда config - Управление конфигурацией
|
||||||
|
|
||||||
|
## 📋 Описание
|
||||||
|
|
||||||
|
Команда `config` позволяет просматривать текущую конфигурацию приложения, включая все настройки, переменные окружения и значения по умолчанию.
|
||||||
|
|
||||||
|
## 🚀 Использование
|
||||||
|
|
||||||
|
### Краткий вывод конфигурации (по умолчанию)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
lcg config
|
||||||
|
# или
|
||||||
|
lcg co
|
||||||
|
```
|
||||||
|
|
||||||
|
**Вывод:**
|
||||||
|
|
||||||
|
``` text
|
||||||
|
Provider: ollama
|
||||||
|
Host: http://192.168.87.108:11434/
|
||||||
|
Model: hf.co/yandex/YandexGPT-5-Lite-8B-instruct-GGUF:Q4_K_M
|
||||||
|
Prompt: Reply with linux command and nothing else. Output with plain response - no need formatting. No need explanation. No need code blocks. No need ` symbols.
|
||||||
|
Timeout: 300 seconds
|
||||||
|
```
|
||||||
|
|
||||||
|
### Полный вывод конфигурации
|
||||||
|
|
||||||
|
```bash
|
||||||
|
lcg config --full
|
||||||
|
# или
|
||||||
|
lcg config -f
|
||||||
|
# или
|
||||||
|
lcg co --full
|
||||||
|
# или
|
||||||
|
lcg co -f
|
||||||
|
```
|
||||||
|
|
||||||
|
**Вывод (JSON формат):**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cwd": "/home/user/projects/golang/linux-command-gpt",
|
||||||
|
"host": "http://192.168.87.108:11434/",
|
||||||
|
"proxy_url": "/api/v1/protected/sberchat/chat",
|
||||||
|
"completions": "api/chat",
|
||||||
|
"model": "hf.co/yandex/YandexGPT-5-Lite-8B-instruct-GGUF:Q4_K_M",
|
||||||
|
"prompt": "Reply with linux command and nothing else. Output with plain response - no need formatting. No need explanation. No need code blocks. No need ` symbols.",
|
||||||
|
"api_key_file": ".openai_api_key",
|
||||||
|
"result_folder": "/home/user/.config/lcg/gpt_results",
|
||||||
|
"prompt_folder": "/home/user/.config/lcg/gpt_sys_prompts",
|
||||||
|
"provider_type": "ollama",
|
||||||
|
"jwt_token": "***not set***",
|
||||||
|
"prompt_id": "1",
|
||||||
|
"timeout": "300",
|
||||||
|
"result_history": "/home/user/.config/lcg/gpt_results/lcg_history.json",
|
||||||
|
"no_history_env": "",
|
||||||
|
"allow_execution": false,
|
||||||
|
"main_flags": {
|
||||||
|
"file": "",
|
||||||
|
"no_history": false,
|
||||||
|
"sys": "",
|
||||||
|
"prompt_id": 0,
|
||||||
|
"timeout": 0,
|
||||||
|
"debug": false
|
||||||
|
},
|
||||||
|
"server": {
|
||||||
|
"port": "8080",
|
||||||
|
"host": "localhost"
|
||||||
|
},
|
||||||
|
"validation": {
|
||||||
|
"max_system_prompt_length": 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` показывает актуальное состояние конфигурации после применения всех переменных окружения и значений по умолчанию.
|
||||||
231
docs/CSRF_TESTING_GUIDE.md
Normal file
231
docs/CSRF_TESTING_GUIDE.md
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
# 🛡️ Руководство по тестированию CSRF защиты
|
||||||
|
|
||||||
|
## 📋 Обзор
|
||||||
|
|
||||||
|
Это руководство поможет вам протестировать CSRF защиту в LCG приложении и понять, как работают CSRF атаки.
|
||||||
|
|
||||||
|
## 🚀 Быстрый старт
|
||||||
|
|
||||||
|
### 1. Запуск сервера с CSRF защитой
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Запуск с аутентификацией и CSRF защитой
|
||||||
|
LCG_SERVER_REQUIRE_AUTH=true ./lcg serve -p 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Автоматическое тестирование
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Запуск автоматических тестов
|
||||||
|
./test_csrf.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Ручное тестирование
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Откройте в браузере
|
||||||
|
open csrf_test.html
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🧪 Типы тестов
|
||||||
|
|
||||||
|
### ✅ **Тест 1: Защищенные запросы**
|
||||||
|
|
||||||
|
- **Цель**: Проверить, что POST запросы без CSRF токена блокируются
|
||||||
|
- **Ожидаемый результат**: 403 Forbidden
|
||||||
|
- **Endpoints**: `/api/execute`, `/api/save-result`, `/api/add-to-history`
|
||||||
|
|
||||||
|
### ✅ **Тест 2: Разрешенные запросы**
|
||||||
|
|
||||||
|
- **Цель**: Проверить, что GET запросы работают
|
||||||
|
- **Ожидаемый результат**: 200 OK
|
||||||
|
- **Endpoints**: `/login`, `/`, `/history`
|
||||||
|
|
||||||
|
### ✅ **Тест 3: CSRF токены**
|
||||||
|
|
||||||
|
- **Цель**: Проверить наличие CSRF токенов в формах
|
||||||
|
- **Ожидаемый результат**: Токены присутствуют в HTML
|
||||||
|
|
||||||
|
### ✅ **Тест 4: Поддельные токены**
|
||||||
|
|
||||||
|
- **Цель**: Проверить защиту от поддельных токенов
|
||||||
|
- **Ожидаемый результат**: 403 Forbidden
|
||||||
|
|
||||||
|
## 🎯 Сценарии атак
|
||||||
|
|
||||||
|
### **Сценарий 1: Выполнение команд**
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- Злонамеренная форма -->
|
||||||
|
<form action="http://localhost:8080/api/execute" method="POST">
|
||||||
|
<input type="hidden" name="prompt" value="rm -rf /">
|
||||||
|
<input type="hidden" name="system_id" value="1">
|
||||||
|
<button type="submit">Нажми меня!</button>
|
||||||
|
</form>
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Сценарий 2: Сохранение данных**
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- Злонамеренная форма -->
|
||||||
|
<form action="http://localhost:8080/api/save-result" method="POST">
|
||||||
|
<input type="hidden" name="result" value="Вредоносные данные">
|
||||||
|
<input type="hidden" name="command" value="malicious_command">
|
||||||
|
<button type="submit">Сохранить</button>
|
||||||
|
</form>
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Сценарий 3: JavaScript атака**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Злонамеренный JavaScript
|
||||||
|
fetch('http://localhost:8080/api/execute', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({prompt: 'whoami', system_id: '1'})
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔍 Анализ результатов
|
||||||
|
|
||||||
|
### **✅ Защита работает, если:**
|
||||||
|
|
||||||
|
- Все POST запросы возвращают 403 Forbidden
|
||||||
|
- В ответах есть "CSRF token required"
|
||||||
|
- GET запросы работают нормально
|
||||||
|
- CSRF токены присутствуют в формах
|
||||||
|
|
||||||
|
### **❌ Уязвимость есть, если:**
|
||||||
|
|
||||||
|
- POST запросы выполняются успешно (200 OK)
|
||||||
|
- Команды выполняются на сервере
|
||||||
|
- Данные сохраняются без CSRF токенов
|
||||||
|
- Нет проверки Origin/Referer заголовков
|
||||||
|
|
||||||
|
## 🛠️ Инструменты тестирования
|
||||||
|
|
||||||
|
### **1. Автоматический скрипт**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./test_csrf.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
- Тестирует все основные endpoints
|
||||||
|
- Проверяет CSRF токены
|
||||||
|
- Выводит подробный отчет
|
||||||
|
|
||||||
|
### **2. HTML тестовая страница**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
open csrf_test.html
|
||||||
|
```
|
||||||
|
|
||||||
|
- Интерактивное тестирование
|
||||||
|
- Визуальная проверка результатов
|
||||||
|
- Тестирование в браузере
|
||||||
|
|
||||||
|
### **3. Демонстрационная атака**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
open csrf_demo.html
|
||||||
|
```
|
||||||
|
|
||||||
|
- Показывает, как работают CSRF атаки
|
||||||
|
- Демонстрирует уязвимости
|
||||||
|
- Образовательные цели
|
||||||
|
|
||||||
|
## 🔧 Настройка тестов
|
||||||
|
|
||||||
|
### **Переменные окружения для тестирования:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Включить аутентификацию
|
||||||
|
LCG_SERVER_REQUIRE_AUTH=true
|
||||||
|
|
||||||
|
# Настроить CSRF защиту
|
||||||
|
LCG_COOKIE_SECURE=false
|
||||||
|
LCG_DOMAIN=.localhost
|
||||||
|
LCG_COOKIE_PATH=/
|
||||||
|
|
||||||
|
# Запуск сервера
|
||||||
|
./lcg serve -H 0.0.0.0 -p 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Настройка reverse proxy для тестирования:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Для тестирования за reverse proxy
|
||||||
|
LCG_SERVER_REQUIRE_AUTH=true \
|
||||||
|
LCG_SERVER_ALLOW_HTTP=true \
|
||||||
|
LCG_DOMAIN=.yourdomain.com \
|
||||||
|
LCG_COOKIE_PATH=/lcg \
|
||||||
|
LCG_COOKIE_SECURE=false \
|
||||||
|
./lcg serve -H 0.0.0.0 -p 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Интерпретация результатов
|
||||||
|
|
||||||
|
### **Успешные тесты:**
|
||||||
|
|
||||||
|
``` text
|
||||||
|
✅ CSRF защита /api/execute: PASS - Запрос заблокирован (403 Forbidden)
|
||||||
|
✅ CSRF защита /api/save-result: PASS - Запрос заблокирован (403 Forbidden)
|
||||||
|
✅ CSRF защита /api/add-to-history: PASS - Запрос заблокирован (403 Forbidden)
|
||||||
|
✅ GET запросы: PASS - GET запросы работают (HTTP 200)
|
||||||
|
✅ CSRF токен на странице входа: PASS - Токен найден
|
||||||
|
✅ CSRF защита от поддельного токена: PASS - Поддельный токен заблокирован (403 Forbidden)
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Проблемные тесты:**
|
||||||
|
|
||||||
|
``` text
|
||||||
|
❌ CSRF защита /api/execute: FAIL - Запрос прошел (HTTP 200)
|
||||||
|
❌ CSRF защита /api/save-result: FAIL - Запрос прошел (HTTP 200)
|
||||||
|
❌ CSRF токен на странице входа: FAIL - Токен не найден
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚨 Частые проблемы
|
||||||
|
|
||||||
|
### **1. Cookies не работают**
|
||||||
|
|
||||||
|
- Проверьте настройки `LCG_DOMAIN`
|
||||||
|
- Убедитесь, что `LCG_COOKIE_PATH` правильный
|
||||||
|
- Проверьте настройки reverse proxy
|
||||||
|
|
||||||
|
### **2. CSRF токены не генерируются**
|
||||||
|
|
||||||
|
- Убедитесь, что `LCG_SERVER_REQUIRE_AUTH=true`
|
||||||
|
- Проверьте инициализацию CSRF менеджера
|
||||||
|
- Проверьте логи сервера
|
||||||
|
|
||||||
|
### **3. Запросы проходят без токенов**
|
||||||
|
|
||||||
|
- Проверьте middleware в `serve/middleware.go`
|
||||||
|
- Убедитесь, что CSRF middleware применяется
|
||||||
|
- Проверьте исключения в middleware
|
||||||
|
|
||||||
|
## 📝 Рекомендации
|
||||||
|
|
||||||
|
### **Для разработчиков:**
|
||||||
|
|
||||||
|
1. Всегда тестируйте CSRF защиту
|
||||||
|
2. Используйте автоматические тесты
|
||||||
|
3. Проверяйте все POST endpoints
|
||||||
|
4. Валидируйте CSRF токены
|
||||||
|
|
||||||
|
### **Для администраторов:**
|
||||||
|
|
||||||
|
1. Регулярно запускайте тесты
|
||||||
|
2. Мониторьте логи на подозрительную активность
|
||||||
|
3. Настройте правильные заголовки в reverse proxy
|
||||||
|
4. Используйте HTTPS в продакшене
|
||||||
|
|
||||||
|
## 🎓 Образовательные материалы
|
||||||
|
|
||||||
|
- **OWASP CSRF Prevention Cheat Sheet**: <https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html>
|
||||||
|
- **CSRF атаки**: <https://owasp.org/www-community/attacks/csrf>
|
||||||
|
- **SameSite cookies**: <https://web.dev/samesite-cookies-explained/>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**⚠️ ВНИМАНИЕ**: Эти тесты предназначены только для проверки безопасности вашего собственного приложения. Не используйте их для атак на чужие системы!
|
||||||
167
docs/README.md
Normal file
167
docs/README.md
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
# Linux Command GPT (lcg)
|
||||||
|
|
||||||
|
This repo is forked from <https://github.com/asrul10/linux-command-gpt.git>
|
||||||
|
|
||||||
|
Generate Linux commands from natural language. Supports Ollama and Proxy backends, system prompts, different explanation levels (v/vv/vvv), and JSON history.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Build from source:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone --depth 1 https://github.com/Direct-Dev-Ru/linux-command-gpt.git ~/.linux-command-gpt
|
||||||
|
cd ~/.linux-command-gpt
|
||||||
|
go build -o lcg
|
||||||
|
|
||||||
|
# Add to your PATH
|
||||||
|
ln -s ~/.linux-command-gpt/lcg ~/.local/bin
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
lcg "I want to extract linux-command-gpt.tar.gz file"
|
||||||
|
```
|
||||||
|
|
||||||
|
After generation you will see a CAPS warning that the answer is from AI and must be verified, the command, and the action menu:
|
||||||
|
|
||||||
|
```text
|
||||||
|
ACTIONS: (c)opy, (s)ave, (r)egenerate, (e)xecute, (v|vv|vvv)explain, (n)othing
|
||||||
|
```
|
||||||
|
|
||||||
|
Explanations:
|
||||||
|
|
||||||
|
- `v` — short; `vv` — medium; `vvv` — detailed with alternatives.
|
||||||
|
|
||||||
|
Clipboard support requires `xclip` or `xsel`.
|
||||||
|
|
||||||
|
## What's new in 2.0.1
|
||||||
|
|
||||||
|
- Mobile UI improvements: better responsiveness (buttons, fonts, spacing) and reduced motion support
|
||||||
|
- Public REST endpoint: `POST /execute` (curl-only) for programmatic access — see `API_GUIDE.md`
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
|
||||||
|
- `LCG_PROVIDER` (default `ollama`) — provider type: `ollama` or `proxy`
|
||||||
|
- `LCG_HOST` (default `http://192.168.87.108:11434/`) — base API URL
|
||||||
|
- `LCG_MODEL` (default `hf.co/yandex/YandexGPT-5-Lite-8B-instruct-GGUF:Q4_K_M`)
|
||||||
|
- `LCG_PROMPT` — default system prompt content
|
||||||
|
- `LCG_PROXY_URL` (default `/api/v1/protected/sberchat/chat`) — proxy chat endpoint
|
||||||
|
- `LCG_COMPLETIONS_PATH` (default `api/chat`) — Ollama chat endpoint (relative)
|
||||||
|
- `LCG_TIMEOUT` (default `300`) — request timeout in seconds
|
||||||
|
- `LCG_RESULT_FOLDER` (default `~/.config/lcg/gpt_results`) — folder for saved results
|
||||||
|
- `LCG_RESULT_HISTORY` (default `$(LCG_RESULT_FOLDER)/lcg_history.json`) — JSON history path
|
||||||
|
- `LCG_PROMPT_FOLDER` (default `~/.config/lcg/gpt_sys_prompts`) — folder for system prompts
|
||||||
|
- `LCG_PROMPT_ID` (default `1`) — default system prompt ID
|
||||||
|
- `LCG_BROWSER_PATH` — custom browser executable path for `--browser` flag
|
||||||
|
- `LCG_JWT_TOKEN` — JWT token for proxy provider
|
||||||
|
- `LCG_NO_HISTORY` — if `1`/`true`, disables history writes for the process
|
||||||
|
- `LCG_ALLOW_EXECUTION` — if `1`/`true`, enables command execution via `(e)` action menu
|
||||||
|
- `LCG_SERVER_PORT` (default `8080`), `LCG_SERVER_HOST` (default `localhost`) — HTTP server settings
|
||||||
|
|
||||||
|
## Flags
|
||||||
|
|
||||||
|
- `--file, -f` read part of prompt from file
|
||||||
|
- `--sys, -s` system prompt content or ID
|
||||||
|
- `--prompt-id, --pid` choose built-in prompt (1–5)
|
||||||
|
- `--timeout, -t` request timeout (sec)
|
||||||
|
- `--no-history, --nh` disable writing/updating JSON history for this run
|
||||||
|
- `--debug, -d` show debug information (request parameters and prompts)
|
||||||
|
- `--version, -v` print version; `--help, -h` help
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
- `models`, `health`, `config`
|
||||||
|
- `prompts list|add|delete`
|
||||||
|
- `test-prompt <prompt-id> <command>`
|
||||||
|
- `update-jwt`, `delete-jwt` (proxy)
|
||||||
|
- `update-key`, `delete-key` (not needed for ollama/proxy)
|
||||||
|
- `history list` — list history from JSON
|
||||||
|
- `history view <index>` — view by index
|
||||||
|
- `history delete <index>` — delete by index (re-numbering)
|
||||||
|
- `serve` — start HTTP server to browse saved results (`--port`, `--host`, `--browser`)
|
||||||
|
- `/run` — web interface for executing requests
|
||||||
|
- `/execute` — API endpoint for programmatic access via curl
|
||||||
|
|
||||||
|
## Saving results
|
||||||
|
|
||||||
|
Files are saved to `LCG_RESULT_FOLDER` (default `~/.config/lcg/gpt_results`).
|
||||||
|
|
||||||
|
- Command result: `gpt_request_<MODEL>_YYYY-MM-DD_HH-MM-SS.md`
|
||||||
|
- `# <title>` — H1 with original request (trimmed to 120 chars: first 116 + `...`)
|
||||||
|
- `## Prompt`
|
||||||
|
- `## Response`
|
||||||
|
|
||||||
|
- Detailed explanation: `gpt_explanation_<MODEL>_YYYY-MM-DD_HH-MM-SS.md`
|
||||||
|
- `# <title>`
|
||||||
|
- `## Prompt`
|
||||||
|
- `## Command`
|
||||||
|
- `## Explanation and Alternatives (model: <MODEL>)`
|
||||||
|
|
||||||
|
## History
|
||||||
|
|
||||||
|
- Stored as JSON array in `LCG_RESULT_HISTORY`.
|
||||||
|
- On new request, if the same command exists, you will be prompted to view or overwrite.
|
||||||
|
- Showing from history does not call the API; the standard action menu is shown.
|
||||||
|
|
||||||
|
## Browser Integration
|
||||||
|
|
||||||
|
The `serve` command supports automatic browser opening:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start server and open browser automatically
|
||||||
|
lcg serve --browser
|
||||||
|
|
||||||
|
# Use custom browser
|
||||||
|
export LCG_BROWSER_PATH="/usr/bin/firefox"
|
||||||
|
lcg serve --browser
|
||||||
|
|
||||||
|
# Start on custom host/port with browser
|
||||||
|
lcg serve --host 0.0.0.0 --port 9000 --browser
|
||||||
|
```
|
||||||
|
|
||||||
|
Supported browsers (in priority order):
|
||||||
|
|
||||||
|
- Yandex Browser (`yandex-browser`, `yandex-browser-stable`)
|
||||||
|
- Mozilla Firefox (`firefox`, `firefox-esr`)
|
||||||
|
- Google Chrome (`google-chrome`, `google-chrome-stable`)
|
||||||
|
- Chromium (`chromium`, `chromium-browser`)
|
||||||
|
|
||||||
|
## API Access
|
||||||
|
|
||||||
|
The `serve` command provides both a web interface and REST API:
|
||||||
|
|
||||||
|
**Web Interface:**
|
||||||
|
|
||||||
|
- Browse results at `http://localhost:8080/`
|
||||||
|
- Execute requests at `http://localhost:8080/run`
|
||||||
|
- Manage prompts at `http://localhost:8080/prompts`
|
||||||
|
- View history at `http://localhost:8080/history`
|
||||||
|
|
||||||
|
**REST API:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start server
|
||||||
|
lcg serve
|
||||||
|
|
||||||
|
# Make API request
|
||||||
|
curl -X POST http://localhost:8080/execute \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"prompt": "create directory test", "verbose": "vv"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"command": "mkdir test",
|
||||||
|
"explanation": "The mkdir command creates a new directory...",
|
||||||
|
"model": "hf.co/yandex/YandexGPT-5-Lite-8B-instruct-GGUF:Q4_K_M",
|
||||||
|
"elapsed": 1.23
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For complete API documentation, see `API_GUIDE.md`.
|
||||||
|
|
||||||
|
For full guide in Russian, see `USAGE_GUIDE.md`.
|
||||||
337
docs/RELEASE_GUIDE.md
Normal file
337
docs/RELEASE_GUIDE.md
Normal file
@@ -0,0 +1,337 @@
|
|||||||
|
# 🚀 Гайд по созданию релизов с помощью GoReleaser
|
||||||
|
|
||||||
|
Этот документ описывает процесс создания релизов для проекта `linux-command-gpt` с использованием GoReleaser.
|
||||||
|
|
||||||
|
## 📋 Содержание
|
||||||
|
|
||||||
|
- [Установка GoReleaser](#установка-goreleaser)
|
||||||
|
- [Конфигурация](#конфигурация)
|
||||||
|
- [Процесс создания релиза](#процесс-создания-релиза)
|
||||||
|
- [Автоматизация](#автоматизация)
|
||||||
|
- [Устранение проблем](#устранение-проблем)
|
||||||
|
|
||||||
|
## 🔧 Установка GoReleaser
|
||||||
|
|
||||||
|
### Linux/macOS
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Скачать и установить последнюю версию
|
||||||
|
curl -sL https://git.io/goreleaser | bash
|
||||||
|
|
||||||
|
# Или через Homebrew (macOS)
|
||||||
|
brew install goreleaser
|
||||||
|
|
||||||
|
# Или через Go
|
||||||
|
go install github.com/goreleaser/goreleaser@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Через Chocolatey
|
||||||
|
choco install goreleaser
|
||||||
|
|
||||||
|
# Или скачать с GitHub Releases
|
||||||
|
# https://github.com/goreleaser/goreleaser/releases
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚙️ Конфигурация
|
||||||
|
|
||||||
|
### Файл `.goreleaser.yaml`
|
||||||
|
|
||||||
|
В проекте используется следующая конфигурация GoReleaser:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
before:
|
||||||
|
hooks:
|
||||||
|
- go mod tidy
|
||||||
|
- go generate ./...
|
||||||
|
|
||||||
|
builds:
|
||||||
|
- binary: lcg
|
||||||
|
env:
|
||||||
|
- CGO_ENABLED=0
|
||||||
|
goarch:
|
||||||
|
- amd64
|
||||||
|
- arm64
|
||||||
|
- arm
|
||||||
|
goos:
|
||||||
|
- linux
|
||||||
|
- darwin
|
||||||
|
|
||||||
|
archives:
|
||||||
|
- formats: [tar.gz]
|
||||||
|
name_template: >-
|
||||||
|
{{ .ProjectName }}_
|
||||||
|
{{- title .Os }}_
|
||||||
|
{{- if eq .Arch "amd64" }}x86_64
|
||||||
|
{{- else if eq .Arch "386" }}i386
|
||||||
|
{{- else }}{{ .Arch }}{{ end }}
|
||||||
|
{{- if .Arm }}v{{ .Arm }}{{ end }}
|
||||||
|
format_overrides:
|
||||||
|
- goos: windows
|
||||||
|
formats: [zip]
|
||||||
|
|
||||||
|
changelog:
|
||||||
|
sort: asc
|
||||||
|
filters:
|
||||||
|
exclude:
|
||||||
|
- "^docs:"
|
||||||
|
- "^test:"
|
||||||
|
|
||||||
|
release:
|
||||||
|
footer: >-
|
||||||
|
---
|
||||||
|
Released by [GoReleaser](https://github.com/goreleaser/goreleaser).
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ключевые настройки
|
||||||
|
|
||||||
|
- **builds**: Сборка для Linux, macOS, Windows (amd64, arm64, arm)
|
||||||
|
- **archives**: Создание архивов tar.gz для Unix и zip для Windows
|
||||||
|
- **changelog**: Автоматическое создание changelog из git commits
|
||||||
|
- **release**: Настройки GitHub релиза
|
||||||
|
|
||||||
|
## 🚀 Процесс создания релиза
|
||||||
|
|
||||||
|
### 1. Подготовка
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Убедитесь, что все изменения закоммичены
|
||||||
|
git status
|
||||||
|
|
||||||
|
# Обновите версию в VERSION.txt
|
||||||
|
echo "v2.0.2" > VERSION.txt
|
||||||
|
|
||||||
|
# Создайте тег
|
||||||
|
git tag v2.0.2
|
||||||
|
git push origin v2.0.2
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Настройка переменных окружения
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Установите GitHub токен
|
||||||
|
export GITHUB_TOKEN="your_github_token_here"
|
||||||
|
|
||||||
|
# Или создайте файл .env
|
||||||
|
echo "GITHUB_TOKEN=your_github_token_here" > .env
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Создание релиза
|
||||||
|
|
||||||
|
#### Полный релиз
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Создать релиз с загрузкой на GitHub
|
||||||
|
goreleaser release
|
||||||
|
|
||||||
|
# Создать релиз без загрузки (только локально)
|
||||||
|
goreleaser release --clean
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Тестовый релиз (snapshot)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Создать тестовую сборку
|
||||||
|
goreleaser release --snapshot
|
||||||
|
|
||||||
|
# Тестовая сборка без загрузки
|
||||||
|
goreleaser release --snapshot --clean
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Проверка результатов
|
||||||
|
|
||||||
|
После выполнения команды GoReleaser создаст:
|
||||||
|
|
||||||
|
- **Архивы**: `dist/` - готовые архивы для всех платформ
|
||||||
|
- **Чексуммы**: `dist/checksums.txt` - контрольные суммы файлов
|
||||||
|
- **GitHub релиз**: Автоматически созданный релиз на GitHub
|
||||||
|
|
||||||
|
## 🤖 Автоматизация
|
||||||
|
|
||||||
|
### GitHub Actions
|
||||||
|
|
||||||
|
Создайте файл `.github/workflows/release.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: Release
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: '1.21'
|
||||||
|
- name: Run GoReleaser
|
||||||
|
uses: goreleaser/goreleaser-action@v4
|
||||||
|
with:
|
||||||
|
distribution: goreleaser
|
||||||
|
version: latest
|
||||||
|
args: release
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Локальные скрипты
|
||||||
|
|
||||||
|
В проекте есть готовые скрипты:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Предварительная подготовка
|
||||||
|
./shell-code/pre-release.sh
|
||||||
|
|
||||||
|
# Создание релиза
|
||||||
|
./shell-code/release.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📁 Структура релиза
|
||||||
|
|
||||||
|
После создания релиза в директории `dist/` будут созданы:
|
||||||
|
|
||||||
|
```
|
||||||
|
dist/
|
||||||
|
├── artifacts.json # Метаданные артефактов
|
||||||
|
├── CHANGELOG.md # Автоматически созданный changelog
|
||||||
|
├── config.yaml # Конфигурация GoReleaser
|
||||||
|
├── digests.txt # Хеши файлов
|
||||||
|
├── go-lcg_2.0.1_checksums.txt
|
||||||
|
├── go-lcg_Darwin_arm64.tar.gz
|
||||||
|
├── go-lcg_Darwin_x86_64.tar.gz
|
||||||
|
├── go-lcg_Linux_arm64.tar.gz
|
||||||
|
├── go-lcg_Linux_i386.tar.gz
|
||||||
|
├── go-lcg_Linux_x86_64.tar.gz
|
||||||
|
├── go-lcg_Windows_arm64.zip
|
||||||
|
├── go-lcg_Windows_i386.zip
|
||||||
|
├── go-lcg_Windows_x86_64.zip
|
||||||
|
└── metadata.json # Метаданные релиза
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔍 Устранение проблем
|
||||||
|
|
||||||
|
### Правильные флаги GoReleaser
|
||||||
|
|
||||||
|
**Важно**: В современных версиях GoReleaser флаг `--skip-publish` больше не поддерживается. Используйте:
|
||||||
|
|
||||||
|
- `--clean` - очищает директорию `dist/` перед сборкой
|
||||||
|
- `--snapshot` - создает тестовую сборку без создания тега
|
||||||
|
- `--debug` - подробный вывод для отладки
|
||||||
|
- `--skip-validate` - пропускает валидацию конфигурации
|
||||||
|
|
||||||
|
### Частые ошибки
|
||||||
|
|
||||||
|
#### 1. Ошибка аутентификации GitHub
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: failed to get GitHub token: missing github token
|
||||||
|
```
|
||||||
|
|
||||||
|
**Решение**: Установите `GITHUB_TOKEN` в переменные окружения.
|
||||||
|
|
||||||
|
#### 2. Ошибка создания тега
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: git tag v1.0.0 already exists
|
||||||
|
```
|
||||||
|
|
||||||
|
**Решение**: Удалите существующий тег или используйте другую версию.
|
||||||
|
|
||||||
|
#### 3. Ошибка сборки
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: failed to build for linux/amd64
|
||||||
|
```
|
||||||
|
|
||||||
|
**Решение**: Проверьте, что код компилируется локально:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go build -o lcg .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Отладка
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Подробный вывод
|
||||||
|
goreleaser release --debug
|
||||||
|
|
||||||
|
# Проверка конфигурации
|
||||||
|
goreleaser check
|
||||||
|
|
||||||
|
# Только сборка без релиза
|
||||||
|
goreleaser build
|
||||||
|
|
||||||
|
# Создание релиза без публикации (только локальная сборка)
|
||||||
|
goreleaser release --clean
|
||||||
|
|
||||||
|
# Создание snapshot релиза без публикации
|
||||||
|
goreleaser release --snapshot --clean
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 Лучшие практики
|
||||||
|
|
||||||
|
### 1. Версионирование
|
||||||
|
|
||||||
|
- Используйте семантическое версионирование (SemVer)
|
||||||
|
- Обновляйте `VERSION.txt` перед созданием релиза
|
||||||
|
- Создавайте теги в формате `v1.0.0`
|
||||||
|
|
||||||
|
### 2. Changelog
|
||||||
|
|
||||||
|
- Пишите понятные commit messages
|
||||||
|
- Используйте conventional commits для автоматического changelog
|
||||||
|
- Исключайте технические коммиты из changelog
|
||||||
|
|
||||||
|
### 3. Тестирование
|
||||||
|
|
||||||
|
- Всегда тестируйте snapshot релизы перед полным релизом
|
||||||
|
- Проверяйте сборки на разных платформах
|
||||||
|
- Тестируйте установку из релиза
|
||||||
|
|
||||||
|
### 4. Безопасность
|
||||||
|
|
||||||
|
- Никогда не коммитьте токены в репозиторий
|
||||||
|
- Используйте GitHub Secrets для CI/CD
|
||||||
|
- Регулярно обновляйте токены доступа
|
||||||
|
|
||||||
|
## 🎯 Пример полного процесса
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Подготовка
|
||||||
|
git checkout main
|
||||||
|
git pull origin main
|
||||||
|
|
||||||
|
# 2. Обновление версии
|
||||||
|
echo "v2.0.2" > VERSION.txt
|
||||||
|
git add VERSION.txt
|
||||||
|
git commit -m "chore: bump version to v2.0.2"
|
||||||
|
|
||||||
|
# 3. Создание тега
|
||||||
|
git tag v2.0.2
|
||||||
|
git push origin v2.0.2
|
||||||
|
|
||||||
|
# 4. Создание релиза
|
||||||
|
export GITHUB_TOKEN="your_token"
|
||||||
|
goreleaser release
|
||||||
|
|
||||||
|
# 5. Проверка
|
||||||
|
ls -la dist/
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📚 Дополнительные ресурсы
|
||||||
|
|
||||||
|
- [Официальная документация GoReleaser](https://goreleaser.com/)
|
||||||
|
- [Примеры конфигураций](https://github.com/goreleaser/goreleaser/tree/main/examples)
|
||||||
|
- [GitHub Actions для GoReleaser](https://github.com/goreleaser/goreleaser-action)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Примечание**: Этот гайд создан специально для проекта `linux-command-gpt`. Для других проектов может потребоваться адаптация конфигурации.
|
||||||
258
docs/REVERSE_PROXY_GUIDE.md
Normal file
258
docs/REVERSE_PROXY_GUIDE.md
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
# 🔄 Гайд по настройке LCG за Reverse Proxy
|
||||||
|
|
||||||
|
## 📋 Переменные окружения для Reverse Proxy
|
||||||
|
|
||||||
|
### 🔧 **Основные настройки:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Включить аутентификацию
|
||||||
|
LCG_SERVER_REQUIRE_AUTH=true
|
||||||
|
|
||||||
|
# Настроить домен для cookies (опционально)
|
||||||
|
LCG_DOMAIN=.yourdomain.com
|
||||||
|
|
||||||
|
# Настроить путь для cookies (для префикса пути)
|
||||||
|
LCG_COOKIE_PATH=/lcg
|
||||||
|
|
||||||
|
# Управление Secure флагом cookies
|
||||||
|
LCG_COOKIE_SECURE=false
|
||||||
|
|
||||||
|
# Разрешить HTTP (для работы за reverse proxy)
|
||||||
|
LCG_SERVER_ALLOW_HTTP=true
|
||||||
|
|
||||||
|
# Настроить хост и порт
|
||||||
|
LCG_SERVER_HOST=0.0.0.0
|
||||||
|
LCG_SERVER_PORT=8080
|
||||||
|
|
||||||
|
# Пароль для входа (по умолчанию: admin#123456)
|
||||||
|
LCG_SERVER_PASSWORD=your_secure_password
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 **Запуск за Reverse Proxy**
|
||||||
|
|
||||||
|
### **1. Nginx конфигурация:**
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name yourdomain.com;
|
||||||
|
|
||||||
|
# SSL настройки
|
||||||
|
ssl_certificate /path/to/cert.pem;
|
||||||
|
ssl_certificate_key /path/to/key.pem;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://localhost:8080;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
# Важно для работы cookies
|
||||||
|
proxy_cookie_domain localhost yourdomain.com;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### **2. Apache конфигурация:**
|
||||||
|
|
||||||
|
```apache
|
||||||
|
<VirtualHost *:443>
|
||||||
|
ServerName yourdomain.com
|
||||||
|
SSLEngine on
|
||||||
|
SSLCertificateFile /path/to/cert.pem
|
||||||
|
SSLCertificateKeyFile /path/to/key.pem
|
||||||
|
|
||||||
|
ProxyPreserveHost On
|
||||||
|
ProxyPass / http://localhost:8080/
|
||||||
|
ProxyPassReverse / http://localhost:8080/
|
||||||
|
|
||||||
|
# Настройки для cookies
|
||||||
|
ProxyPassReverseCookieDomain localhost yourdomain.com
|
||||||
|
</VirtualHost>
|
||||||
|
```
|
||||||
|
|
||||||
|
### **3. Caddy конфигурация:**
|
||||||
|
|
||||||
|
```caddy
|
||||||
|
yourdomain.com {
|
||||||
|
reverse_proxy localhost:8080 {
|
||||||
|
header_up Host {host}
|
||||||
|
header_up X-Real-IP {remote}
|
||||||
|
header_up X-Forwarded-For {remote}
|
||||||
|
header_up X-Forwarded-Proto {scheme}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🏃♂️ **Команды запуска**
|
||||||
|
|
||||||
|
### **Базовый запуск:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
LCG_SERVER_REQUIRE_AUTH=true LCG_SERVER_ALLOW_HTTP=true ./lcg serve -H 0.0.0.0 -p 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
### **С настройкой домена:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
LCG_SERVER_REQUIRE_AUTH=true \
|
||||||
|
LCG_SERVER_ALLOW_HTTP=true \
|
||||||
|
LCG_DOMAIN=.yourdomain.com \
|
||||||
|
LCG_COOKIE_SECURE=false \
|
||||||
|
./lcg serve -H 0.0.0.0 -p 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
### **С кастомным паролем:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
LCG_SERVER_REQUIRE_AUTH=true \
|
||||||
|
LCG_SERVER_ALLOW_HTTP=true \
|
||||||
|
LCG_SERVER_PASSWORD=my_secure_password \
|
||||||
|
LCG_DOMAIN=.yourdomain.com \
|
||||||
|
./lcg serve -H 0.0.0.0 -p 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔒 **Безопасность**
|
||||||
|
|
||||||
|
### **Рекомендуемые настройки:**
|
||||||
|
|
||||||
|
- ✅ `LCG_SERVER_REQUIRE_AUTH=true` - всегда включайте аутентификацию
|
||||||
|
- ✅ `LCG_COOKIE_SECURE=false` - для HTTP за reverse proxy
|
||||||
|
- ✅ `LCG_DOMAIN=.yourdomain.com` - для правильной работы cookies
|
||||||
|
- ✅ Сильный пароль в `LCG_SERVER_PASSWORD`
|
||||||
|
|
||||||
|
### **Настройки Reverse Proxy:**
|
||||||
|
|
||||||
|
- ✅ Передавайте заголовки `X-Forwarded-*`
|
||||||
|
- ✅ Настройте `proxy_cookie_domain` в Nginx
|
||||||
|
- ✅ Используйте HTTPS на уровне reverse proxy
|
||||||
|
|
||||||
|
## 🐳 **Docker Compose пример**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
lcg:
|
||||||
|
image: your-lcg-image
|
||||||
|
environment:
|
||||||
|
- LCG_SERVER_REQUIRE_AUTH=true
|
||||||
|
- LCG_SERVER_ALLOW_HTTP=true
|
||||||
|
- LCG_DOMAIN=.yourdomain.com
|
||||||
|
- LCG_COOKIE_SECURE=false
|
||||||
|
- LCG_SERVER_PASSWORD=secure_password
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
image: nginx:alpine
|
||||||
|
volumes:
|
||||||
|
- ./nginx.conf:/etc/nginx/nginx.conf
|
||||||
|
- ./ssl:/etc/nginx/ssl
|
||||||
|
ports:
|
||||||
|
- "443:443"
|
||||||
|
depends_on:
|
||||||
|
- lcg
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔍 **Диагностика проблем**
|
||||||
|
|
||||||
|
### **Проверка cookies:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Проверить установку cookies
|
||||||
|
curl -I https://yourdomain.com/login
|
||||||
|
|
||||||
|
# Проверить домен cookies
|
||||||
|
curl -v https://yourdomain.com/login 2>&1 | grep -i cookie
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Логи приложения:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Запуск с debug режимом
|
||||||
|
LCG_SERVER_REQUIRE_AUTH=true \
|
||||||
|
LCG_SERVER_ALLOW_HTTP=true \
|
||||||
|
./lcg -d serve -H 0.0.0.0 -p 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 **Примечания**
|
||||||
|
|
||||||
|
- **SameSite=Lax** - более мягкий режим для reverse proxy
|
||||||
|
- **Domain cookies** - работают только с указанным доменом
|
||||||
|
- **Secure=false** - обязательно для HTTP за reverse proxy
|
||||||
|
- **X-Forwarded-* заголовки** - важны для правильной работы
|
||||||
|
|
||||||
|
## 🆘 **Частые проблемы**
|
||||||
|
|
||||||
|
1. **Cookies не работают** → Проверьте `LCG_DOMAIN` и настройки reverse proxy
|
||||||
|
2. **Ошибка 403 CSRF** → Проверьте передачу cookies через reverse proxy
|
||||||
|
3. **Не работает аутентификация** → Убедитесь что `LCG_SERVER_REQUIRE_AUTH=true`
|
||||||
|
4. **Проблемы с HTTPS** → Настройте `LCG_COOKIE_SECURE=false` для HTTP за reverse proxy
|
||||||
|
|
||||||
|
## 🛣️ **Конфигурация с префиксом пути**
|
||||||
|
|
||||||
|
### **Пример: example.com/lcg**
|
||||||
|
|
||||||
|
#### **Переменные окружения для префикса:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
LCG_SERVER_REQUIRE_AUTH=true \
|
||||||
|
LCG_SERVER_ALLOW_HTTP=true \
|
||||||
|
LCG_DOMAIN=.example.com \
|
||||||
|
LCG_COOKIE_PATH=/lcg \
|
||||||
|
LCG_COOKIE_SECURE=false \
|
||||||
|
./lcg serve -H 0.0.0.0 -p 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **Nginx с префиксом:**
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name example.com;
|
||||||
|
|
||||||
|
location /lcg/ {
|
||||||
|
proxy_pass http://localhost:8080/;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
# Важно для работы cookies с префиксом
|
||||||
|
proxy_cookie_domain localhost example.com;
|
||||||
|
proxy_cookie_path / /lcg/;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **Apache с префиксом:**
|
||||||
|
|
||||||
|
```apache
|
||||||
|
<VirtualHost *:443>
|
||||||
|
ServerName example.com
|
||||||
|
SSLEngine on
|
||||||
|
|
||||||
|
ProxyPreserveHost On
|
||||||
|
ProxyPass /lcg/ http://localhost:8080/
|
||||||
|
ProxyPassReverse /lcg/ http://localhost:8080/
|
||||||
|
|
||||||
|
# Настройки для cookies с префиксом
|
||||||
|
ProxyPassReverseCookieDomain localhost example.com
|
||||||
|
ProxyPassReverseCookiePath / /lcg/
|
||||||
|
</VirtualHost>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **Caddy с префиксом:**
|
||||||
|
|
||||||
|
```caddy
|
||||||
|
example.com {
|
||||||
|
reverse_proxy /lcg/* localhost:8080 {
|
||||||
|
header_up Host {host}
|
||||||
|
header_up X-Real-IP {remote}
|
||||||
|
header_up X-Forwarded-For {remote}
|
||||||
|
header_up X-Forwarded-Proto {scheme}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
118
docs/ROADMAP.md
Normal file
118
docs/ROADMAP.md
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
# Дорожная карта развития (функциональность и безопасность)
|
||||||
|
|
||||||
|
Документ описывает план развития проекта на ближайшие релизы с фокусом на улучшение функциональности и усиление безопасности.
|
||||||
|
|
||||||
|
## Принципы
|
||||||
|
|
||||||
|
- Безопасность по умолчанию: новые возможности включают безопасные дефолты, опционально ослабляются.
|
||||||
|
- Обратная совместимость: не ломать существующие сценарии CLI и API.
|
||||||
|
- Прозрачность: чёткие Changelog, версии по SemVer, миграции и откаты.
|
||||||
|
- Качество: тесты, линтеры, аудит зависимостей, автоматизация релизов.
|
||||||
|
|
||||||
|
## Вехи и цели
|
||||||
|
|
||||||
|
### v2.1.0 — Формализация API и UX улучшения
|
||||||
|
|
||||||
|
- REST API
|
||||||
|
- Описать `POST /execute` в OpenAPI (swagger.yaml/json) и приложить в репозитории.
|
||||||
|
- Валидация входа по схеме: обязательные поля, ограничения длины, лимит размера тела.
|
||||||
|
- Явные коды ошибок и структура ответа (коды/сообщения).
|
||||||
|
- Безопасность API (первый этап)
|
||||||
|
- Дополнить защиту: ограничение размера тела (например, 64KB), тайм-ауты на чтение/запись.
|
||||||
|
- Rate limit (встроенный простой токен-бакет, по IP). Конфиг через env.
|
||||||
|
- Логирование попыток доступа и ошибок API (с редактированием PII).
|
||||||
|
- Веб-интерфейс
|
||||||
|
- Улучшения мобильной версии (доступность, контраст, a11y-метки).
|
||||||
|
- Переключатель темы (light/dark), сохранение предпочтений.
|
||||||
|
- Промпты
|
||||||
|
- Экспорт/импорт системных промптов (JSON) из UI/CLI.
|
||||||
|
- Превью при редактировании промптов в UI.
|
||||||
|
- Документация
|
||||||
|
- `API_GUIDE.md`: синхронизировать с OpenAPI.
|
||||||
|
- `USAGE_GUIDE.md`: добавить раздел «Ограничения API и лимиты».
|
||||||
|
|
||||||
|
### v2.2.0 — Усиление безопасности и управление доступом
|
||||||
|
|
||||||
|
- Аутентификация/Авторизация для веб-сервера
|
||||||
|
- Ввести токен доступа для API: `LCG_SERVER_TOKEN` (Bearer), отключаемо.
|
||||||
|
- Сессии UI (опционально): cookie HttpOnly + SameSite=strict, CSRF-защита форм.
|
||||||
|
- CORS: явный список разрешённых Origin через `LCG_CORS_ORIGINS`.
|
||||||
|
- Транспорт и заголовки безопасности
|
||||||
|
- Рекомендации по TLS терминации (пример конфигов nginx/caddy) в `serve/README.md`.
|
||||||
|
- Security headers: CSP, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, HSTS (при HTTPS).
|
||||||
|
- Хранилище и история
|
||||||
|
- Опциональное шифрование истории на диске (`LCG_HISTORY_ENCRYPTION_KEY_FILE`).
|
||||||
|
- Права на файлы истории и результатов: 0600, директории 0700.
|
||||||
|
- Настраиваемая ретенция истории (дни/размер). Авто-очистка.
|
||||||
|
- Наблюдаемость
|
||||||
|
- Аудит-лог действий UI/API с маскированием чувствительных данных.
|
||||||
|
- Включаемые/отключаемые метрики (prometheus endpoint — опционально, по отдельному порту/токену).
|
||||||
|
|
||||||
|
### v2.3.0 — Расширяемость и производительность
|
||||||
|
|
||||||
|
- Плагины провайдеров
|
||||||
|
- Интерфейс адаптеров (провайдеры LLM), регистрация через конфиг/билд-теги.
|
||||||
|
- Образцы адаптеров и гайды по разработке.
|
||||||
|
- Производительность
|
||||||
|
- Пулы HTTP-клиентов, connection reuse, упреждающие таймауты на уровне контекста.
|
||||||
|
- Кэширование результатов подробных объяснений (опционально, по ключу запроса).
|
||||||
|
- Расширения API
|
||||||
|
- Пакетная обработка запросов (batch) с квотой.
|
||||||
|
- Пагинация и фильтрация для `/history` (если будет публичный REST).
|
||||||
|
- Дистрибуция
|
||||||
|
- Улучшения .goreleaser: публикация SBOM, подписи (cosign), детерминированные сборки.
|
||||||
|
- Готовые пакеты: deb/rpm, инструкции для brew/scoop (по возможности).
|
||||||
|
|
||||||
|
## Backlog (кандидаты)
|
||||||
|
|
||||||
|
- Потоковая генерация (stream) и WebSocket-канал (при наличии поддержки у провайдеров).
|
||||||
|
- Оффлайн-режим/кэширование моделей для локальных провайдеров.
|
||||||
|
- Расширенный поиск по результатам/истории, теги и сохранённые фильтры.
|
||||||
|
- Резервное копирование и восстановление каталога результатов/истории.
|
||||||
|
- Улучшение доступности (a11y), горячие клавиши, локализация интерфейса.
|
||||||
|
|
||||||
|
## Техническое качество
|
||||||
|
|
||||||
|
- Обновление стека
|
||||||
|
- Обновить Go (минимум 1.20+), пересобрать и протестировать совместимость.
|
||||||
|
- Регулярные обновления зависимостей и проверка уязвимостей (`govulncheck`).
|
||||||
|
- Линтеры и проверка качества
|
||||||
|
- Включить `golangci-lint`, `staticcheck`, `gosec` в CI.
|
||||||
|
- Форматирование и единый стиль, pre-commit хуки.
|
||||||
|
- Тесты
|
||||||
|
- Unit-тесты на `serve/*` (маршруты, валидация входных данных, заголовки).
|
||||||
|
- Интеграционные тесты API `/execute` (позитив/негатив, лимиты, токены).
|
||||||
|
- Фаззинг критичных функций парсинга/валидации.
|
||||||
|
- CI/CD
|
||||||
|
- GitHub Actions: сборка, тесты, линты, релизы. Генерация чек-сумм, подписи, SBOM.
|
||||||
|
- Автоматическая публикация релизов и проверок артефактов.
|
||||||
|
|
||||||
|
## Конфигурация (новые/уточняемые переменные)
|
||||||
|
|
||||||
|
- `LCG_SERVER_TOKEN` — токен доступа для API (Bearer). Отключаемый режим.
|
||||||
|
- `LCG_RATE_LIMIT` — глобальные лимиты (например, `60/m`, `5/s`).
|
||||||
|
- `LCG_CORS_ORIGINS` — список разрешённых Origin.
|
||||||
|
- `LCG_HISTORY_ENCRYPTION_KEY_FILE` — путь к ключу для шифрования истории (опц.).
|
||||||
|
- `LCG_MAX_BODY_BYTES` — максимальный размер тела запроса, байты (по умолчанию 65536).
|
||||||
|
- `LCG_BROWSER_PATH` — путь к браузеру для `--browser`.
|
||||||
|
|
||||||
|
## Политика релизов
|
||||||
|
|
||||||
|
- SemVer: MINOR — функционал без ломаний, PATCH — багфиксы/мелкие улучшения.
|
||||||
|
- Каждый релиз: обновлённый `CHANGELOG.txt`, теги `vX.Y.Z`, двуязычная документация (RU/EN при возможности).
|
||||||
|
- Security Advisories: отдельный раздел/ISSUE шаблон для отчётов об уязвимостях.
|
||||||
|
|
||||||
|
## Критерии приемки (примеры)
|
||||||
|
|
||||||
|
- v2.1.0: OpenAPI спецификация доступна, API валидируется по схеме, лимит размера тела и таймауты соблюдаются, добавлены тесты в CI.
|
||||||
|
- v2.2.0: Доступ к API с токеном включаем/отключаем через env; активированы security-заголовки; есть базовые правила CORS; аудит-лог включается флагом.
|
||||||
|
- v2.3.0: Пулы клиентов, бенчмарки показывают улучшение p95 латентности, есть механизм подключения новых провайдеров.
|
||||||
|
|
||||||
|
## Риски и смягчение
|
||||||
|
|
||||||
|
- Ломание совместимости при усилении безопасности → режим совместимости через env/флаги.
|
||||||
|
- Рост сложности конфигурации → шаблоны конфигов и «рецепты» в README/serve/README.md.
|
||||||
|
- Производительные регрессии из-за валидации/лимитов → профилирование и кэширование на горячих путях.
|
||||||
|
|
||||||
|
---
|
||||||
|
Последнее обновление: 2025-10-22
|
||||||
199
docs/SECURITY_FEATURES.md
Normal file
199
docs/SECURITY_FEATURES.md
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
# 🔒 Функции безопасности LCG
|
||||||
|
|
||||||
|
## 🛡️ Автоматическое принуждение к HTTPS
|
||||||
|
|
||||||
|
### Логика безопасности
|
||||||
|
|
||||||
|
Приложение автоматически определяет, нужно ли использовать HTTPS:
|
||||||
|
|
||||||
|
1. **Небезопасные хосты** (не localhost/127.0.0.1) → **принудительно HTTPS**
|
||||||
|
2. **Безопасные хосты** (localhost/127.0.0.1) → HTTP (если не указано иное)
|
||||||
|
3. **Переменная `LCG_SERVER_ALLOW_HTTP=true`** → разрешает HTTP для любых хостов
|
||||||
|
|
||||||
|
### Примеры
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Небезопасно - принудительно HTTPS
|
||||||
|
LCG_SERVER_HOST=192.168.1.100 lcg serve
|
||||||
|
# Результат: https://192.168.1.100:8080
|
||||||
|
|
||||||
|
# Безопасно - HTTP по умолчанию
|
||||||
|
LCG_SERVER_HOST=localhost lcg serve
|
||||||
|
# Результат: http://localhost:8080
|
||||||
|
|
||||||
|
# Принудительно HTTP для любого хоста
|
||||||
|
LCG_SERVER_HOST=192.168.1.100 LCG_SERVER_ALLOW_HTTP=true lcg serve
|
||||||
|
# Результат: http://192.168.1.100:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔐 SSL/TLS сертификаты
|
||||||
|
|
||||||
|
### Автоматическая генерация
|
||||||
|
|
||||||
|
Приложение автоматически генерирует самоподписанный сертификат если:
|
||||||
|
|
||||||
|
1. Не указаны переменные `LCG_SERVER_SSL_CERT_FILE` и `LCG_SERVER_SSL_KEY_FILE`
|
||||||
|
2. Не найдены файлы в `~/.config/lcg/server/ssl/cert.pem` и `~/.config/lcg/server/ssl/key.pem`
|
||||||
|
|
||||||
|
### Расположение сертификатов
|
||||||
|
|
||||||
|
``` text
|
||||||
|
~/.config/lcg/
|
||||||
|
├── config/
|
||||||
|
│ └── server/
|
||||||
|
│ └── ssl/
|
||||||
|
│ ├── cert.pem # Сертификат
|
||||||
|
│ └── key.pem # Приватный ключ
|
||||||
|
```
|
||||||
|
|
||||||
|
### Переменные окружения
|
||||||
|
|
||||||
|
| Переменная | Описание | По умолчанию |
|
||||||
|
|------------|----------|--------------|
|
||||||
|
| `LCG_CONFIG_FOLDER` | Папка конфигурации | `~/.config/lcg/config` |
|
||||||
|
| `LCG_SERVER_ALLOW_HTTP` | Разрешить HTTP для любых хостов | `false` |
|
||||||
|
| `LCG_SERVER_SSL_CERT_FILE` | Путь к сертификату | `""` (авто) |
|
||||||
|
| `LCG_SERVER_SSL_KEY_FILE` | Путь к ключу | `""` (авто) |
|
||||||
|
|
||||||
|
## 🚀 Примеры использования
|
||||||
|
|
||||||
|
### Безопасный режим (по умолчанию)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Локальный сервер - HTTP
|
||||||
|
lcg serve
|
||||||
|
|
||||||
|
# Внешний сервер - принудительно HTTPS
|
||||||
|
LCG_SERVER_HOST=192.168.1.100 lcg serve
|
||||||
|
```
|
||||||
|
|
||||||
|
### Настройка SSL сертификатов
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Использовать собственные сертификаты
|
||||||
|
LCG_SERVER_SSL_CERT_FILE=/path/to/cert.pem \
|
||||||
|
LCG_SERVER_SSL_KEY_FILE=/path/to/key.pem \
|
||||||
|
lcg serve
|
||||||
|
|
||||||
|
# Разрешить HTTP для внешних хостов
|
||||||
|
LCG_SERVER_HOST=192.168.1.100 \
|
||||||
|
LCG_SERVER_ALLOW_HTTP=true \
|
||||||
|
lcg serve
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker контейнер
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
FROM golang:1.21-alpine AS builder
|
||||||
|
# ... build steps ...
|
||||||
|
|
||||||
|
FROM alpine:latest
|
||||||
|
COPY --from=builder /app/lcg /usr/local/bin/
|
||||||
|
ENV LCG_SERVER_HOST=0.0.0.0
|
||||||
|
ENV LCG_SERVER_ALLOW_HTTP=false
|
||||||
|
CMD ["lcg", "serve"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Systemd сервис
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=LCG Server
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=lcg
|
||||||
|
WorkingDirectory=/opt/lcg
|
||||||
|
ExecStart=/opt/lcg/lcg serve
|
||||||
|
Environment=LCG_SERVER_HOST=0.0.0.0
|
||||||
|
Environment=LCG_SERVER_ALLOW_HTTP=false
|
||||||
|
Restart=always
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Технические детали
|
||||||
|
|
||||||
|
### Генерация сертификата
|
||||||
|
|
||||||
|
Самоподписанный сертификат генерируется с:
|
||||||
|
|
||||||
|
- **Размер ключа**: 2048 бит RSA
|
||||||
|
- **Срок действия**: 1 год
|
||||||
|
- **Поддерживаемые хосты**: localhost, 127.0.0.1, указанный хост
|
||||||
|
- **Использование**: Server Authentication
|
||||||
|
|
||||||
|
### Безопасные хосты
|
||||||
|
|
||||||
|
Следующие хосты считаются безопасными для HTTP:
|
||||||
|
|
||||||
|
- `localhost`
|
||||||
|
- `127.0.0.1`
|
||||||
|
- `::1` (IPv6 localhost)
|
||||||
|
|
||||||
|
### Проверка безопасности
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Проверка хоста
|
||||||
|
if !ssl.IsSecureHost(host) {
|
||||||
|
// Принудительно HTTPS
|
||||||
|
useHTTPS = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка разрешения HTTP
|
||||||
|
if config.AppConfig.Server.AllowHTTP {
|
||||||
|
useHTTPS = false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠️ Отладка
|
||||||
|
|
||||||
|
### Проверка конфигурации
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Показать текущую конфигурацию
|
||||||
|
lcg config --full | jq '.server'
|
||||||
|
|
||||||
|
# Проверить SSL сертификаты
|
||||||
|
ls -la ~/.config/lcg/config/server/ssl/
|
||||||
|
|
||||||
|
# Проверить переменные окружения
|
||||||
|
env | grep LCG_SERVER
|
||||||
|
```
|
||||||
|
|
||||||
|
### Логи безопасности
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Запуск с отладкой
|
||||||
|
LCG_SERVER_HOST=192.168.1.100 lcg serve --debug
|
||||||
|
|
||||||
|
# Проверка SSL
|
||||||
|
openssl x509 -in ~/.config/lcg/config/server/ssl/cert.pem -text -noout
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚠️ Важные замечания
|
||||||
|
|
||||||
|
### Безопасность
|
||||||
|
|
||||||
|
1. **Самоподписанные сертификаты** - браузеры будут показывать предупреждение
|
||||||
|
2. **Продакшен** - используйте настоящие SSL сертификаты от CA
|
||||||
|
3. **Сетевой доступ** - HTTPS защищает трафик, но не аутентификацию
|
||||||
|
|
||||||
|
### Производительность
|
||||||
|
|
||||||
|
1. **HTTPS** - небольшая нагрузка на CPU для шифрования
|
||||||
|
2. **Сертификаты** - генерируются один раз, затем кэшируются
|
||||||
|
3. **Память** - сертификаты загружаются в память при запуске
|
||||||
|
|
||||||
|
## 📚 Связанные файлы
|
||||||
|
|
||||||
|
- `config/config.go` - конфигурация безопасности
|
||||||
|
- `ssl/ssl.go` - генерация и управление сертификатами
|
||||||
|
- `serve/serve.go` - HTTP/HTTPS сервер
|
||||||
|
- `SECURITY_FEATURES.md` - эта документация
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Результат**: Приложение теперь автоматически обеспечивает безопасность соединения в зависимости от конфигурации хоста!
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Что это
|
## Что это
|
||||||
|
|
||||||
Linux Command GPT (`lcg`) преобразует описание на естественном языке в готовую Linux‑команду. Инструмент поддерживает сменные провайдеры LLM (Ollama или Proxy), управление системными промптами, историю запросов, сохранение результатов, HTTP сервер для просмотра результатов и интерактивные действия над сгенерированной командой.
|
Linux Command GPT (`lcg`) преобразует описание на естественном языке в готовую команду для Linux или Windows. Инструмент автоматически определяет операционную систему и использует соответствующие промпты. Поддерживает сменные провайдеры LLM (Ollama или Proxy), управление системными промптами, историю запросов, сохранение результатов, HTTP сервер для просмотра результатов, аутентификацию, CSRF защиту, интерактивные действия над сгенерированной командой и деплой в Kubernetes.
|
||||||
|
|
||||||
## Требования
|
## Требования
|
||||||
|
|
||||||
@@ -21,7 +21,8 @@ sudo apt-get install xsel
|
|||||||
Сборка из исходников:
|
Сборка из исходников:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone --depth 1 https://github.com/asrul10/linux-command-gpt.git ~/.linux-command-gpt
|
|
||||||
|
git clone --depth 1 https://github.com/Direct-Dev-Ru/go-lcg.git ~/.linux-command-gpt
|
||||||
cd ~/.linux-command-gpt
|
cd ~/.linux-command-gpt
|
||||||
go build -o lcg
|
go build -o lcg
|
||||||
|
|
||||||
@@ -59,6 +60,19 @@ lcg --file /path/to/context.txt "хочу вывести список дирек
|
|||||||
Действия: (c)копировать, (s)сохранить, (r)перегенерировать, (e)выполнить, (v|vv|vvv)подробно, (n)ничего:
|
Действия: (c)копировать, (s)сохранить, (r)перегенерировать, (e)выполнить, (v|vv|vvv)подробно, (n)ничего:
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Что нового в 3.0.0
|
||||||
|
|
||||||
|
- **Аутентификация**: Добавлена система аутентификации с JWT токенами и HTTP-only cookies
|
||||||
|
- **CSRF защита**: Полная защита от CSRF атак с токенами и middleware
|
||||||
|
- **Безопасность**: Улучшенная безопасность с проверкой токенов и сессий
|
||||||
|
- **Kubernetes деплой**: Полный набор манифестов для деплоя в Kubernetes с Traefik
|
||||||
|
- **Flux CD**: GitOps конфигурация для автоматического деплоя
|
||||||
|
- **Reverse Proxy**: Поддержка работы за reverse proxy с настройкой cookies
|
||||||
|
- **Веб-интерфейс**: Улучшенный веб-интерфейс с современным дизайном
|
||||||
|
- **Мониторинг**: Prometheus метрики и ServiceMonitor
|
||||||
|
- **Масштабирование**: HPA для автоматического масштабирования
|
||||||
|
- **Тестирование**: Инструменты для тестирования CSRF защиты
|
||||||
|
|
||||||
## Переменные окружения
|
## Переменные окружения
|
||||||
|
|
||||||
Можно настроить поведение без изменения командной строки.
|
Можно настроить поведение без изменения командной строки.
|
||||||
@@ -75,12 +89,21 @@ lcg --file /path/to/context.txt "хочу вывести список дирек
|
|||||||
| `LCG_PROVIDER` | `ollama` | Тип провайдера: `ollama` или `proxy`. |
|
| `LCG_PROVIDER` | `ollama` | Тип провайдера: `ollama` или `proxy`. |
|
||||||
| `LCG_JWT_TOKEN` | пусто | JWT токен для `proxy` провайдера (альтернатива — файл `~/.proxy_jwt_token`). |
|
| `LCG_JWT_TOKEN` | пусто | JWT токен для `proxy` провайдера (альтернатива — файл `~/.proxy_jwt_token`). |
|
||||||
| `LCG_PROMPT_ID` | `1` | ID системного промпта по умолчанию. |
|
| `LCG_PROMPT_ID` | `1` | ID системного промпта по умолчанию. |
|
||||||
|
| `LCG_BROWSER_PATH` | пусто | Путь к браузеру для автооткрытия (`--browser`). |
|
||||||
| `LCG_TIMEOUT` | `300` | Таймаут запроса в секундах. |
|
| `LCG_TIMEOUT` | `300` | Таймаут запроса в секундах. |
|
||||||
| `LCG_RESULT_HISTORY` | `$(LCG_RESULT_FOLDER)/lcg_history.json` | Путь к JSON‑истории запросов. |
|
| `LCG_RESULT_HISTORY` | `$(LCG_RESULT_FOLDER)/lcg_history.json` | Путь к JSON‑истории запросов. |
|
||||||
| `LCG_PROMPT_FOLDER` | `~/.config/lcg/gpt_sys_prompts` | Папка для хранения системных промптов. |
|
| `LCG_PROMPT_FOLDER` | `~/.config/lcg/gpt_sys_prompts` | Папка для хранения системных промптов. |
|
||||||
| `LCG_NO_HISTORY` | пусто | Если `1`/`true` — полностью отключает запись/обновление истории. |
|
| `LCG_NO_HISTORY` | пусто | Если `1`/`true` — полностью отключает запись/обновление истории. |
|
||||||
|
| `LCG_ALLOW_EXECUTION` | пусто | Если `1`/`true` — включает возможность выполнения команд через опцию `(e)` в меню действий. |
|
||||||
| `LCG_SERVER_PORT` | `8080` | Порт для HTTP сервера просмотра результатов. |
|
| `LCG_SERVER_PORT` | `8080` | Порт для HTTP сервера просмотра результатов. |
|
||||||
| `LCG_SERVER_HOST` | `localhost` | Хост для HTTP сервера просмотра результатов. |
|
| `LCG_SERVER_HOST` | `localhost` | Хост для HTTP сервера просмотра результатов. |
|
||||||
|
| `LCG_SERVER_REQUIRE_AUTH` | `false` | Требовать аутентификацию для доступа к веб-интерфейсу. |
|
||||||
|
| `LCG_SERVER_PASSWORD` | `admin#123456` | Пароль для аутентификации. |
|
||||||
|
| `LCG_COOKIE_SECURE` | `false` | Использовать Secure флаг для cookies (для HTTPS). |
|
||||||
|
| `LCG_DOMAIN` | пусто | Домен для cookies (для reverse proxy). |
|
||||||
|
| `LCG_COOKIE_PATH` | `/` | Путь для cookies (для reverse proxy). |
|
||||||
|
| `LCG_COOKIE_TTL_HOURS` | `168` | Время жизни cookies в часах (по умолчанию 7 дней). |
|
||||||
|
| `LCG_CSRF_SECRET` | пусто | Секрет для CSRF токенов (генерируется автоматически). |
|
||||||
|
|
||||||
Примеры настройки:
|
Примеры настройки:
|
||||||
|
|
||||||
@@ -95,6 +118,14 @@ export LCG_PROVIDER=proxy
|
|||||||
export LCG_HOST=http://localhost:8080
|
export LCG_HOST=http://localhost:8080
|
||||||
export LCG_MODEL=GigaChat-2
|
export LCG_MODEL=GigaChat-2
|
||||||
export LCG_JWT_TOKEN=your_jwt_token_here
|
export LCG_JWT_TOKEN=your_jwt_token_here
|
||||||
|
|
||||||
|
# Аутентификация и безопасность
|
||||||
|
export LCG_SERVER_REQUIRE_AUTH=true
|
||||||
|
export LCG_SERVER_PASSWORD=my_secure_password
|
||||||
|
export LCG_COOKIE_SECURE=false
|
||||||
|
export LCG_DOMAIN=.example.com
|
||||||
|
export LCG_COOKIE_PATH=/lcg
|
||||||
|
export LCG_COOKIE_TTL_HOURS=72 # 3 дня
|
||||||
```
|
```
|
||||||
|
|
||||||
## Базовый синтаксис
|
## Базовый синтаксис
|
||||||
@@ -108,7 +139,7 @@ lcg [глобальные опции] <описание команды>
|
|||||||
- `--file, -f string` — прочитать часть запроса из файла и добавить к описанию.
|
- `--file, -f string` — прочитать часть запроса из файла и добавить к описанию.
|
||||||
- `--sys, -s string` — системный промпт (содержимое или ID как строка). Если не задан, используется `--prompt-id` или `LCG_PROMPT`.
|
- `--sys, -s string` — системный промпт (содержимое или ID как строка). Если не задан, используется `--prompt-id` или `LCG_PROMPT`.
|
||||||
- `--prompt-id, --pid int` — ID системного промпта (1–5 для стандартных, либо ваш кастомный ID).
|
- `--prompt-id, --pid int` — ID системного промпта (1–5 для стандартных, либо ваш кастомный ID).
|
||||||
- `--timeout, -t int` — таймаут запроса в секундах (по умолчанию 300).
|
- `--timeout, -t int` — таймаут запроса в секундах (по умолчанию 120; через `LCG_TIMEOUT` — 300).
|
||||||
- `--no-history, --nh` — отключить запись/обновление истории для текущего запуска.
|
- `--no-history, --nh` — отключить запись/обновление истории для текущего запуска.
|
||||||
- `--debug, -d` — показать отладочную информацию (параметры запроса и промпты).
|
- `--debug, -d` — показать отладочную информацию (параметры запроса и промпты).
|
||||||
- `--version, -v` — вывести версию.
|
- `--version, -v` — вывести версию.
|
||||||
@@ -132,10 +163,13 @@ lcg [глобальные опции] <описание команды>
|
|||||||
- `lcg prompts list --full` (`-f`) — полный вывод содержимого без обрезки длинных строк.
|
- `lcg prompts list --full` (`-f`) — полный вывод содержимого без обрезки длинных строк.
|
||||||
- `lcg prompts add` (`-a`) — добавить пользовательский промпт (по шагам в интерактиве).
|
- `lcg prompts add` (`-a`) — добавить пользовательский промпт (по шагам в интерактиве).
|
||||||
- `lcg prompts delete <id>` (`-d`) — удалить пользовательский промпт по ID (>5).
|
- `lcg prompts delete <id>` (`-d`) — удалить пользовательский промпт по ID (>5).
|
||||||
- `lcg test-prompt <prompt-id> <описание>` (`-tp`): показать детали выбранного системного промпта и протестировать его на заданном описании.
|
- `lcg test-prompt <prompt-id> <описание>` (алиас: `tp`): показать детали выбранного системного промпта и протестировать его на заданном описании.
|
||||||
- `lcg serve-result` (`serve`): запустить HTTP сервер для просмотра сохраненных результатов:
|
- `lcg serve`: запустить HTTP сервер для просмотра сохраненных результатов:
|
||||||
- `--port, -p` — порт сервера (по умолчанию из `LCG_SERVER_PORT`)
|
- `--port, -p` — порт сервера (по умолчанию из `LCG_SERVER_PORT`)
|
||||||
- `--host, -H` — хост сервера (по умолчанию из `LCG_SERVER_HOST`)
|
- `--host, -H` — хост сервера (по умолчанию из `LCG_SERVER_HOST`)
|
||||||
|
- `--browser, -b` — открыть браузер автоматически после старта
|
||||||
|
- `--require-auth` — включить аутентификацию (переопределяет `LCG_SERVER_REQUIRE_AUTH`)
|
||||||
|
- `--password` — пароль для аутентификации (переопределяет `LCG_SERVER_PASSWORD`)
|
||||||
|
|
||||||
### Подробные объяснения (v/vv/vvv)
|
### Подробные объяснения (v/vv/vvv)
|
||||||
|
|
||||||
@@ -186,6 +220,39 @@ lcg [глобальные опции] <описание команды>
|
|||||||
- Не вставляйте секреты в запросы. Перед выполнением (`e`) проверяйте команду вручную.
|
- Не вставляйте секреты в запросы. Перед выполнением (`e`) проверяйте команду вручную.
|
||||||
- Для структуры API см. `API_CONTRACT.md` (эндпоинты и форматы запросов/ответов).
|
- Для структуры API см. `API_CONTRACT.md` (эндпоинты и форматы запросов/ответов).
|
||||||
|
|
||||||
|
## Поддержка операционных систем
|
||||||
|
|
||||||
|
### Автоматическое определение ОС
|
||||||
|
|
||||||
|
Приложение автоматически определяет операционную систему и использует соответствующие промпты:
|
||||||
|
|
||||||
|
- **Linux/Unix системы** (включая macOS): используются промпты для Linux команд
|
||||||
|
- **Windows**: используются промпты для Windows команд (PowerShell, CMD, Batch)
|
||||||
|
|
||||||
|
### Промпты для Windows
|
||||||
|
|
||||||
|
На Windows системах доступны следующие встроенные промпты:
|
||||||
|
|
||||||
|
| ID | Name | Описание |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 1 | windows-command | Основной промпт для генерации Windows команд |
|
||||||
|
| 2 | windows-command-with-explanation | Промпт с подробным объяснением команд |
|
||||||
|
| 3 | windows-command-safe | Безопасный анализ команд с предупреждениями |
|
||||||
|
| 4 | windows-command-verbose | Подробный анализ с техническими деталями |
|
||||||
|
| 5 | windows-command-simple | Простое и понятное объяснение |
|
||||||
|
|
||||||
|
### Примеры использования на Windows
|
||||||
|
|
||||||
|
```cmd
|
||||||
|
# PowerShell команды
|
||||||
|
lcg "хочу получить список всех процессов"
|
||||||
|
lcg "показать информацию о дисках"
|
||||||
|
|
||||||
|
# CMD команды
|
||||||
|
lcg "создать папку test и перейти в неё"
|
||||||
|
lcg "найти все файлы .txt в текущей директории"
|
||||||
|
```
|
||||||
|
|
||||||
## Системные промпты
|
## Системные промпты
|
||||||
|
|
||||||
### Управление промптами
|
### Управление промптами
|
||||||
@@ -200,6 +267,9 @@ lcg [глобальные опции] <описание команды>
|
|||||||
|
|
||||||
### Встроенные промпты (ID 1–5)
|
### Встроенные промпты (ID 1–5)
|
||||||
|
|
||||||
|
Промпты автоматически выбираются в зависимости от операционной системы:
|
||||||
|
|
||||||
|
**Linux/Unix системы:**
|
||||||
| ID | Name | Описание |
|
| ID | Name | Описание |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| 1 | linux-command | «Ответь только Linux‑командой, без форматирования и объяснений». |
|
| 1 | linux-command | «Ответь только Linux‑командой, без форматирования и объяснений». |
|
||||||
@@ -208,6 +278,15 @@ lcg [глобальные опции] <описание команды>
|
|||||||
| 4 | linux-command-verbose | Команда с подробными объяснениями флагов и альтернатив. |
|
| 4 | linux-command-verbose | Команда с подробными объяснениями флагов и альтернатив. |
|
||||||
| 5 | linux-command-simple | Простые команды, избегать сложных опций. |
|
| 5 | linux-command-simple | Простые команды, избегать сложных опций. |
|
||||||
|
|
||||||
|
**Windows системы:**
|
||||||
|
| ID | Name | Описание |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 1 | windows-command | «Ответь только Windows‑командой, без форматирования и объяснений». |
|
||||||
|
| 2 | windows-command-with-explanation | Сгенерируй команду и кратко объясни, что она делает (формат: COMMAND: explanation). |
|
||||||
|
| 3 | windows-command-safe | Безопасные команды (без потери данных). Вывод — только команда. |
|
||||||
|
| 4 | windows-command-verbose | Команда с подробными объяснениями флагов и альтернатив. |
|
||||||
|
| 5 | windows-command-simple | Простые команды, избегать сложных опций. |
|
||||||
|
|
||||||
### Промпты подробности (ID 6–8)
|
### Промпты подробности (ID 6–8)
|
||||||
|
|
||||||
| ID | Name | Описание |
|
| ID | Name | Описание |
|
||||||
@@ -218,7 +297,7 @@ lcg [глобальные опции] <описание команды>
|
|||||||
|
|
||||||
### Веб-интерфейс управления
|
### Веб-интерфейс управления
|
||||||
|
|
||||||
Через HTTP сервер (`lcg serve-result`) доступно полное управление промптами:
|
Через HTTP сервер (`lcg serve`) доступно полное управление промптами:
|
||||||
|
|
||||||
- **Просмотр всех промптов** (встроенных и пользовательских)
|
- **Просмотр всех промптов** (встроенных и пользовательских)
|
||||||
- **Редактирование любых промптов** (включая встроенные)
|
- **Редактирование любых промптов** (включая встроенные)
|
||||||
@@ -236,22 +315,25 @@ gpt_request_<MODEL>_YYYY-MM-DD_HH-MM-SS.md
|
|||||||
|
|
||||||
## HTTP сервер для просмотра результатов
|
## HTTP сервер для просмотра результатов
|
||||||
|
|
||||||
Команда `lcg serve-result` запускает веб-сервер для удобного просмотра всех сохраненных результатов:
|
Команда `lcg serve` запускает веб-сервер для удобного просмотра всех сохраненных результатов:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Запуск с настройками по умолчанию
|
# Запуск с настройками по умолчанию
|
||||||
lcg serve-result
|
lcg serve
|
||||||
|
|
||||||
# Запуск на другом порту
|
# Запуск на другом порту
|
||||||
lcg serve-result --port 9090
|
lcg serve --port 9090
|
||||||
|
|
||||||
# Запуск на другом хосте
|
# Запуск на другом хосте
|
||||||
lcg serve-result --host 0.0.0.0 --port 8080
|
lcg serve --host 0.0.0.0 --port 8080
|
||||||
|
|
||||||
|
# Автооткрытие браузера (опционально)
|
||||||
|
lcg serve --browser
|
||||||
|
|
||||||
# Использование переменных окружения
|
# Использование переменных окружения
|
||||||
export LCG_SERVER_PORT=3000
|
export LCG_SERVER_PORT=3000
|
||||||
export LCG_SERVER_HOST=0.0.0.0
|
export LCG_SERVER_HOST=0.0.0.0
|
||||||
lcg serve-result
|
lcg serve
|
||||||
```
|
```
|
||||||
|
|
||||||
### Возможности веб-интерфейса
|
### Возможности веб-интерфейса
|
||||||
@@ -262,6 +344,12 @@ lcg serve-result
|
|||||||
- **Современный дизайн** — адаптивный интерфейс с карточками файлов
|
- **Современный дизайн** — адаптивный интерфейс с карточками файлов
|
||||||
- **Сортировка** — файлы отсортированы по дате изменения (новые сверху)
|
- **Сортировка** — файлы отсортированы по дате изменения (новые сверху)
|
||||||
- **Превью содержимого** — первые 200 символов каждого файла
|
- **Превью содержимого** — первые 200 символов каждого файла
|
||||||
|
- **Аутентификация** — защищенный доступ с JWT токенами
|
||||||
|
- **CSRF защита** — защита от межсайтовых атак
|
||||||
|
- **История запросов** (`/history`) — просмотр истории всех запросов
|
||||||
|
- **Управление промптами** (`/prompts`) — редактирование системных промптов
|
||||||
|
- **Выполнение команд** (`/run`) — интерактивное выполнение команд
|
||||||
|
- **Безопасность** — HTTP-only cookies, проверка токенов
|
||||||
|
|
||||||
Структура файла (команда):
|
Структура файла (команда):
|
||||||
|
|
||||||
@@ -276,6 +364,11 @@ lcg serve-result
|
|||||||
- `## Command` — первая сгенерированная команда.
|
- `## Command` — первая сгенерированная команда.
|
||||||
- `## Explanation and Alternatives (model: <MODEL>)` — подробное объяснение и альтернативы.
|
- `## Explanation and Alternatives (model: <MODEL>)` — подробное объяснение и альтернативы.
|
||||||
|
|
||||||
|
### Браузер
|
||||||
|
|
||||||
|
- По умолчанию UI не открывается автоматически. Для автооткрытия используйте `--browser`.
|
||||||
|
- Путь к конкретному браузеру можно задать переменной `LCG_BROWSER_PATH`.
|
||||||
|
|
||||||
## Выполнение сгенерированной команды
|
## Выполнение сгенерированной команды
|
||||||
|
|
||||||
Действие `e` запустит команду через `bash -c`. Перед запуском потребуется подтверждение `y/yes`. Всегда проверяйте команду вручную, особенно при операциях с файлами и сетью.
|
Действие `e` запустит команду через `bash -c`. Перед запуском потребуется подтверждение `y/yes`. Всегда проверяйте команду вручную, особенно при операциях с файлами и сетью.
|
||||||
@@ -333,18 +426,26 @@ lcg models
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Запуск сервера
|
# Запуск сервера
|
||||||
lcg serve-result
|
lcg serve
|
||||||
|
|
||||||
# Запуск на другом порту
|
# Запуск на другом порту
|
||||||
lcg serve-result --port 9090
|
lcg serve --port 9090
|
||||||
|
|
||||||
# Запуск на всех интерфейсах
|
# Запуск на всех интерфейсах
|
||||||
lcg serve-result --host 0.0.0.0 --port 8080
|
lcg serve --host 0.0.0.0 --port 8080
|
||||||
|
|
||||||
|
# Запуск с аутентификацией
|
||||||
|
lcg serve --require-auth --password my_secure_password
|
||||||
|
|
||||||
|
# Запуск с переменными окружения
|
||||||
|
export LCG_SERVER_REQUIRE_AUTH=true
|
||||||
|
export LCG_SERVER_PASSWORD=admin#123456
|
||||||
|
lcg serve
|
||||||
```
|
```
|
||||||
|
|
||||||
## История
|
## История
|
||||||
|
|
||||||
`lcg history` выводит историю текущего процесса (не сохраняется между запусками, максимум 100 записей):
|
`lcg history` выводит историю из JSON‑файла (`LCG_RESULT_HISTORY`), сохраняется между запусками:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
lcg history list
|
lcg history list
|
||||||
@@ -359,6 +460,10 @@ lcg history list
|
|||||||
- Для `ollama`/`proxy` API‑ключ не нужен; команды `update-key`/`delete-key` просто уведомят об этом.
|
- Для `ollama`/`proxy` API‑ключ не нужен; команды `update-key`/`delete-key` просто уведомят об этом.
|
||||||
- HTTP сервер не запускается: проверьте, что порт свободен, используйте `--port` для смены порта.
|
- HTTP сервер не запускается: проверьте, что порт свободен, используйте `--port` для смены порта.
|
||||||
- Веб-интерфейс не отображает файлы: убедитесь, что в `LCG_RESULT_FOLDER` есть `.md` файлы.
|
- Веб-интерфейс не отображает файлы: убедитесь, что в `LCG_RESULT_FOLDER` есть `.md` файлы.
|
||||||
|
- **Аутентификация не работает**: проверьте `LCG_SERVER_REQUIRE_AUTH=true` и правильность пароля.
|
||||||
|
- **CSRF ошибки**: убедитесь, что токены передаются в заголовках `X-CSRF-Token`.
|
||||||
|
- **Cookies не сохраняются**: проверьте настройки `LCG_DOMAIN` и `LCG_COOKIE_PATH` для reverse proxy.
|
||||||
|
- **Kubernetes деплой не работает**: проверьте права доступа к кластеру и наличие всех манифестов.
|
||||||
|
|
||||||
## JSON‑история запросов
|
## JSON‑история запросов
|
||||||
|
|
||||||
@@ -387,3 +492,151 @@ lcg history list
|
|||||||
## Лицензия и исходники
|
## Лицензия и исходники
|
||||||
|
|
||||||
См. README и репозиторий проекта. Предложения и баг‑репорты приветствуются в Issues.
|
См. README и репозиторий проекта. Предложения и баг‑репорты приветствуются в Issues.
|
||||||
|
|
||||||
|
## Доступ к локальному API
|
||||||
|
|
||||||
|
### Основные эндпоинты
|
||||||
|
|
||||||
|
- `POST /api/execute` — выполнение запросов к LLM
|
||||||
|
- `POST /api/save-result` — сохранение результатов
|
||||||
|
- `POST /api/add-to-history` — добавление в историю
|
||||||
|
- `GET /api/login` — страница аутентификации
|
||||||
|
- `POST /api/login` — аутентификация
|
||||||
|
- `POST /api/logout` — выход из системы
|
||||||
|
- `GET /metrics` — Prometheus метрики
|
||||||
|
|
||||||
|
### Примеры использования
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Запустить сервер
|
||||||
|
lcg serve
|
||||||
|
|
||||||
|
# Выполнить запрос (без аутентификации)
|
||||||
|
curl -X POST http://localhost:8080/api/execute \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-A curl \
|
||||||
|
-d '{"prompt": "create directory test", "verbose": "vv"}'
|
||||||
|
|
||||||
|
# Аутентификация
|
||||||
|
curl -X POST http://localhost:8080/api/login \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"username": "admin", "password": "admin#123456"}'
|
||||||
|
|
||||||
|
# Выполнение с CSRF токеном
|
||||||
|
curl -X POST http://localhost:8080/api/execute \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-CSRF-Token: your_csrf_token" \
|
||||||
|
-H "Cookie: auth_token=your_jwt_token" \
|
||||||
|
-d '{"prompt": "create directory test"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Подробности и примеры: `API_GUIDE.md`.
|
||||||
|
|
||||||
|
## Kubernetes деплой
|
||||||
|
|
||||||
|
### Быстрый деплой
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Переход в папку деплоя
|
||||||
|
cd deploy
|
||||||
|
|
||||||
|
# Полный деплой (сборка + деплой + проверка)
|
||||||
|
./full-deploy.sh
|
||||||
|
|
||||||
|
# Или поэтапно
|
||||||
|
./build.sh lcg latest
|
||||||
|
./deploy.sh
|
||||||
|
./health-check.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Использование Make
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Справка
|
||||||
|
make help
|
||||||
|
|
||||||
|
# Сборка и деплой
|
||||||
|
make build
|
||||||
|
make deploy
|
||||||
|
|
||||||
|
# Мониторинг
|
||||||
|
make status
|
||||||
|
make logs
|
||||||
|
make monitor
|
||||||
|
|
||||||
|
# Удаление
|
||||||
|
make undeploy
|
||||||
|
```
|
||||||
|
|
||||||
|
### Flux CD (GitOps)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Настройка Flux CD
|
||||||
|
cd deploy/flux
|
||||||
|
./setup-flux.sh
|
||||||
|
|
||||||
|
# Создание Kustomization
|
||||||
|
./create_kustomization.sh
|
||||||
|
|
||||||
|
# Мониторинг
|
||||||
|
kubectl get kustomization lcg -n flux-system
|
||||||
|
```
|
||||||
|
|
||||||
|
### Конфигурация для reverse proxy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Настройка для работы за reverse proxy
|
||||||
|
export LCG_SERVER_REQUIRE_AUTH=true
|
||||||
|
export LCG_SERVER_ALLOW_HTTP=true
|
||||||
|
export LCG_DOMAIN=.example.com
|
||||||
|
export LCG_COOKIE_PATH=/lcg
|
||||||
|
export LCG_COOKIE_SECURE=false
|
||||||
|
|
||||||
|
# Запуск
|
||||||
|
./lcg serve -H 0.0.0.0 -p 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
### Мониторинг и безопасность
|
||||||
|
|
||||||
|
- **Prometheus метрики**: `/metrics` endpoint
|
||||||
|
- **Health checks**: автоматические проверки готовности
|
||||||
|
- **HPA**: автоматическое масштабирование (2-10 replicas)
|
||||||
|
- **CSRF защита**: токены для всех POST запросов
|
||||||
|
- **Аутентификация**: JWT токены в HTTP-only cookies
|
||||||
|
- **Security context**: non-root пользователь, минимальные права
|
||||||
|
|
||||||
|
Подробности: `deploy/README.md` и `deploy/flux/README.md`.
|
||||||
|
|
||||||
|
## Тестирование CSRF защиты
|
||||||
|
|
||||||
|
### Автоматическое тестирование
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Запуск тестов CSRF защиты
|
||||||
|
./test_csrf.sh
|
||||||
|
|
||||||
|
# Проверка результатов
|
||||||
|
echo "Проверьте вывод на наличие ошибок 403 Forbidden"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ручное тестирование
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Откройте csrf_test.html в браузере
|
||||||
|
open csrf_test.html
|
||||||
|
|
||||||
|
# Или используйте curl для тестирования
|
||||||
|
curl -X POST http://localhost:8080/api/execute \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"prompt": "test"}' \
|
||||||
|
-v
|
||||||
|
```
|
||||||
|
|
||||||
|
### Демонстрация уязвимости
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Откройте csrf_demo.html для демонстрации атаки
|
||||||
|
open csrf_demo.html
|
||||||
|
```
|
||||||
|
|
||||||
|
Подробности: `CSRF_TESTING_GUIDE.md`.
|
||||||
205
docs/VALIDATION_CONFIG.md
Normal file
205
docs/VALIDATION_CONFIG.md
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
# 🔧 Конфигурация валидации длины полей
|
||||||
|
|
||||||
|
## 📋 Переменные окружения
|
||||||
|
|
||||||
|
Все настройки валидации можно настроить через переменные окружения:
|
||||||
|
|
||||||
|
### Основные лимиты
|
||||||
|
|
||||||
|
| Переменная | Описание | По умолчанию |
|
||||||
|
|------------|----------|--------------|
|
||||||
|
| `LCG_MAX_SYSTEM_PROMPT_LENGTH` | Максимальная длина системного промпта | 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
docs/VERBOSE_PROMPT_EDITING.md
Normal file
63
docs/VERBOSE_PROMPT_EDITING.md
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# Редактирование промптов подробности
|
||||||
|
|
||||||
|
## 🎯 Реализованная функциональность
|
||||||
|
|
||||||
|
### ✅ **Что добавлено:**
|
||||||
|
|
||||||
|
1. **Функция редактирования в JavaScript:**
|
||||||
|
- `editVerbosePrompt(mode, content)` - открывает форму редактирования для промптов подробности
|
||||||
|
- Автоматически заполняет поля формы данными промпта
|
||||||
|
- Показывает режим в заголовке формы
|
||||||
|
|
||||||
|
2. **Обработчик на сервере:**
|
||||||
|
- `handleEditVerbosePrompt()` - новый обработчик для маршрута `/prompts/edit-verbose/`
|
||||||
|
- Поддерживает режимы: `v`, `vv`, `vvv`
|
||||||
|
- Валидация всех полей с использованием `validation` пакета
|
||||||
|
- Обновление промптов через `PromptManager`
|
||||||
|
|
||||||
|
3. **Маршрутизация:**
|
||||||
|
- Добавлен маршрут `/prompts/edit-verbose/` в `serve.go`
|
||||||
|
- Поддержка HTTP методов PUT
|
||||||
|
- Интеграция с существующей системой маршрутов
|
||||||
|
|
||||||
|
### 🔧 **Как работает:**
|
||||||
|
|
||||||
|
1. **Пользователь нажимает кнопку "✏️"** на промпте подробности
|
||||||
|
2. **JavaScript вызывает** `editVerbosePrompt(mode, content)`
|
||||||
|
3. **Форма открывается** с заполненными полями
|
||||||
|
4. **При сохранении** отправляется PUT запрос на `/prompts/edit-verbose/{mode}`
|
||||||
|
5. **Сервер обрабатывает** запрос через `handleEditVerbosePrompt()`
|
||||||
|
6. **Промпт обновляется** в файловой системе
|
||||||
|
7. **Страница перезагружается** с обновленными данными
|
||||||
|
|
||||||
|
### 📋 **Поддерживаемые режимы:**
|
||||||
|
|
||||||
|
- **`v`** → ID 6 (базовый verbose)
|
||||||
|
- **`vv`** → ID 7 (средний verbose)
|
||||||
|
- **`vvv`** → ID 8 (максимальный verbose)
|
||||||
|
|
||||||
|
### 🛡️ **Валидация:**
|
||||||
|
|
||||||
|
- **Содержимое:** максимум символов из `LCG_MAX_SYSTEM_PROMPT_LENGTH`
|
||||||
|
- **Название:** максимум символов из `LCG_MAX_PROMPT_NAME_LENGTH`
|
||||||
|
- **Описание:** максимум символов из `LCG_MAX_PROMPT_DESC_LENGTH`
|
||||||
|
|
||||||
|
### 🎨 **UI/UX:**
|
||||||
|
|
||||||
|
- **Единая форма** для редактирования всех типов промптов
|
||||||
|
- **Автоматическое определение** типа промпта (системный/verbose)
|
||||||
|
- **Правильная маршрутизация** запросов
|
||||||
|
- **Валидация на клиенте** и сервере
|
||||||
|
- **Отзывчивый дизайн** для мобильных устройств
|
||||||
|
|
||||||
|
## 🚀 **Использование:**
|
||||||
|
|
||||||
|
1. Откройте страницу `/prompts`
|
||||||
|
2. Перейдите на вкладку "📝 Промпты подробности"
|
||||||
|
3. Нажмите кнопку "✏️" на нужном промпте
|
||||||
|
4. Отредактируйте содержимое
|
||||||
|
5. Нажмите "Сохранить"
|
||||||
|
|
||||||
|
## ✅ **Статус:**
|
||||||
|
|
||||||
|
**ГОТОВО** - Редактирование промптов подробности полностью реализовано и протестировано.
|
||||||
6
go.mod
6
go.mod
@@ -1,11 +1,15 @@
|
|||||||
module github.com/direct-dev-ru/linux-command-gpt
|
module github.com/direct-dev-ru/linux-command-gpt
|
||||||
|
|
||||||
go 1.18
|
go 1.21
|
||||||
|
|
||||||
|
toolchain go1.23.4
|
||||||
|
|
||||||
require github.com/atotto/clipboard v0.1.4
|
require github.com/atotto/clipboard v0.1.4
|
||||||
|
|
||||||
require gopkg.in/yaml.v3 v3.0.1
|
require gopkg.in/yaml.v3 v3.0.1
|
||||||
|
|
||||||
|
require github.com/golang-jwt/jwt/v5 v5.3.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 //indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.5 //indirect
|
||||||
github.com/russross/blackfriday/v2 v2.1.0
|
github.com/russross/blackfriday/v2 v2.1.0
|
||||||
|
|||||||
3
go.sum
3
go.sum
@@ -2,12 +2,15 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z
|
|||||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
|
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
|
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
|
||||||
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
|
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
|
||||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
|
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
|
||||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package gpt
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
_ "embed"
|
_ "embed"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
@@ -9,6 +10,9 @@ import (
|
|||||||
//go:embed builtin_prompts.yaml
|
//go:embed builtin_prompts.yaml
|
||||||
var builtinPromptsYAML string
|
var builtinPromptsYAML string
|
||||||
|
|
||||||
|
//go:embed builtin_prompts_windows.yaml
|
||||||
|
var builtinPromptsWindowsYAML string
|
||||||
|
|
||||||
var builtinPrompts string
|
var builtinPrompts string
|
||||||
|
|
||||||
// BuiltinPromptsData структура для YAML файла
|
// BuiltinPromptsData структура для YAML файла
|
||||||
@@ -117,7 +121,12 @@ func GetBuiltinPromptByIDAndLanguage(id int, lang string) *SystemPrompt {
|
|||||||
func InitBuiltinPrompts(embeddedBuiltinPromptsYAML string) {
|
func InitBuiltinPrompts(embeddedBuiltinPromptsYAML string) {
|
||||||
// Используем встроенный YAML, если переданный параметр пустой
|
// Используем встроенный YAML, если переданный параметр пустой
|
||||||
if embeddedBuiltinPromptsYAML == "" {
|
if embeddedBuiltinPromptsYAML == "" {
|
||||||
|
// Выбираем промпты в зависимости от операционной системы
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
builtinPrompts = builtinPromptsWindowsYAML
|
||||||
|
} else {
|
||||||
builtinPrompts = builtinPromptsYAML
|
builtinPrompts = builtinPromptsYAML
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
builtinPrompts = embeddedBuiltinPromptsYAML
|
builtinPrompts = embeddedBuiltinPromptsYAML
|
||||||
}
|
}
|
||||||
|
|||||||
262
gpt/builtin_prompts_windows.yaml
Normal file
262
gpt/builtin_prompts_windows.yaml
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
prompts:
|
||||||
|
- id: 1
|
||||||
|
name: "windows-command"
|
||||||
|
description:
|
||||||
|
en: "Main prompt for generating Windows commands"
|
||||||
|
ru: "Основной промпт для генерации Windows команд"
|
||||||
|
content:
|
||||||
|
en: |
|
||||||
|
You are a Windows command line expert.
|
||||||
|
Analyze the user's task, given in natural language, and suggest
|
||||||
|
a Windows command (PowerShell, CMD, or batch) that will help accomplish this task, and provide a detailed explanation of what it does,
|
||||||
|
its parameters and possible use cases.
|
||||||
|
Focus on practical examples and best practices.
|
||||||
|
In the response, you should only provide the commands or sequence of commands ready to copy and execute
|
||||||
|
in the command line without any explanation formatting or code blocks, without ```powershell``` or ```cmd```, ` or ``` symbols.
|
||||||
|
|
||||||
|
ru: |
|
||||||
|
Вы эксперт по Windows командам и командной строке.
|
||||||
|
Проанализируйте задачу пользователя на естественном языке и предложите Windows команду или набор команд (PowerShell, CMD или batch), которые помогут выполнить эту задачу, и предоставьте подробное объяснение того, что она делает, её параметры и возможные случаи использования.
|
||||||
|
Сосредоточьтесь на практических примерах и лучших практиках.
|
||||||
|
В ответе должна присутствовать только команда или последовательность команд,
|
||||||
|
готовая к копированию и выполнению в командной строке
|
||||||
|
без объяснений, выделений и форматирования наподобие ```powershell``` или ```cmd```, без символов ` или ```.
|
||||||
|
|
||||||
|
- id: 2
|
||||||
|
name: "windows-command-with-explanation"
|
||||||
|
description:
|
||||||
|
en: "Prompt with detailed command explanation"
|
||||||
|
ru: "Промпт с подробным объяснением команд"
|
||||||
|
content:
|
||||||
|
en: |
|
||||||
|
You are a Windows system administrator with extensive experience.
|
||||||
|
Generate Windows commands based on user task descriptions and provide comprehensive explanations.
|
||||||
|
|
||||||
|
Provide a detailed analysis including:
|
||||||
|
1. **Generated Command**: The Windows command that accomplishes the task
|
||||||
|
2. **Command Breakdown**: Explain each part of the command
|
||||||
|
3. **Parameters**: Explain each flag and option used
|
||||||
|
4. **Examples**: Show practical usage scenarios
|
||||||
|
5. **Security**: Highlight any security considerations
|
||||||
|
6. **Alternatives**: Suggest similar commands if applicable
|
||||||
|
7. **Best Practices**: Recommend optimal usage
|
||||||
|
|
||||||
|
Use clear formatting with headers and bullet points for readability.
|
||||||
|
ru: |
|
||||||
|
Вы системный администратор Windows с обширным опытом.
|
||||||
|
Генерируйте Windows команды на основе описаний задач пользователей и предоставляйте исчерпывающие объяснения.
|
||||||
|
|
||||||
|
Предоставьте подробный анализ, включая:
|
||||||
|
1. **Сгенерированная команда**: Windows команда, которая выполняет задачу
|
||||||
|
2. **Разбор команды**: Объясните каждую часть команды
|
||||||
|
3. **Параметры**: Объясните каждый используемый флаг и опцию
|
||||||
|
4. **Примеры**: Покажите практические сценарии использования
|
||||||
|
5. **Безопасность**: Выделите любые соображения безопасности
|
||||||
|
6. **Альтернативы**: Предложите похожие команды, если применимо
|
||||||
|
7. **Лучшие практики**: Рекомендуйте оптимальное использование
|
||||||
|
|
||||||
|
Используйте четкое форматирование с заголовками и маркерами для читаемости.
|
||||||
|
|
||||||
|
- id: 3
|
||||||
|
name: "windows-command-safe"
|
||||||
|
description:
|
||||||
|
en: "Safe command analysis with warnings"
|
||||||
|
ru: "Безопасный анализ команд с предупреждениями"
|
||||||
|
content:
|
||||||
|
en: |
|
||||||
|
You are a Windows security expert. Generate safe Windows commands based on user task descriptions with a focus on safety and security implications.
|
||||||
|
|
||||||
|
Provide a security-focused analysis:
|
||||||
|
1. **Generated Safe Command**: The secure Windows command for the task
|
||||||
|
2. **Safety Assessment**: Why this command is safe to run
|
||||||
|
3. **Potential Risks**: What could go wrong and how to mitigate
|
||||||
|
4. **Data Impact**: What files or data might be affected
|
||||||
|
5. **Permissions**: What permissions are required
|
||||||
|
6. **Recovery**: How to undo changes if needed
|
||||||
|
7. **Best Practices**: Safe alternatives or precautions
|
||||||
|
8. **Warnings**: Critical safety considerations
|
||||||
|
|
||||||
|
Always prioritize user safety and data protection.
|
||||||
|
ru: |
|
||||||
|
Вы эксперт по безопасности Windows. Генерируйте безопасные Windows команды на основе описаний задач пользователей с акцентом на безопасность и последствия для безопасности.
|
||||||
|
|
||||||
|
Предоставьте анализ, ориентированный на безопасность:
|
||||||
|
1. **Сгенерированная безопасная команда**: Безопасная Windows команда для задачи
|
||||||
|
2. **Оценка безопасности**: Почему эта команда безопасна для выполнения
|
||||||
|
3. **Потенциальные риски**: Что может пойти не так и как это смягчить
|
||||||
|
4. **Воздействие на данные**: Какие файлы или данные могут быть затронуты
|
||||||
|
5. **Разрешения**: Какие разрешения требуются
|
||||||
|
6. **Восстановление**: Как отменить изменения при необходимости
|
||||||
|
7. **Лучшие практики**: Безопасные альтернативы или меры предосторожности
|
||||||
|
8. **Предупреждения**: Критические соображения безопасности
|
||||||
|
|
||||||
|
Всегда приоритизируйте безопасность пользователя и защиту данных.
|
||||||
|
|
||||||
|
- id: 4
|
||||||
|
name: "windows-command-verbose"
|
||||||
|
description:
|
||||||
|
en: "Detailed analysis with technical details"
|
||||||
|
ru: "Подробный анализ с техническими деталями"
|
||||||
|
content:
|
||||||
|
en: |
|
||||||
|
You are a Windows kernel and system expert. Generate Windows commands based on user task descriptions and provide an in-depth technical analysis.
|
||||||
|
|
||||||
|
Deliver a comprehensive technical breakdown:
|
||||||
|
1. **Generated Command**: The Windows command that accomplishes the task
|
||||||
|
2. **System Level**: How the command interacts with the Windows kernel
|
||||||
|
3. **Process Flow**: Step-by-step execution details
|
||||||
|
4. **Resource Usage**: CPU, memory, I/O implications
|
||||||
|
5. **Registry**: Impact on Windows registry
|
||||||
|
6. **Services**: Windows services interactions
|
||||||
|
7. **Performance**: Optimization considerations
|
||||||
|
8. **Debugging**: Troubleshooting approaches
|
||||||
|
9. **Advanced Usage**: Expert-level techniques
|
||||||
|
|
||||||
|
Include technical details, system calls, and low-level operations.
|
||||||
|
ru: |
|
||||||
|
Вы эксперт по ядру Windows и системам. Генерируйте Windows команды на основе описаний задач пользователей и предоставляйте глубокий технический анализ.
|
||||||
|
|
||||||
|
Предоставьте исчерпывающий технический разбор:
|
||||||
|
1. **Сгенерированная команда**: Windows команда, которая выполняет задачу
|
||||||
|
2. **Системный уровень**: Как команда взаимодействует с ядром Windows
|
||||||
|
3. **Поток выполнения**: Детали пошагового выполнения
|
||||||
|
4. **Использование ресурсов**: Последствия для CPU, памяти, I/O
|
||||||
|
5. **Реестр**: Воздействие на реестр Windows
|
||||||
|
6. **Службы**: Взаимодействие со службами Windows
|
||||||
|
7. **Производительность**: Соображения по оптимизации
|
||||||
|
8. **Отладка**: Подходы к устранению неполадок
|
||||||
|
9. **Продвинутое использование**: Техники экспертного уровня
|
||||||
|
|
||||||
|
Включите технические детали, системные вызовы и низкоуровневые операции.
|
||||||
|
|
||||||
|
- id: 5
|
||||||
|
name: "windows-command-simple"
|
||||||
|
description:
|
||||||
|
en: "Simple and clear explanation"
|
||||||
|
ru: "Простое и понятное объяснение"
|
||||||
|
content:
|
||||||
|
en: |
|
||||||
|
You are a friendly Windows mentor. Explain the given command in simple, easy-to-understand terms.
|
||||||
|
|
||||||
|
Command: {{.command}}
|
||||||
|
|
||||||
|
Provide a beginner-friendly explanation:
|
||||||
|
1. **What it does**: Simple, clear description
|
||||||
|
2. **Why use it**: Common reasons to use this command
|
||||||
|
3. **Basic example**: Simple usage example
|
||||||
|
4. **What to expect**: Expected output or behavior
|
||||||
|
5. **Tips**: Helpful hints for beginners
|
||||||
|
|
||||||
|
Use plain language, avoid jargon, and focus on practical understanding.
|
||||||
|
ru: |
|
||||||
|
Вы дружелюбный наставник по Windows. Объясните данную команду простыми, понятными терминами.
|
||||||
|
|
||||||
|
Команда: {{.command}}
|
||||||
|
|
||||||
|
Предоставьте объяснение, подходящее для начинающих:
|
||||||
|
1. **Что она делает**: Простое, четкое описание
|
||||||
|
2. **Зачем использовать**: Общие причины использования этой команды
|
||||||
|
3. **Базовый пример**: Простой пример использования
|
||||||
|
4. **Что ожидать**: Ожидаемый вывод или поведение
|
||||||
|
5. **Советы**: Полезные подсказки для начинающих
|
||||||
|
|
||||||
|
Используйте простой язык, избегайте жаргона и сосредоточьтесь на практическом понимании.
|
||||||
|
|
||||||
|
- id: 6
|
||||||
|
name: "verbose-v"
|
||||||
|
description:
|
||||||
|
en: "Prompt for v mode (basic explanation)"
|
||||||
|
ru: "Промпт для режима v (базовое объяснение)"
|
||||||
|
content:
|
||||||
|
en: |
|
||||||
|
You are a Windows command expert. You can provide a clear and concise explanation of the given Windows command.
|
||||||
|
Your explanation should include:
|
||||||
|
1. What this command does for the task
|
||||||
|
2. Main parameters and their purpose
|
||||||
|
3. Common use cases
|
||||||
|
4. Any important warnings or considerations
|
||||||
|
ru: |
|
||||||
|
Вы эксперт по Windows командам. Вы можете предоставьте четкое и краткое объяснение заданной Windows команды.
|
||||||
|
Ваши краткие объяснения должны включать:
|
||||||
|
1. Что делает эта команда
|
||||||
|
2. Основные параметры и их назначение
|
||||||
|
3. Общие случаи использования
|
||||||
|
4. Любые важные предупреждения или соображения
|
||||||
|
|
||||||
|
- id: 7
|
||||||
|
name: "verbose-vv"
|
||||||
|
description:
|
||||||
|
en: "Prompt for vv mode (detailed explanation)"
|
||||||
|
ru: "Промпт для режима vv (подробное объяснение)"
|
||||||
|
content:
|
||||||
|
en: |
|
||||||
|
You are a Windows system expert. Provide a detailed technical explanation of the given command.
|
||||||
|
|
||||||
|
Provide a comprehensive analysis:
|
||||||
|
1. **Command Purpose**: What it accomplishes
|
||||||
|
2. **Syntax Breakdown**: Detailed parameter analysis
|
||||||
|
3. **Technical Details**: How it works internally
|
||||||
|
4. **Use Cases**: Practical scenarios and examples
|
||||||
|
5. **Performance Impact**: Resource usage and optimization
|
||||||
|
6. **Security Considerations**: Potential risks and mitigations
|
||||||
|
7. **Advanced Usage**: Expert techniques and tips
|
||||||
|
8. **Troubleshooting**: Common issues and solutions
|
||||||
|
|
||||||
|
Include technical depth while maintaining clarity.
|
||||||
|
ru: |
|
||||||
|
Вы эксперт по Windows системам. Предоставьте подробное техническое объяснение заданной команды.
|
||||||
|
|
||||||
|
Предоставьте исчерпывающий анализ:
|
||||||
|
1. **Цель команды**: Что она достигает
|
||||||
|
2. **Разбор синтаксиса**: Подробный анализ параметров
|
||||||
|
3. **Технические детали**: Как она работает внутренне
|
||||||
|
4. **Случаи использования**: Практические сценарии и примеры
|
||||||
|
5. **Влияние на производительность**: Использование ресурсов и оптимизация
|
||||||
|
6. **Соображения безопасности**: Потенциальные риски и меры по их снижению
|
||||||
|
7. **Продвинутое использование**: Экспертные техники и советы
|
||||||
|
8. **Устранение неполадок**: Общие проблемы и решения
|
||||||
|
|
||||||
|
Включите техническую глубину, сохраняя ясность.
|
||||||
|
|
||||||
|
- id: 8
|
||||||
|
name: "verbose-vvv"
|
||||||
|
description:
|
||||||
|
en: "Prompt for vvv mode (maximum detailed explanation)"
|
||||||
|
ru: "Промпт для режима vvv (максимально подробное объяснение)"
|
||||||
|
content:
|
||||||
|
en: |
|
||||||
|
You are a Windows kernel and system architecture expert. Provide an exhaustive technical analysis of the given command.
|
||||||
|
|
||||||
|
Deliver a comprehensive technical deep-dive:
|
||||||
|
1. **System Architecture**: How it fits into the Windows ecosystem
|
||||||
|
2. **Kernel Interaction**: System calls and kernel operations
|
||||||
|
3. **Process Management**: Process creation, scheduling, and lifecycle
|
||||||
|
4. **Memory Management**: Memory allocation and management
|
||||||
|
5. **Registry Operations**: Registry I/O operations and impact
|
||||||
|
6. **Network Stack**: Network operations and protocols
|
||||||
|
7. **Security Model**: Permissions, capabilities, and security implications
|
||||||
|
8. **Performance Analysis**: CPU, memory, I/O, and network impact
|
||||||
|
9. **Debugging and Profiling**: Advanced troubleshooting techniques
|
||||||
|
10. **Source Code Analysis**: Key implementation details
|
||||||
|
11. **Alternative Implementations**: Different approaches and trade-offs
|
||||||
|
12. **Historical Context**: Evolution and development history
|
||||||
|
|
||||||
|
Provide maximum technical depth with system-level insights, code examples, and architectural understanding.
|
||||||
|
ru: |
|
||||||
|
Вы эксперт по ядру Windows и системной архитектуре. Предоставьте исчерпывающий технический анализ заданной команды.
|
||||||
|
|
||||||
|
Предоставьте исчерпывающий технический глубокий анализ:
|
||||||
|
1. **Системная архитектура**: Как она вписывается в экосистему Windows
|
||||||
|
2. **Взаимодействие с ядром**: Системные вызовы и операции ядра
|
||||||
|
3. **Управление процессами**: Создание, планирование и жизненный цикл процессов
|
||||||
|
4. **Управление памятью**: Выделение и управление памятью
|
||||||
|
5. **Операции реестра**: I/O операции реестра и воздействие
|
||||||
|
6. **Сетевой стек**: Сетевые операции и протоколы
|
||||||
|
7. **Модель безопасности**: Разрешения, возможности и последствия безопасности
|
||||||
|
8. **Анализ производительности**: Воздействие на CPU, память, I/O и сеть
|
||||||
|
9. **Отладка и профилирование**: Продвинутые техники устранения неполадок
|
||||||
|
10. **Анализ исходного кода**: Ключевые детали реализации
|
||||||
|
11. **Альтернативные реализации**: Разные подходы и компромиссы
|
||||||
|
12. **Исторический контекст**: Эволюция и история разработки
|
||||||
|
|
||||||
|
Предоставьте максимальную техническую глубину с системными инсайтами, примерами кода и архитектурным пониманием.
|
||||||
@@ -53,13 +53,12 @@ func NewPromptManager(homeDir string) *PromptManager {
|
|||||||
|
|
||||||
// createInitialPromptsFile создает начальный файл с системными промптами и промптами подробности
|
// createInitialPromptsFile создает начальный файл с системными промптами и промптами подробности
|
||||||
func (pm *PromptManager) createInitialPromptsFile() {
|
func (pm *PromptManager) createInitialPromptsFile() {
|
||||||
// Загружаем все встроенные промпты из YAML (английские по умолчанию)
|
// Устанавливаем язык по умолчанию как русский
|
||||||
pm.Prompts = GetBuiltinPrompts()
|
pm.Language = "ru"
|
||||||
|
|
||||||
// Фикс: при первичном сохранении явно выставляем язык файла
|
// Загружаем все встроенные промпты из YAML на русском языке
|
||||||
if pm.Language == "" {
|
// Функция GetBuiltinPromptsByLanguage уже учитывает операционную систему
|
||||||
pm.Language = "en"
|
pm.Prompts = GetBuiltinPromptsByLanguage("ru")
|
||||||
}
|
|
||||||
|
|
||||||
// Сохраняем все промпты в файл
|
// Сохраняем все промпты в файл
|
||||||
pm.saveAllPrompts()
|
pm.saveAllPrompts()
|
||||||
@@ -67,40 +66,8 @@ func (pm *PromptManager) createInitialPromptsFile() {
|
|||||||
|
|
||||||
// loadDefaultPrompts загружает предустановленные промпты
|
// loadDefaultPrompts загружает предустановленные промпты
|
||||||
func (pm *PromptManager) LoadDefaultPrompts() {
|
func (pm *PromptManager) LoadDefaultPrompts() {
|
||||||
defaultPrompts := []SystemPrompt{
|
// Используем встроенные промпты, которые автоматически выбираются по ОС
|
||||||
{
|
pm.Prompts = GetBuiltinPromptsByLanguage("en")
|
||||||
ID: 1,
|
|
||||||
Name: "linux-command",
|
|
||||||
Description: "Generate Linux commands (default)",
|
|
||||||
Content: "Reply with linux command and nothing else. Output with plain response - no need formatting. No need explanation. No need code blocks. No need ` symbols.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: 2,
|
|
||||||
Name: "linux-command-with-explanation",
|
|
||||||
Description: "Generate Linux commands with explanation",
|
|
||||||
Content: "Generate a Linux command and provide a brief explanation of what it does. Format: COMMAND: explanation",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: 3,
|
|
||||||
Name: "linux-command-safe",
|
|
||||||
Description: "Generate safe Linux commands",
|
|
||||||
Content: "Generate a safe Linux command that won't cause data loss or system damage. Reply with linux command and nothing else. Output with plain response - no need formatting.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: 4,
|
|
||||||
Name: "linux-command-verbose",
|
|
||||||
Description: "Generate Linux commands with detailed explanation",
|
|
||||||
Content: "Generate a Linux command and provide detailed explanation including what each flag does and potential alternatives.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: 5,
|
|
||||||
Name: "linux-command-simple",
|
|
||||||
Description: "Generate simple Linux commands",
|
|
||||||
Content: "Generate a simple, easy-to-understand Linux command. Avoid complex flags and options when possible.",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
pm.Prompts = defaultPrompts
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadAllPrompts загружает все промпты из файла sys_prompts
|
// loadAllPrompts загружает все промпты из файла sys_prompts
|
||||||
@@ -379,3 +346,27 @@ func truncateString(s string, maxLen int) string {
|
|||||||
}
|
}
|
||||||
return s[:maxLen-3] + "..."
|
return s[:maxLen-3] + "..."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetVerbosePromptByLevel возвращает промпт для подробного объяснения по уровню
|
||||||
|
func GetVerbosePromptByLevel(level int) string {
|
||||||
|
// Создаем PromptManager для получения текущего языка из sys_prompts (без принудительной загрузки дефолтов)
|
||||||
|
pm := NewPromptManager("")
|
||||||
|
currentLang := pm.GetCurrentLanguage()
|
||||||
|
|
||||||
|
var prompt *SystemPrompt
|
||||||
|
switch level {
|
||||||
|
case 1:
|
||||||
|
prompt = GetBuiltinPromptByIDAndLanguage(6, currentLang) // v
|
||||||
|
case 2:
|
||||||
|
prompt = GetBuiltinPromptByIDAndLanguage(7, currentLang) // vv
|
||||||
|
case 3:
|
||||||
|
prompt = GetBuiltinPromptByIDAndLanguage(8, currentLang) // vvv
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if prompt != nil {
|
||||||
|
return prompt.Content
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Provider интерфейс для работы с разными LLM провайдерами
|
// Provider интерфейс для работы с разными LLM провайдерами
|
||||||
@@ -112,7 +114,7 @@ func (p *ProxyAPIProvider) Chat(messages []Chat) (string, error) {
|
|||||||
return "", fmt.Errorf("ошибка маршалинга запроса: %w", err)
|
return "", fmt.Errorf("ошибка маршалинга запроса: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequest("POST", p.BaseURL+"/api/v1/protected/sberchat/chat", bytes.NewBuffer(jsonData))
|
req, err := http.NewRequest("POST", p.BaseURL+config.AppConfig.Server.ProxyUrl, bytes.NewBuffer(jsonData))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("ошибка создания запроса: %w", err)
|
return "", fmt.Errorf("ошибка создания запроса: %w", err)
|
||||||
}
|
}
|
||||||
@@ -155,7 +157,7 @@ func (p *ProxyAPIProvider) Chat(messages []Chat) (string, error) {
|
|||||||
|
|
||||||
// Health для ProxyAPIProvider
|
// Health для ProxyAPIProvider
|
||||||
func (p *ProxyAPIProvider) Health() error {
|
func (p *ProxyAPIProvider) Health() error {
|
||||||
req, err := http.NewRequest("GET", p.BaseURL+"/api/v1/protected/sberchat/health", nil)
|
req, err := http.NewRequest("GET", p.BaseURL+config.AppConfig.Server.HealthUrl, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("ошибка создания health check запроса: %w", err)
|
return fmt.Errorf("ошибка создания health check запроса: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
46
kustomize/configmap.yaml
Normal file
46
kustomize/configmap.yaml
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: lcg-config
|
||||||
|
namespace: lcg
|
||||||
|
data:
|
||||||
|
# Основные настройки
|
||||||
|
LCG_VERSION: "v2.0.2"
|
||||||
|
LCG_BASE_PATH: "/lcg"
|
||||||
|
LCG_SERVER_HOST: "0.0.0.0"
|
||||||
|
LCG_SERVER_PORT: "8080"
|
||||||
|
LCG_SERVER_ALLOW_HTTP: "true"
|
||||||
|
LCG_APP_NAME: "Linux Command GPT"
|
||||||
|
LCG_RESULT_FOLDER: "/app/data/results"
|
||||||
|
LCG_PROMPT_FOLDER: "/app/data/prompts"
|
||||||
|
LCG_CONFIG_FOLDER: "/app/data/config"
|
||||||
|
LCG_NO_HISTORY: "false"
|
||||||
|
LCG_ALLOW_EXECUTION: "false"
|
||||||
|
LCG_DEBUG: "false"
|
||||||
|
|
||||||
|
# Настройки аутентификации
|
||||||
|
LCG_SERVER_REQUIRE_AUTH: "true"
|
||||||
|
|
||||||
|
LCG_COOKIE_SECURE: "true"
|
||||||
|
LCG_COOKIE_TTL_HOURS: "168"
|
||||||
|
LCG_DOMAIN: "direct-dev.ru"
|
||||||
|
LCG_COOKIE_PATH: "/lcg"
|
||||||
|
|
||||||
|
# Настройки провайдера (по умолчанию)
|
||||||
|
LCG_PROVIDER_TYPE: "proxy"
|
||||||
|
LCG_HOST: "https://direct-dev.ru"
|
||||||
|
LCG_HEALTH_URL: "/api/v1/protected/sberchat/health"
|
||||||
|
LCG_PROXY_URL: "/api/v1/protected/sberchat/chat"
|
||||||
|
LCG_MODEL: "GigaChat-2-Max"
|
||||||
|
|
||||||
|
# Настройки валидации
|
||||||
|
LCG_MAX_SYSTEM_PROMPT_LENGTH: "2000"
|
||||||
|
LCG_MAX_USER_MESSAGE_LENGTH: "4000"
|
||||||
|
LCG_MAX_PROMPT_NAME_LENGTH: "2000"
|
||||||
|
LCG_MAX_PROMPT_DESC_LENGTH: "50000"
|
||||||
|
|
||||||
|
# Настройки таймаутов
|
||||||
|
LCG_TIMEOUT: "300"
|
||||||
|
|
||||||
|
# Настройки отладки
|
||||||
|
LCG_DEBUG: "false"
|
||||||
95
kustomize/deployment.yaml
Normal file
95
kustomize/deployment.yaml
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: lcg
|
||||||
|
namespace: lcg
|
||||||
|
labels:
|
||||||
|
app: lcg
|
||||||
|
version: v2.0.2
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: lcg
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: lcg
|
||||||
|
version: v2.0.2
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: lcg
|
||||||
|
image: kuznetcovay/lcg:v2.0.2
|
||||||
|
imagePullPolicy: Always
|
||||||
|
ports:
|
||||||
|
- containerPort: 8080
|
||||||
|
protocol: TCP
|
||||||
|
envFrom:
|
||||||
|
- configMapRef:
|
||||||
|
name: lcg-config
|
||||||
|
- secretRef:
|
||||||
|
name: lcg-secret
|
||||||
|
env:
|
||||||
|
# Pod information
|
||||||
|
- name: POD_NAME
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: metadata.name
|
||||||
|
- name: POD_NAMESPACE
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: metadata.namespace
|
||||||
|
- name: NODE_NAME
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: spec.nodeName
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpu: 1000m
|
||||||
|
memory: 1Gi
|
||||||
|
requests:
|
||||||
|
cpu: 250m
|
||||||
|
memory: 512Mi
|
||||||
|
volumeMounts:
|
||||||
|
- name: lcg-data
|
||||||
|
mountPath: /app/data
|
||||||
|
- name: lcg-config
|
||||||
|
mountPath: /app/config
|
||||||
|
readOnly: true
|
||||||
|
# Health checks
|
||||||
|
startupProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /login
|
||||||
|
port: 8080
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 5
|
||||||
|
failureThreshold: 30
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /login
|
||||||
|
port: 8080
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 10
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /login
|
||||||
|
port: 8080
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 60
|
||||||
|
volumes:
|
||||||
|
- name: lcg-data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: lcg-data
|
||||||
|
- name: lcg-config
|
||||||
|
configMap:
|
||||||
|
name: lcg-config
|
||||||
|
# Security context
|
||||||
|
securityContext:
|
||||||
|
runAsNonRoot: true
|
||||||
|
runAsUser: 1001
|
||||||
|
readOnlyRootFilesystem: false
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
capabilities:
|
||||||
|
drop:
|
||||||
|
- ALL
|
||||||
|
restartPolicy: Always
|
||||||
64
kustomize/ingress-route.yaml
Normal file
64
kustomize/ingress-route.yaml
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: IngressRoute
|
||||||
|
metadata:
|
||||||
|
name: lcg-route
|
||||||
|
namespace: lcg
|
||||||
|
labels:
|
||||||
|
app: lcg
|
||||||
|
version: v2.0.2
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- websecure
|
||||||
|
routes:
|
||||||
|
- kind: Rule
|
||||||
|
match: Host(`direct-dev.ru`) && PathPrefix(`/lcg`)
|
||||||
|
services:
|
||||||
|
- name: lcg
|
||||||
|
port: 8080
|
||||||
|
tls:
|
||||||
|
secretName: le-root-direct-dev-ru
|
||||||
|
---
|
||||||
|
|
||||||
|
# apiVersion: traefik.io/v1alpha1
|
||||||
|
# kind: IngressRoute
|
||||||
|
# metadata:
|
||||||
|
# name: lcg-route
|
||||||
|
# namespace: lcg
|
||||||
|
# spec:
|
||||||
|
# entryPoints:
|
||||||
|
# - websecure
|
||||||
|
# routes:
|
||||||
|
# - kind: Rule
|
||||||
|
# match: Host(`direct-dev.ru`) && PathPrefix(`/lcg`)
|
||||||
|
# services:
|
||||||
|
# - name: lcg
|
||||||
|
# port: 8080
|
||||||
|
# middlewares:
|
||||||
|
# - name: lcg-strip-prefix
|
||||||
|
# tls:
|
||||||
|
# secretName: le-root-direct-dev-ru
|
||||||
|
# ---
|
||||||
|
# apiVersion: traefik.io/v1alpha1
|
||||||
|
# kind: Middleware
|
||||||
|
# metadata:
|
||||||
|
# name: lcg-strip-prefix
|
||||||
|
# namespace: lcg
|
||||||
|
# spec:
|
||||||
|
# stripPrefix:
|
||||||
|
# prefixes:
|
||||||
|
# - /lcg
|
||||||
|
# ---
|
||||||
|
# apiVersion: traefik.io/v1alpha1
|
||||||
|
# kind: Middleware
|
||||||
|
# metadata:
|
||||||
|
# name: lcg-headers
|
||||||
|
# namespace: lcg
|
||||||
|
# spec:
|
||||||
|
# headers:
|
||||||
|
# customRequestHeaders:
|
||||||
|
# X-Forwarded-Proto: "https"
|
||||||
|
# X-Forwarded-Port: "443"
|
||||||
|
# customResponseHeaders:
|
||||||
|
# X-Frame-Options: "DENY"
|
||||||
|
# X-Content-Type-Options: "nosniff"
|
||||||
|
# X-XSS-Protection: "1; mode=block"
|
||||||
25
kustomize/kustomization.yaml
Normal file
25
kustomize/kustomization.yaml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
|
||||||
|
# Namespace
|
||||||
|
namespace: lcg
|
||||||
|
|
||||||
|
# Resources
|
||||||
|
resources:
|
||||||
|
- configmap.yaml
|
||||||
|
- secret.yaml
|
||||||
|
- deployment.yaml
|
||||||
|
- service.yaml
|
||||||
|
- ingress-route.yaml
|
||||||
|
|
||||||
|
# Common labels
|
||||||
|
commonLabels:
|
||||||
|
app: lcg
|
||||||
|
version: v2.0.2
|
||||||
|
managed-by: kustomize
|
||||||
|
|
||||||
|
# Images
|
||||||
|
images:
|
||||||
|
- name: lcg
|
||||||
|
newName: kuznetcovay/lcg
|
||||||
|
newTag: v2.0.2
|
||||||
18
kustomize/secret.yaml
Normal file
18
kustomize/secret.yaml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
apiVersion: bitnami.com/v1alpha1
|
||||||
|
kind: SealedSecret
|
||||||
|
metadata:
|
||||||
|
creationTimestamp: null
|
||||||
|
name: lcg-secrets
|
||||||
|
namespace: lcg
|
||||||
|
spec:
|
||||||
|
encryptedData:
|
||||||
|
LCG_CSRF_SECRET: AgDYqxr2Xlf0Hmqgb9u1OildXogus4pFcqB80Dfc1uYw7wnHNOZZP+Hto1OAifXGg2eOYbB1QEsbW/lLSim/C2zpAjNUfRTkwDhQhK3FWmLOjruKBYHZxHyMuhbBmGhVTFbKApOc9zQXTASMLgIeP2vK5yDhVRr97/OJmMKeTOq8FX8CxXZPKTjehronFS3syUlIocboi0PveFf0dgj7nkYePBMST/FKGe2I1NbpOXYH2VrZWOHeoLYDUrEQMQvSAS1mnxAPkdEOuqbWMJA7cs+KKiIFsoy5eBkOXe8hoQqLfddi7ifnJ6h32StQg67qrAm4IMClS/U9iZK5C3Rm/COsywp8Mp3e1iF0hQ2yPuiHMBibxb3aRKwIryJjeo7x51PMMjJaErbTU+bwaAvMav7znOQ14N410bp2Io6KBNsOLW0DG8/mvmQcEn4Q5f4ZtSLcvaq5BQ0NjzrrIv+eCyvNzA28oAyuR0VieJdrUPqpgELrT64MwC+4m5dqdrNPdWkxbOXPG8ghs1nMSSfI+aU+JjT1vupvJA8m83NPqh+YBewmZiTwq5PdvnlAYZ+SYFLZwDWpdUTnK/hqiUkSDOJbE0pbcm03BaG9FjTJpNsmOcKu05Rs8QfWFJMOqYKC4dTA1itxTkyCxz9Y8KFJzX0RXuksN19xzLiZm+e586VtxExEfkYuBo01R39LOtTey9e8pIB9dKwFsHRAORDaZwkaKRe3RlyF8FoSq4khr79CYdGw6IAYzrg8sXAbWawXPg1osQpNR825PejO3/TwbJDPKfjo8RaHHiFa7UhaHbrm/4FwAj4gSGVL
|
||||||
|
LCG_JWT_SECRET: AgBq2Eb0fh1lO0FP2/ebnwuyiTyrAF+dD3iu7uLVgo7JuLnKRA4998rB1OZGRcZ+MNLpajIa20ezJ3aV2wCl24PSJErhzKC3IPSXoLnpJ0EfXrFGS0Pd86TQqAf0TMN2ALFrHY/LywYW09I90/Ct93WqjDKY4yVPbyVni+njUSwavp0iMEWDjENRTdWDK3f/ym1i8SMJgSjX5N6dazwyWVSeWC8oHAGPBF8rr+rhPnY1Ds7QeR/8M0GK7YLiI2nnKljxo7LjXA61jbgCLyiATcWYRylkylcO+ZU8bAsaOSJGXEjtO6s0GHY7Y9CcHQlb1VTZCVfzu/Pb7OSbIiL4wVWHi0DYScTJluVr5SorUjti8HprW9LJE0sy7GIxMdsRZ/7jfJY/s1nxFJph3Q+wkImoPD7R0Njpkwph14CH3xfhPxBpXTQO14hlhx1VYtVkYeWAtcFht+z1qFz0vn+eYkm9B28U+2uxP4WA5AyyMZBPJVekUxQiEmr2nnowbV+zu0+wRtor/lPREQBInbf9tpvth5fpnXtUCS4P2Ne4WWlY0Y5Sij5Z20l0FA0CwmMOliWUcl9vIAy+of3RvM9XGgGqMWTuJP2QFEt7E8VKzM/aI8XgdJuExSCDjd/cyOIgnz5Byy1YvdICqL1CrVpvL0mdy+KMobr59Fgdv+5q0hiHvXF8p2bkG1DHgWiSK3WARDpKzAs+cfsPomD3jMhJZFOopp1Vu9Jpy+l4IsWoY8W1KajPW2DQWjwULRt+ewNr1mAfj+8ESVSXH8zqLv+hkXeef28h7McPcfH9fx9ejs9ovS/E6Sks6EWp
|
||||||
|
LCG_JWT_TOKEN: AgB6PPDflcIav6fqhCi80Ysv9HPkI5zXIjqfot3jaYON3fNmpKNDIhyvKk4YvLbT4PEZfY+JZP/f17MoJ1eikeiZAO7klkg3wNq3h6TcRTuwM/ST9R/KsWqnfLxm5HzGBsqh39cwv9eU2ovAMXqXPJeO/23HcjOqZg7cWZ2WfknaAUydJc39Cue4zmgxlpIxF37p6/rvJqUGByOOUzlDHoVV3TORi+j6dui352PGG6gVCzcCVGNSsbf4j1VibJ1Lz06WEayMi7ZYkD18rsiKHcFGo2SBhEjGGo00Cbdq0EOUTu6k1Q47evHMLFAhdFK3T2gESB4NfMaAL+6gHS7ouI6SbyOCeAZIGT8e3ggM7MlIsNBrLUeDLEwZG8DjHGItY9KcJG/YxbjZ24b9/IzWDduR8VIUG6XCIrGwQd2jlH8GXmrsq+3KkQr81Tj6Z4/QIa4mcgqSKBr8nCzf31GQhhWgj143VwZtPuHUaAbSsZ4ISbo1PoISUaHymWh1J6qjjrzsvfOdeiKHihA8nLe4ggnV8nrQ5EusA+DzmL/Ti+K+2cc277nC0J1pFhuZs37xi+eQT6TMyUeE12uyCHlG1SiwG8t0wfv2N/yzdugW2eILZbRDZnEoN46lLoeXrTGRiFi25/6Jue0/iTo1AV7ameK4J2teGIhYROqB06kResWVECWm2mWhMpJ7Am5ij7tho3Ot75wrmgXXWCb962MzmIpJG8VIimtoIRNVtlu7+cxMb4D9KFb/i69cMkb+7R+Vm6c37T0T0R/o6QCY+MP0w29xgbGz5PNcLEh//avz4E1JI+AsbvtHOi8/aZ9F7c2DcTfXDcxoA2suxJjRy8Y4uu6rrKUWhklla5G/hs0rZsuTM9iruFasV+AybXDLN2/YNqSAj4oDzi/lYNwvQm5CTZwklHK/fwNPbfCNkY5C94rvzW+OJJ2mR1rcHCfHVWYW/IQRfE11mZVX2m0HT70rDPVopYSHrmlvuuTk3ky5gXym+/FOKBq+BcE0GiDDGl3C5VFtiREhpW4J7zRux5QnHk5fIVyEAZlidKsNSNLwq73+E6W77kMNDU7KCRH23A9BIJPOpN87oZDVX1eBghiM/qBOzP04fw3C6dxu+W/OQvTwZmxLtod07Y64EbdaeqJtjnd7GihAEW2jj+Wkcfz9WHTw38cNpyLqcU3ap13790qVJO6V27b0OmiEiloMYyYHUwcHs8wQA946ns0XOz7zw3r1goJgJS6il93dAThK2UBzw3DIY4yJGrmscPZesWSqL3a+ElGjZWz6n9idmIN7L7oViR7A+p17zwFnpczz/VsV+vj8DwSBwLsw6Q==
|
||||||
|
LCG_SERVER_PASSWORD: AgBYXu3YpewbdveXVFDGKBzJe8Gur70LxYSL3kmM7Q65lTU/Q4smpPKhb/bDPntNc4XmNFUfVZ/P5iv2bCdgZB0ccMp6eGZUKKfa7PVJ1/I1hAVwSWYmkD673tbfF7CoGrgtTYw93IaU4t1aWBRiqgapktRawFY1SEIoU6dAx3nrKivyFFJx5akHnm+7vr0GMAtE+H8P/htW+bg1peg5rVYkQwMyeYdefQ+AA/RMDG74XlRGv0EfU6S/LmJ0HF84jb5VNjyJMAD7NssuSUXglpVhfTRwZZAD789/hTgElQnR1JUAIyRHeY6zerU5sXaaS7l+MJGxMMNGsfgOFQ57kzA7Eafumf0ChKfdGl7c4UaEcq6zENSp1Rrzc84WLoghwhvVIhszuGkSE9aLelJJamBMOnm/rlwOuQJJsMAlTiuGO7pewsHKDQXnuZGKOQyAflEX/SNXzUSdYEGonuWGkjWopZrvjO9TwBReFUfsV/ALoDgjA2b7nFpXff0Ffx+EvYtuudDZFw1PobBAHy2aFkMuUP0GxVh14nbmC38VNMiug4+xfl92TxbxRkOkx/tufb2p2QFaglW5TK9I3ysHxiCODhq+YdhlW+gDS+mzBEQZbXd+3TMjh7sE1DGqbloMDhfvJJA6u6C2t6olW3BghU/AS20tOgNC+aIgWzetC6ODOKJQLAq2jjXG+PmUoJDUhs1SbS7uIsIlLIt/Y62b6roVAK81zQPt3w==
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
creationTimestamp: null
|
||||||
|
name: lcg-secrets
|
||||||
|
namespace: lcg
|
||||||
|
---
|
||||||
18
kustomize/service.yaml
Normal file
18
kustomize/service.yaml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: lcg
|
||||||
|
namespace: lcg
|
||||||
|
labels:
|
||||||
|
app: lcg
|
||||||
|
version: v2.0.2
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
ports:
|
||||||
|
- port: 8080
|
||||||
|
targetPort: 8080
|
||||||
|
protocol: TCP
|
||||||
|
name: http
|
||||||
|
selector:
|
||||||
|
app: lcg
|
||||||
|
version: v2.0.2
|
||||||
334
main.go
334
main.go
@@ -2,11 +2,13 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
_ "embed"
|
_ "embed"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"os/user"
|
"os/user"
|
||||||
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -16,13 +18,23 @@ import (
|
|||||||
"github.com/direct-dev-ru/linux-command-gpt/config"
|
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||||
"github.com/direct-dev-ru/linux-command-gpt/gpt"
|
"github.com/direct-dev-ru/linux-command-gpt/gpt"
|
||||||
"github.com/direct-dev-ru/linux-command-gpt/reader"
|
"github.com/direct-dev-ru/linux-command-gpt/reader"
|
||||||
|
"github.com/direct-dev-ru/linux-command-gpt/serve"
|
||||||
|
"github.com/direct-dev-ru/linux-command-gpt/validation"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed VERSION.txt
|
//go:embed VERSION.txt
|
||||||
var Version string
|
var Version string
|
||||||
|
|
||||||
// используем глобальный экземпляр конфига из пакета config
|
//go:embed build-conditions.yaml
|
||||||
|
var BuildConditionsFromYaml string
|
||||||
|
|
||||||
|
type buildConditions struct {
|
||||||
|
NoServe bool `yaml:"no-serve"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var CompileConditions buildConditions
|
||||||
|
|
||||||
// disableHistory управляет записью/обновлением истории на уровне процесса (флаг имеет приоритет над env)
|
// disableHistory управляет записью/обновлением истории на уровне процесса (флаг имеет приоритет над env)
|
||||||
var disableHistory bool
|
var disableHistory bool
|
||||||
@@ -42,6 +54,14 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
||||||
|
if err := yaml.Unmarshal([]byte(BuildConditionsFromYaml), &CompileConditions); err != nil {
|
||||||
|
fmt.Println("Error parsing build conditions:", err)
|
||||||
|
CompileConditions.NoServe = false
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Build conditions:", CompileConditions)
|
||||||
|
|
||||||
_ = colorBlue
|
_ = colorBlue
|
||||||
|
|
||||||
gpt.InitBuiltinPrompts("")
|
gpt.InitBuiltinPrompts("")
|
||||||
@@ -53,7 +73,7 @@ func main() {
|
|||||||
|
|
||||||
app := &cli.App{
|
app := &cli.App{
|
||||||
Name: "lcg",
|
Name: "lcg",
|
||||||
Usage: "Linux Command GPT - Генерация Linux команд из описаний",
|
Usage: config.AppConfig.AppName + " - Генерация Linux команд из описаний",
|
||||||
Version: Version,
|
Version: Version,
|
||||||
Commands: getCommands(),
|
Commands: getCommands(),
|
||||||
UsageText: `
|
UsageText: `
|
||||||
@@ -64,7 +84,7 @@ lcg [опции] <описание команды>
|
|||||||
lcg --file /path/to/file.txt "хочу вывести все директории с помощью ls"
|
lcg --file /path/to/file.txt "хочу вывести все директории с помощью ls"
|
||||||
`,
|
`,
|
||||||
Description: `
|
Description: `
|
||||||
Linux Command GPT - инструмент для генерации Linux команд из описаний на естественном языке.
|
{{.AppName}} - инструмент для генерации Linux команд из описаний на естественном языке.
|
||||||
Поддерживает чтение частей промпта из файлов и позволяет сохранять, копировать или перегенерировать результаты.
|
Поддерживает чтение частей промпта из файлов и позволяет сохранять, копировать или перегенерировать результаты.
|
||||||
может задавать системный промпт или выбирать из предустановленных промптов.
|
может задавать системный промпт или выбирать из предустановленных промптов.
|
||||||
Переменные окружения:
|
Переменные окружения:
|
||||||
@@ -155,6 +175,12 @@ Linux Command GPT - инструмент для генерации Linux ком
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if CompileConditions.NoServe {
|
||||||
|
if len(args) > 1 && args[0] == "serve" {
|
||||||
|
printColored("❌ Error: serve command is disabled in this build\n", colorRed)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
executeMain(file, system, strings.Join(args, " "), timeout)
|
executeMain(file, system, strings.Join(args, " "), timeout)
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
@@ -176,7 +202,7 @@ Linux Command GPT - инструмент для генерации Linux ком
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getCommands() []*cli.Command {
|
func getCommands() []*cli.Command {
|
||||||
return []*cli.Command{
|
commands := []*cli.Command{
|
||||||
{
|
{
|
||||||
Name: "update-key",
|
Name: "update-key",
|
||||||
Aliases: []string{"u"},
|
Aliases: []string{"u"},
|
||||||
@@ -306,24 +332,20 @@ func getCommands() []*cli.Command {
|
|||||||
Name: "config",
|
Name: "config",
|
||||||
Aliases: []string{"co"}, // Изменено с "c" на "co"
|
Aliases: []string{"co"}, // Изменено с "c" на "co"
|
||||||
Usage: "Show current configuration",
|
Usage: "Show current configuration",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "full",
|
||||||
|
Aliases: []string{"f"},
|
||||||
|
Usage: "Show full configuration object",
|
||||||
|
},
|
||||||
|
},
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
fmt.Printf("Provider: %s\n", config.AppConfig.ProviderType)
|
if c.Bool("full") {
|
||||||
fmt.Printf("Host: %s\n", config.AppConfig.Host)
|
// Выводим полную конфигурацию в JSON формате
|
||||||
fmt.Printf("Model: %s\n", config.AppConfig.Model)
|
showFullConfig()
|
||||||
fmt.Printf("Prompt: %s\n", config.AppConfig.Prompt)
|
} else {
|
||||||
fmt.Printf("Timeout: %s seconds\n", config.AppConfig.Timeout)
|
// Выводим краткую конфигурацию
|
||||||
if config.AppConfig.ProviderType == "proxy" {
|
showShortConfig()
|
||||||
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***"
|
|
||||||
}())
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
@@ -517,8 +539,7 @@ func getCommands() []*cli.Command {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "serve-result",
|
Name: "serve",
|
||||||
Aliases: []string{"serve"},
|
|
||||||
Usage: "Start HTTP server to browse saved results",
|
Usage: "Start HTTP server to browse saved results",
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
@@ -533,22 +554,106 @@ func getCommands() []*cli.Command {
|
|||||||
Usage: "Server host",
|
Usage: "Server host",
|
||||||
Value: config.AppConfig.Server.Host,
|
Value: config.AppConfig.Server.Host,
|
||||||
},
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "browser",
|
||||||
|
Aliases: []string{"b"},
|
||||||
|
Usage: "Open browser automatically after starting server",
|
||||||
|
Value: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
port := c.String("port")
|
port := c.String("port")
|
||||||
host := c.String("host")
|
host := c.String("host")
|
||||||
|
openBrowser := c.Bool("browser")
|
||||||
|
|
||||||
printColored(fmt.Sprintf("🌐 Запускаю HTTP сервер на %s:%s\n", host, port), colorCyan)
|
// Пробрасываем глобальный флаг debug для web-сервера
|
||||||
|
// Позволяет запускать: lcg -d serve -p ...
|
||||||
|
if c.Bool("debug") {
|
||||||
|
config.AppConfig.MainFlags.Debug = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем конфигурацию сервера с новыми параметрами
|
||||||
|
config.AppConfig.Server.Host = host
|
||||||
|
config.AppConfig.Server.Port = port
|
||||||
|
// Пересчитываем AllowHTTP на основе нового хоста
|
||||||
|
config.AppConfig.Server.AllowHTTP = getServerAllowHTTPForHost(host)
|
||||||
|
|
||||||
|
// Определяем протокол на основе хоста
|
||||||
|
useHTTPS := !config.AppConfig.Server.AllowHTTP
|
||||||
|
protocol := "http"
|
||||||
|
if useHTTPS {
|
||||||
|
protocol = "https"
|
||||||
|
}
|
||||||
|
|
||||||
|
printColored(fmt.Sprintf("🌐 Запускаю %s сервер на %s:%s\n", strings.ToUpper(protocol), host, port), colorCyan)
|
||||||
printColored(fmt.Sprintf("📁 Папка результатов: %s\n", config.AppConfig.ResultFolder), colorYellow)
|
printColored(fmt.Sprintf("📁 Папка результатов: %s\n", config.AppConfig.ResultFolder), colorYellow)
|
||||||
printColored(fmt.Sprintf("🔗 Откройте в браузере: http://%s:%s\n", host, port), colorGreen)
|
|
||||||
|
|
||||||
return cmdPackage.StartResultServer(host, port)
|
// Предупреждение о самоподписанном сертификате
|
||||||
|
if useHTTPS {
|
||||||
|
printColored("⚠️ Используется самоподписанный SSL сертификат\n", colorYellow)
|
||||||
|
printColored(" Браузер может показать предупреждение о безопасности\n", colorYellow)
|
||||||
|
printColored(" Нажмите 'Дополнительно' → 'Перейти на сайт' для продолжения\n", colorYellow)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Для автооткрытия браузера заменяем 0.0.0.0 на localhost
|
||||||
|
browserHost := host
|
||||||
|
if host == "0.0.0.0" {
|
||||||
|
browserHost = "localhost"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Учитываем BasePath в URL
|
||||||
|
basePath := config.AppConfig.Server.BasePath
|
||||||
|
if basePath == "" || basePath == "/" {
|
||||||
|
basePath = ""
|
||||||
|
} else {
|
||||||
|
basePath = strings.TrimSuffix(basePath, "/")
|
||||||
|
}
|
||||||
|
url := fmt.Sprintf("%s://%s:%s%s", protocol, browserHost, port, basePath)
|
||||||
|
|
||||||
|
if openBrowser {
|
||||||
|
printColored("🌍 Открываю браузер...\n", colorGreen)
|
||||||
|
if err := openBrowserURL(url); err != nil {
|
||||||
|
printColored(fmt.Sprintf("⚠️ Не удалось открыть браузер: %v\n", err), colorYellow)
|
||||||
|
printColored("📱 Откройте браузер вручную и перейдите по адресу: ", colorGreen)
|
||||||
|
printColored(url+"\n", colorYellow)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
printColored("🔗 Откройте в браузере: ", colorGreen)
|
||||||
|
printColored(url+"\n", colorYellow)
|
||||||
|
}
|
||||||
|
|
||||||
|
return serve.StartResultServer(host, port)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if CompileConditions.NoServe {
|
||||||
|
filteredCommands := []*cli.Command{}
|
||||||
|
for _, cmd := range commands {
|
||||||
|
if cmd.Name != "serve" {
|
||||||
|
filteredCommands = append(filteredCommands, cmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
commands = filteredCommands
|
||||||
|
}
|
||||||
|
|
||||||
|
return commands
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func executeMain(file, system, commandInput string, timeout int) {
|
func executeMain(file, system, commandInput string, timeout int) {
|
||||||
|
// Валидация длины пользовательского сообщения
|
||||||
|
if err := validation.ValidateUserMessage(commandInput); err != nil {
|
||||||
|
printColored(fmt.Sprintf("❌ Ошибка: %s\n", err.Error()), colorRed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Валидация длины системного промпта
|
||||||
|
if err := validation.ValidateSystemPrompt(system); err != nil {
|
||||||
|
printColored(fmt.Sprintf("❌ Ошибка: %s\n", err.Error()), colorRed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Выводим debug информацию если включен флаг
|
// Выводим debug информацию если включен флаг
|
||||||
if config.AppConfig.MainFlags.Debug {
|
if config.AppConfig.MainFlags.Debug {
|
||||||
printDebugInfo(file, system, commandInput, timeout)
|
printDebugInfo(file, system, commandInput, timeout)
|
||||||
@@ -668,7 +773,14 @@ func getCommand(gpt3 gpt.Gpt3, cmd string) (string, float64) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handlePostResponse(response string, gpt3 gpt.Gpt3, system, cmd string, timeout int, explanation string) {
|
func handlePostResponse(response string, gpt3 gpt.Gpt3, system, cmd string, timeout int, explanation string) {
|
||||||
fmt.Printf("Действия: (c)копировать, (s)сохранить, (r)перегенерировать, (e)выполнить, (v|vv|vvv)подробно, (n)ничего: ")
|
// Формируем меню действий
|
||||||
|
menu := "Действия: (c)копировать, (s)сохранить, (r)перегенерировать"
|
||||||
|
if config.AppConfig.AllowExecution {
|
||||||
|
menu += ", (e)выполнить"
|
||||||
|
}
|
||||||
|
menu += ", (v|vv|vvv)подробно, (n)ничего: "
|
||||||
|
|
||||||
|
fmt.Print(menu)
|
||||||
var choice string
|
var choice string
|
||||||
fmt.Scanln(&choice)
|
fmt.Scanln(&choice)
|
||||||
|
|
||||||
@@ -700,6 +812,7 @@ func handlePostResponse(response string, gpt3 gpt.Gpt3, system, cmd string, time
|
|||||||
fmt.Println("🔄 Перегенерирую...")
|
fmt.Println("🔄 Перегенерирую...")
|
||||||
executeMain("", system, cmd, timeout)
|
executeMain("", system, cmd, timeout)
|
||||||
case "e":
|
case "e":
|
||||||
|
if config.AppConfig.AllowExecution {
|
||||||
executeCommand(response)
|
executeCommand(response)
|
||||||
if !disableHistory {
|
if !disableHistory {
|
||||||
if fromHistory {
|
if fromHistory {
|
||||||
@@ -708,6 +821,9 @@ func handlePostResponse(response string, gpt3 gpt.Gpt3, system, cmd string, time
|
|||||||
cmdPackage.SaveToHistory(config.AppConfig.ResultHistory, config.AppConfig.ResultFolder, cmd, response, gpt3.Prompt)
|
cmdPackage.SaveToHistory(config.AppConfig.ResultHistory, config.AppConfig.ResultFolder, cmd, response, gpt3.Prompt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Println("⚠️ Выполнение команд отключено. Установите LCG_ALLOW_EXECUTION=1 для включения этой функции.")
|
||||||
|
}
|
||||||
case "v", "vv", "vvv":
|
case "v", "vv", "vvv":
|
||||||
level := len(choice) // 1, 2, 3
|
level := len(choice) // 1, 2, 3
|
||||||
deps := cmdPackage.ExplainDeps{
|
deps := cmdPackage.ExplainDeps{
|
||||||
@@ -782,7 +898,9 @@ func showTips() {
|
|||||||
fmt.Println(" • Команда 'history list' покажет историю запросов")
|
fmt.Println(" • Команда 'history list' покажет историю запросов")
|
||||||
fmt.Println(" • Команда 'config' покажет текущие настройки")
|
fmt.Println(" • Команда 'config' покажет текущие настройки")
|
||||||
fmt.Println(" • Команда 'health' проверит доступность API")
|
fmt.Println(" • Команда 'health' проверит доступность API")
|
||||||
fmt.Println(" • Команда 'serve-result' запустит HTTP сервер для просмотра результатов")
|
fmt.Println(" • Команда 'serve' запустит HTTP сервер для просмотра результатов")
|
||||||
|
fmt.Println(" • Используйте --browser для автоматического открытия браузера")
|
||||||
|
fmt.Println(" • Установите LCG_BROWSER_PATH для указания конкретного браузера")
|
||||||
}
|
}
|
||||||
|
|
||||||
// printDebugInfo выводит отладочную информацию о параметрах запроса
|
// printDebugInfo выводит отладочную информацию о параметрах запроса
|
||||||
@@ -798,3 +916,163 @@ func printDebugInfo(file, system, commandInput string, timeout int) {
|
|||||||
fmt.Printf("📝 История: %t\n", !config.AppConfig.MainFlags.NoHistory)
|
fmt.Printf("📝 История: %t\n", !config.AppConfig.MainFlags.NoHistory)
|
||||||
printColored("────────────────────────────────────────\n", colorCyan)
|
printColored("────────────────────────────────────────\n", colorCyan)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// openBrowserURL открывает URL в браузере
|
||||||
|
func openBrowserURL(url string) error {
|
||||||
|
// Проверяем переменную окружения LCG_BROWSER_PATH
|
||||||
|
if browserPath := os.Getenv("LCG_BROWSER_PATH"); browserPath != "" {
|
||||||
|
return exec.Command(browserPath, url).Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Список браузеров в порядке приоритета
|
||||||
|
browsers := []string{
|
||||||
|
"yandex-browser", // Яндекс.Браузер
|
||||||
|
"yandex-browser-stable", // Яндекс.Браузер (стабильная версия)
|
||||||
|
"firefox", // Mozilla Firefox
|
||||||
|
"firefox-esr", // Firefox ESR
|
||||||
|
"google-chrome", // Google Chrome
|
||||||
|
"google-chrome-stable", // Google Chrome (стабильная версия)
|
||||||
|
"chromium", // Chromium
|
||||||
|
"chromium-browser", // Chromium (Ubuntu/Debian)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Стандартные пути для поиска браузеров
|
||||||
|
paths := []string{
|
||||||
|
"/usr/bin",
|
||||||
|
"/usr/local/bin",
|
||||||
|
"/opt/google/chrome",
|
||||||
|
"/opt/yandex/browser",
|
||||||
|
"/snap/bin",
|
||||||
|
"/usr/lib/chromium-browser",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ищем браузер в указанном порядке
|
||||||
|
for _, browser := range browsers {
|
||||||
|
for _, path := range paths {
|
||||||
|
fullPath := filepath.Join(path, browser)
|
||||||
|
if _, err := os.Stat(fullPath); err == nil {
|
||||||
|
return exec.Command(fullPath, url).Start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Также пробуем найти в PATH
|
||||||
|
if _, err := exec.LookPath(browser); err == nil {
|
||||||
|
return exec.Command(browser, url).Start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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"`
|
||||||
|
Completions string `json:"completions"`
|
||||||
|
Model string `json:"model"`
|
||||||
|
Prompt string `json:"prompt"`
|
||||||
|
ApiKeyFile string `json:"api_key_file"`
|
||||||
|
ResultFolder string `json:"result_folder"`
|
||||||
|
PromptFolder string `json:"prompt_folder"`
|
||||||
|
ProviderType string `json:"provider_type"`
|
||||||
|
JwtToken string `json:"jwt_token"` // Показываем статус, не сам токен
|
||||||
|
PromptID string `json:"prompt_id"`
|
||||||
|
Timeout string `json:"timeout"`
|
||||||
|
ResultHistory string `json:"result_history"`
|
||||||
|
NoHistoryEnv string `json:"no_history_env"`
|
||||||
|
AllowExecution bool `json:"allow_execution"`
|
||||||
|
MainFlags config.MainFlags `json:"main_flags"`
|
||||||
|
Server config.ServerConfig `json:"server"`
|
||||||
|
Validation config.ValidationConfig `json:"validation"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем безопасную копию конфигурации
|
||||||
|
safeConfig := SafeConfig{
|
||||||
|
Cwd: config.AppConfig.Cwd,
|
||||||
|
Host: config.AppConfig.Host,
|
||||||
|
Completions: config.AppConfig.Completions,
|
||||||
|
Model: config.AppConfig.Model,
|
||||||
|
Prompt: config.AppConfig.Prompt,
|
||||||
|
ApiKeyFile: config.AppConfig.ApiKeyFile,
|
||||||
|
ResultFolder: config.AppConfig.ResultFolder,
|
||||||
|
PromptFolder: config.AppConfig.PromptFolder,
|
||||||
|
ProviderType: config.AppConfig.ProviderType,
|
||||||
|
JwtToken: func() string {
|
||||||
|
if config.AppConfig.JwtToken != "" {
|
||||||
|
return "***set***"
|
||||||
|
}
|
||||||
|
currentUser, _ := user.Current()
|
||||||
|
jwtFile := currentUser.HomeDir + "/.proxy_jwt_token"
|
||||||
|
if _, err := os.Stat(jwtFile); err == nil {
|
||||||
|
return "***from file***"
|
||||||
|
}
|
||||||
|
return "***not set***"
|
||||||
|
}(),
|
||||||
|
PromptID: config.AppConfig.PromptID,
|
||||||
|
Timeout: config.AppConfig.Timeout,
|
||||||
|
ResultHistory: config.AppConfig.ResultHistory,
|
||||||
|
NoHistoryEnv: config.AppConfig.NoHistoryEnv,
|
||||||
|
AllowExecution: config.AppConfig.AllowExecution,
|
||||||
|
MainFlags: config.AppConfig.MainFlags,
|
||||||
|
Server: config.AppConfig.Server,
|
||||||
|
Validation: config.AppConfig.Validation,
|
||||||
|
}
|
||||||
|
|
||||||
|
safeConfig.Server.Password = "***"
|
||||||
|
|
||||||
|
// Выводим JSON с отступами
|
||||||
|
jsonData, err := json.MarshalIndent(safeConfig, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Ошибка сериализации конфигурации: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(string(jsonData))
|
||||||
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
package main
|
|
||||||
110
serve/README.md
Normal file
110
serve/README.md
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
# Пакет serve
|
||||||
|
|
||||||
|
Этот пакет содержит HTTP сервер для веб-интерфейса LCG (Linux Command GPT).
|
||||||
|
|
||||||
|
## Структура файлов
|
||||||
|
|
||||||
|
### serve.go
|
||||||
|
|
||||||
|
Основной файл пакета. Содержит:
|
||||||
|
|
||||||
|
- `StartResultServer()` - функция запуска HTTP сервера
|
||||||
|
- `registerRoutes()` - регистрация всех маршрутов
|
||||||
|
|
||||||
|
### results.go
|
||||||
|
|
||||||
|
Обработчики для результатов и файлов:
|
||||||
|
|
||||||
|
- `handleResultsPage()` - главная страница со списком файлов результатов
|
||||||
|
- `handleFileView()` - просмотр конкретного файла
|
||||||
|
- `handleDeleteFile()` - удаление файла результата
|
||||||
|
- `getResultFiles()` - получение списка файлов
|
||||||
|
- `formatFileSize()` - форматирование размера файла
|
||||||
|
|
||||||
|
### history.go
|
||||||
|
|
||||||
|
Обработчики для работы с историей запросов:
|
||||||
|
|
||||||
|
- `handleHistoryPage()` - страница истории запросов
|
||||||
|
- `handleDeleteHistoryEntry()` - удаление записи из истории
|
||||||
|
- `handleClearHistory()` - очистка всей истории
|
||||||
|
- `readHistoryEntries()` - чтение записей истории
|
||||||
|
|
||||||
|
### history_utils.go
|
||||||
|
|
||||||
|
Утилиты для работы с историей:
|
||||||
|
|
||||||
|
- `HistoryEntry` - структура записи истории
|
||||||
|
- `read()` - чтение истории из файла
|
||||||
|
- `write()` - запись истории в файл
|
||||||
|
- `DeleteHistoryEntry()` - удаление записи по индексу
|
||||||
|
|
||||||
|
### prompts.go
|
||||||
|
|
||||||
|
Обработчики для управления промптами:
|
||||||
|
|
||||||
|
- `handlePromptsPage()` - страница управления промптами
|
||||||
|
- `handleAddPrompt()` - добавление нового промпта
|
||||||
|
- `handleEditPrompt()` - редактирование промпта
|
||||||
|
- `handleDeletePrompt()` - удаление промпта
|
||||||
|
- `handleRestorePrompt()` - восстановление системного промпта к значению по умолчанию
|
||||||
|
- `handleRestoreVerbosePrompt()` - восстановление verbose промпта
|
||||||
|
- `handleSaveLang()` - сохранение промптов при переключении языка
|
||||||
|
|
||||||
|
### prompts_helpers.go
|
||||||
|
|
||||||
|
Вспомогательные функции для работы с промптами:
|
||||||
|
|
||||||
|
- `getVerbosePromptsFromFile()` - получение verbose промптов из файла
|
||||||
|
- `translateVerbosePrompt()` - перевод verbose промпта
|
||||||
|
- `getVerbosePrompts()` - получение встроенных verbose промптов (fallback)
|
||||||
|
- `getSystemPromptsWithLang()` - получение системных промптов с учетом языка
|
||||||
|
- `translateSystemPrompt()` - перевод системного промпта
|
||||||
|
|
||||||
|
## Использование
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "github.com/direct-dev-ru/linux-command-gpt/serve"
|
||||||
|
|
||||||
|
// Запуск сервера на localhost:8080
|
||||||
|
err := serve.StartResultServer("localhost", "8080")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Маршруты
|
||||||
|
|
||||||
|
### Результаты
|
||||||
|
|
||||||
|
- `GET /` - главная страница со списком файлов
|
||||||
|
- `GET /file/{filename}` - просмотр файла результата
|
||||||
|
- `DELETE /delete/{filename}` - удаление файла
|
||||||
|
|
||||||
|
### История
|
||||||
|
|
||||||
|
- `GET /history` - страница истории запросов
|
||||||
|
- `GET /history/view/{id}` - просмотр записи истории в развернутом виде
|
||||||
|
- `DELETE /history/delete/{id}` - удаление записи
|
||||||
|
- `DELETE /history/clear` - очистка всей истории
|
||||||
|
|
||||||
|
### Промпты
|
||||||
|
|
||||||
|
- `GET /prompts` - страница управления промптами
|
||||||
|
- `POST /prompts/add` - добавление промпта
|
||||||
|
- `PUT /prompts/edit/{id}` - редактирование промпта
|
||||||
|
- `DELETE /prompts/delete/{id}` - удаление промпта
|
||||||
|
- `POST /prompts/restore/{id}` - восстановление системного промпта
|
||||||
|
- `POST /prompts/restore-verbose/{mode}` - восстановление verbose промпта (v/vv/vvv)
|
||||||
|
- `POST /prompts/save-lang` - сохранение языка промптов
|
||||||
|
|
||||||
|
### Выполнение запросов
|
||||||
|
|
||||||
|
- `GET /run` - веб-страница для выполнения запросов
|
||||||
|
- `POST /run` - обработка выполнения запроса
|
||||||
|
- `POST /execute` - API для программного доступа (только curl)
|
||||||
|
|
||||||
|
## Особенности
|
||||||
|
|
||||||
|
- **Многоязычность**: Поддержка английского и русского языков для промптов
|
||||||
|
- **Responsive дизайн**: Адаптивный интерфейс для различных устройств
|
||||||
|
- **Markdown**: Автоматическая конвертация Markdown файлов в HTML
|
||||||
|
- **История**: Поиск дубликатов с учетом регистра
|
||||||
|
- **Промпты**: Управление встроенными и пользовательскими промптами
|
||||||
242
serve/api.go
Normal file
242
serve/api.go
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
package serve
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||||
|
"github.com/direct-dev-ru/linux-command-gpt/validation"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SaveResultRequest представляет запрос на сохранение результата
|
||||||
|
type SaveResultRequest struct {
|
||||||
|
Prompt string `json:"prompt"`
|
||||||
|
Command string `json:"command"`
|
||||||
|
Explanation string `json:"explanation,omitempty"`
|
||||||
|
Model string `json:"model"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveResultResponse представляет ответ на сохранение результата
|
||||||
|
type SaveResultResponse struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
File string `json:"file,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddToHistoryRequest представляет запрос на добавление в историю
|
||||||
|
type AddToHistoryRequest struct {
|
||||||
|
Prompt string `json:"prompt"`
|
||||||
|
Command string `json:"command"`
|
||||||
|
Response string `json:"response"`
|
||||||
|
Explanation string `json:"explanation,omitempty"`
|
||||||
|
System string `json:"system"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddToHistoryResponse представляет ответ на добавление в историю
|
||||||
|
type AddToHistoryResponse struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleSaveResult обрабатывает сохранение результата
|
||||||
|
func handleSaveResult(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req SaveResultRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Prompt == "" || req.Command == "" {
|
||||||
|
http.Error(w, "Prompt and command are required", http.StatusBadRequest)
|
||||||
|
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{
|
||||||
|
Success: false,
|
||||||
|
Error: "Failed to create result folder",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Генерируем имя файла
|
||||||
|
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
||||||
|
filename := fmt.Sprintf("gpt_request_%s_%s.md", req.Model, timestamp)
|
||||||
|
filePath := path.Join(config.AppConfig.ResultFolder, filename)
|
||||||
|
title := truncateTitle(req.Prompt)
|
||||||
|
|
||||||
|
// Формируем содержимое
|
||||||
|
var content string
|
||||||
|
if strings.TrimSpace(req.Explanation) != "" {
|
||||||
|
content = fmt.Sprintf("# %s\n\n## Prompt\n\n%s\n\n## Response\n\n%s\n\n## Explanation\n\n%s\n",
|
||||||
|
title, req.Prompt, req.Command, req.Explanation)
|
||||||
|
} else {
|
||||||
|
content = fmt.Sprintf("# %s\n\n## Prompt\n\n%s\n\n## Response\n\n%s\n",
|
||||||
|
title, req.Prompt, req.Command)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохраняем файл
|
||||||
|
if err := os.WriteFile(filePath, []byte(content), 0644); err != nil {
|
||||||
|
apiJsonResponse(w, SaveResultResponse{
|
||||||
|
Success: false,
|
||||||
|
Error: "Failed to save file",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug вывод для сохранения результата
|
||||||
|
PrintWebSaveDebugInfo("SAVE_RESULT", req.Prompt, req.Command, req.Explanation, req.Model, filename)
|
||||||
|
|
||||||
|
apiJsonResponse(w, SaveResultResponse{
|
||||||
|
Success: true,
|
||||||
|
Message: "Result saved successfully",
|
||||||
|
File: filename,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleAddToHistory обрабатывает добавление в историю
|
||||||
|
func handleAddToHistory(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req AddToHistoryRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Prompt == "" || req.Command == "" || req.Response == "" {
|
||||||
|
http.Error(w, "Prompt, command and response are required", http.StatusBadRequest)
|
||||||
|
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 {
|
||||||
|
// Если файл не существует, создаем пустой массив
|
||||||
|
entries = []HistoryEntry{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ищем дубликат
|
||||||
|
duplicateIndex := -1
|
||||||
|
for i, entry := range entries {
|
||||||
|
if strings.EqualFold(strings.TrimSpace(entry.Command), strings.TrimSpace(req.Prompt)) {
|
||||||
|
duplicateIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем новую запись
|
||||||
|
newEntry := HistoryEntry{
|
||||||
|
Index: len(entries) + 1,
|
||||||
|
Command: req.Prompt,
|
||||||
|
Response: req.Response,
|
||||||
|
Explanation: req.Explanation,
|
||||||
|
System: req.System,
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if duplicateIndex == -1 {
|
||||||
|
// Добавляем новую запись
|
||||||
|
entries = append(entries, newEntry)
|
||||||
|
} else {
|
||||||
|
// Перезаписываем существующую
|
||||||
|
newEntry.Index = entries[duplicateIndex].Index
|
||||||
|
entries[duplicateIndex] = newEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохраняем историю
|
||||||
|
if err := Write(config.AppConfig.ResultHistory, entries); err != nil {
|
||||||
|
apiJsonResponse(w, AddToHistoryResponse{
|
||||||
|
Success: false,
|
||||||
|
Error: "Failed to save to history",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
message := "Added to history successfully"
|
||||||
|
if duplicateIndex != -1 {
|
||||||
|
message = "Updated existing history entry"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug вывод для добавления в историю
|
||||||
|
PrintWebSaveDebugInfo("ADD_TO_HISTORY", req.Prompt, req.Command, req.Explanation, req.System, "")
|
||||||
|
|
||||||
|
apiJsonResponse(w, AddToHistoryResponse{
|
||||||
|
Success: true,
|
||||||
|
Message: message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// apiJsonResponse отправляет JSON ответ
|
||||||
|
func apiJsonResponse(w http.ResponseWriter, data interface{}) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
if err := json.NewEncoder(w).Encode(data); err != nil {
|
||||||
|
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// truncateTitle сокращает строку до 120 символов (по рунам), добавляя " ..." при усечении
|
||||||
|
func truncateTitle(s string) string {
|
||||||
|
const maxLen = 120
|
||||||
|
if runeCount := len([]rune(s)); runeCount <= maxLen {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
const head = 116
|
||||||
|
r := []rune(s)
|
||||||
|
if len(r) <= head {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return string(r[:head]) + " ..."
|
||||||
|
}
|
||||||
269
serve/auth.go
Normal file
269
serve/auth.go
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
package serve
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JWTClaims представляет claims для JWT токена
|
||||||
|
type JWTClaims struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
jwt.RegisteredClaims
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthRequest представляет запрос на аутентификацию
|
||||||
|
type AuthRequest struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthResponse представляет ответ на аутентификацию
|
||||||
|
type AuthResponse struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// JWTSecretKey генерирует или загружает секретный ключ для JWT
|
||||||
|
func getJWTSecretKey() ([]byte, error) {
|
||||||
|
// Пытаемся загрузить из переменной окружения
|
||||||
|
if secret := os.Getenv("LCG_JWT_SECRET"); secret != "" {
|
||||||
|
return []byte(secret), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Пытаемся загрузить из файла
|
||||||
|
secretFile := fmt.Sprintf("%s/server/jwt_secret", config.AppConfig.Server.ConfigFolder)
|
||||||
|
if data, err := os.ReadFile(secretFile); err == nil {
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Генерируем новый секретный ключ
|
||||||
|
secret := make([]byte, 32)
|
||||||
|
if _, err := rand.Read(secret); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to generate JWT secret: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем директорию если не существует
|
||||||
|
if err := os.MkdirAll(fmt.Sprintf("%s/server", config.AppConfig.Server.ConfigFolder), 0755); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create config directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохраняем секретный ключ в файл
|
||||||
|
if err := os.WriteFile(secretFile, secret, 0600); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to save JWT secret: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return secret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateJWTToken создает JWT токен для пользователя
|
||||||
|
func generateJWTToken(username string) (string, error) {
|
||||||
|
secret, err := getJWTSecretKey()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем claims
|
||||||
|
claims := JWTClaims{
|
||||||
|
Username: username,
|
||||||
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)), // Токен действителен 24 часа
|
||||||
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||||
|
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||||
|
Issuer: "lcg-server",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем токен
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
|
return token.SignedString(secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateJWTToken проверяет JWT токен
|
||||||
|
func validateJWTToken(tokenString string) (*JWTClaims, error) {
|
||||||
|
secret, err := getJWTSecretKey()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Парсим токен
|
||||||
|
token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||||
|
// Проверяем метод подписи
|
||||||
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
|
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||||
|
}
|
||||||
|
return secret, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем валидность токена
|
||||||
|
if claims, ok := token.Claims.(*JWTClaims); ok && token.Valid {
|
||||||
|
return claims, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("invalid token")
|
||||||
|
}
|
||||||
|
|
||||||
|
// getTokenFromCookie извлекает JWT токен из cookies
|
||||||
|
func getTokenFromCookie(r *http.Request) (string, error) {
|
||||||
|
cookie, err := r.Cookie("auth_token")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return cookie.Value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// setAuthCookie устанавливает HTTP-only cookie с JWT токеном
|
||||||
|
func setAuthCookie(w http.ResponseWriter, token string) {
|
||||||
|
cookie := &http.Cookie{
|
||||||
|
Name: "auth_token",
|
||||||
|
Domain: config.AppConfig.Server.Domain,
|
||||||
|
Value: token,
|
||||||
|
Path: config.AppConfig.Server.CookiePath,
|
||||||
|
HttpOnly: true,
|
||||||
|
Secure: config.AppConfig.Server.CookieSecure,
|
||||||
|
SameSite: http.SameSiteLaxMode, // Более мягкий режим для reverse proxy
|
||||||
|
MaxAge: config.AppConfig.Server.CookieTTLHours * 60 * 60,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем домен если указан
|
||||||
|
if config.AppConfig.Server.Domain != "" {
|
||||||
|
cookie.Domain = config.AppConfig.Server.Domain
|
||||||
|
}
|
||||||
|
|
||||||
|
http.SetCookie(w, cookie)
|
||||||
|
}
|
||||||
|
|
||||||
|
// clearAuthCookie удаляет cookie с токеном
|
||||||
|
func clearAuthCookie(w http.ResponseWriter) {
|
||||||
|
cookie := &http.Cookie{
|
||||||
|
Name: "auth_token",
|
||||||
|
Value: "",
|
||||||
|
Path: config.AppConfig.Server.CookiePath,
|
||||||
|
HttpOnly: true,
|
||||||
|
Secure: config.AppConfig.Server.CookieSecure,
|
||||||
|
SameSite: http.SameSiteLaxMode,
|
||||||
|
MaxAge: -1, // Удаляем cookie
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем домен если указан
|
||||||
|
if config.AppConfig.Server.Domain != "" {
|
||||||
|
cookie.Domain = config.AppConfig.Server.Domain
|
||||||
|
}
|
||||||
|
|
||||||
|
http.SetCookie(w, cookie)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleLogin обрабатывает запрос на вход
|
||||||
|
func handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req AuthRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
apiJsonResponse(w, AuthResponse{
|
||||||
|
Success: false,
|
||||||
|
Error: "Invalid request body",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем пароль
|
||||||
|
if req.Password != config.AppConfig.Server.Password {
|
||||||
|
apiJsonResponse(w, AuthResponse{
|
||||||
|
Success: false,
|
||||||
|
Error: "Неверный пароль",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Генерируем JWT токен
|
||||||
|
token, err := generateJWTToken(req.Username)
|
||||||
|
if err != nil {
|
||||||
|
apiJsonResponse(w, AuthResponse{
|
||||||
|
Success: false,
|
||||||
|
Error: "Failed to generate token",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Устанавливаем cookie
|
||||||
|
setAuthCookie(w, token)
|
||||||
|
|
||||||
|
apiJsonResponse(w, AuthResponse{
|
||||||
|
Success: true,
|
||||||
|
Message: "Успешная авторизация",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleLogout обрабатывает запрос на выход
|
||||||
|
func handleLogout(w http.ResponseWriter, r *http.Request) {
|
||||||
|
clearAuthCookie(w)
|
||||||
|
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleValidateToken обрабатывает проверку валидности токена
|
||||||
|
func handleValidateToken(w http.ResponseWriter, r *http.Request) {
|
||||||
|
token, err := getTokenFromCookie(r)
|
||||||
|
if err != nil {
|
||||||
|
apiJsonResponse(w, AuthResponse{
|
||||||
|
Success: false,
|
||||||
|
Error: "Token not found",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = validateJWTToken(token)
|
||||||
|
if err != nil {
|
||||||
|
apiJsonResponse(w, AuthResponse{
|
||||||
|
Success: false,
|
||||||
|
Error: "Invalid token",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiJsonResponse(w, AuthResponse{
|
||||||
|
Success: true,
|
||||||
|
Message: "Token is valid",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// requireAuth middleware проверяет аутентификацию
|
||||||
|
func requireAuth(next http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Проверяем, требуется ли аутентификация
|
||||||
|
if !config.AppConfig.Server.RequireAuth {
|
||||||
|
next(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем токен из cookie
|
||||||
|
token, err := getTokenFromCookie(r)
|
||||||
|
if err != nil {
|
||||||
|
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем валидность токена
|
||||||
|
_, err = validateJWTToken(token)
|
||||||
|
if err != nil {
|
||||||
|
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Токен валиден, продолжаем
|
||||||
|
next(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
263
serve/csrf.go
Normal file
263
serve/csrf.go
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
package serve
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CSRFManager управляет CSRF токенами
|
||||||
|
type CSRFManager struct {
|
||||||
|
secretKey []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// CSRFData содержит данные для CSRF токена
|
||||||
|
type CSRFData struct {
|
||||||
|
Token string
|
||||||
|
Timestamp int64
|
||||||
|
UserID string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCSRFManager создает новый менеджер CSRF
|
||||||
|
func NewCSRFManager() (*CSRFManager, error) {
|
||||||
|
secret, err := getCSRFSecretKey()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &CSRFManager{secretKey: secret}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getCSRFSecretKey получает или генерирует секретный ключ для CSRF
|
||||||
|
func getCSRFSecretKey() ([]byte, error) {
|
||||||
|
// Пытаемся загрузить из переменной окружения
|
||||||
|
if secret := os.Getenv("LCG_CSRF_SECRET"); secret != "" {
|
||||||
|
return []byte(secret), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Пытаемся загрузить из файла
|
||||||
|
secretFile := fmt.Sprintf("%s/server/csrf_secret", config.AppConfig.Server.ConfigFolder)
|
||||||
|
if data, err := os.ReadFile(secretFile); err == nil {
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Генерируем новый секретный ключ
|
||||||
|
secret := make([]byte, 32)
|
||||||
|
if _, err := rand.Read(secret); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to generate CSRF secret: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем директорию если не существует
|
||||||
|
if err := os.MkdirAll(fmt.Sprintf("%s/server", config.AppConfig.Server.ConfigFolder), 0755); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create config directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохраняем секретный ключ в файл
|
||||||
|
if err := os.WriteFile(secretFile, secret, 0600); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to save CSRF secret: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return secret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateToken генерирует CSRF токен для пользователя
|
||||||
|
func (c *CSRFManager) GenerateToken(userID string) (string, error) {
|
||||||
|
// Создаем данные токена
|
||||||
|
data := CSRFData{
|
||||||
|
Token: generateRandomString(32),
|
||||||
|
Timestamp: time.Now().Unix(),
|
||||||
|
UserID: userID,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем подпись
|
||||||
|
signature := c.createSignature(data)
|
||||||
|
|
||||||
|
// Кодируем данные в base64
|
||||||
|
encodedData := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%d:%s", data.Token, data.Timestamp, data.UserID)))
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s.%s", encodedData, signature), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateToken проверяет CSRF токен
|
||||||
|
func (c *CSRFManager) ValidateToken(token, userID string) bool {
|
||||||
|
// Разделяем токен на данные и подпись
|
||||||
|
parts := splitToken(token)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
encodedData, signature := parts[0], parts[1]
|
||||||
|
|
||||||
|
// Декодируем данные
|
||||||
|
dataBytes, err := base64.StdEncoding.DecodeString(encodedData)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Парсим данные
|
||||||
|
dataParts := splitString(string(dataBytes), ":")
|
||||||
|
if len(dataParts) != 3 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenValue, timestampStr, tokenUserID := dataParts[0], dataParts[1], dataParts[2]
|
||||||
|
|
||||||
|
// Проверяем пользователя
|
||||||
|
if tokenUserID != userID {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем время жизни токена (24 часа)
|
||||||
|
timestamp, err := parseInt64(timestampStr)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if time.Now().Unix()-timestamp > 24*60*60 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем данные для проверки подписи
|
||||||
|
data := CSRFData{
|
||||||
|
Token: tokenValue,
|
||||||
|
Timestamp: timestamp,
|
||||||
|
UserID: tokenUserID,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем подпись
|
||||||
|
expectedSignature := c.createSignature(data)
|
||||||
|
return signature == expectedSignature
|
||||||
|
}
|
||||||
|
|
||||||
|
// createSignature создает подпись для данных
|
||||||
|
func (c *CSRFManager) createSignature(data CSRFData) string {
|
||||||
|
message := fmt.Sprintf("%s:%d:%s", data.Token, data.Timestamp, data.UserID)
|
||||||
|
hash := sha256.Sum256(append(c.secretKey, []byte(message)...))
|
||||||
|
return hex.EncodeToString(hash[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// getTokenFromCookie извлекает CSRF токен из cookie
|
||||||
|
func GetCSRFTokenFromCookie(r *http.Request) string {
|
||||||
|
cookie, err := r.Cookie("csrf_token")
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return cookie.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
// setCSRFCookie устанавливает CSRF токен в cookie
|
||||||
|
func setCSRFCookie(w http.ResponseWriter, token string) {
|
||||||
|
cookie := &http.Cookie{
|
||||||
|
Name: "csrf_token",
|
||||||
|
Value: token,
|
||||||
|
Path: config.AppConfig.Server.CookiePath,
|
||||||
|
HttpOnly: true,
|
||||||
|
Secure: config.AppConfig.Server.CookieSecure,
|
||||||
|
SameSite: http.SameSiteLaxMode, // Более мягкий режим для reverse proxy
|
||||||
|
MaxAge: 1 * 60 * 60,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем домен если указан
|
||||||
|
if config.AppConfig.Server.Domain != "" {
|
||||||
|
cookie.Domain = config.AppConfig.Server.Domain
|
||||||
|
}
|
||||||
|
|
||||||
|
http.SetCookie(w, cookie)
|
||||||
|
}
|
||||||
|
|
||||||
|
// clearCSRFCookie удаляет CSRF cookie
|
||||||
|
func СlearCSRFCookie(w http.ResponseWriter) {
|
||||||
|
cookie := &http.Cookie{
|
||||||
|
Name: "csrf_token",
|
||||||
|
Value: "",
|
||||||
|
Path: config.AppConfig.Server.CookiePath,
|
||||||
|
HttpOnly: true,
|
||||||
|
Secure: config.AppConfig.Server.CookieSecure,
|
||||||
|
SameSite: http.SameSiteLaxMode,
|
||||||
|
MaxAge: -1,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем домен если указан
|
||||||
|
if config.AppConfig.Server.Domain != "" {
|
||||||
|
cookie.Domain = config.AppConfig.Server.Domain
|
||||||
|
}
|
||||||
|
|
||||||
|
http.SetCookie(w, cookie)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateRandomString генерирует случайную строку
|
||||||
|
func generateRandomString(length int) string {
|
||||||
|
bytes := make([]byte, length)
|
||||||
|
rand.Read(bytes)
|
||||||
|
return base64.URLEncoding.EncodeToString(bytes)[:length]
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitToken разделяет токен на части
|
||||||
|
func splitToken(token string) []string {
|
||||||
|
// Ищем последнюю точку
|
||||||
|
lastDot := -1
|
||||||
|
for i := len(token) - 1; i >= 0; i-- {
|
||||||
|
if token[i] == '.' {
|
||||||
|
lastDot = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if lastDot == -1 {
|
||||||
|
return []string{token}
|
||||||
|
}
|
||||||
|
|
||||||
|
return []string{token[:lastDot], token[lastDot+1:]}
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitString разделяет строку по разделителю
|
||||||
|
func splitString(s, sep string) []string {
|
||||||
|
if s == "" {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []string
|
||||||
|
start := 0
|
||||||
|
for i := 0; i < len(s); i++ {
|
||||||
|
if i+len(sep) <= len(s) && s[i:i+len(sep)] == sep {
|
||||||
|
result = append(result, s[start:i])
|
||||||
|
start = i + len(sep)
|
||||||
|
i += len(sep) - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = append(result, s[start:])
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseInt64 парсит строку в int64
|
||||||
|
func parseInt64(s string) (int64, error) {
|
||||||
|
var result int64
|
||||||
|
for _, char := range s {
|
||||||
|
if char < '0' || char > '9' {
|
||||||
|
return 0, fmt.Errorf("invalid number: %s", s)
|
||||||
|
}
|
||||||
|
result = result*10 + int64(char-'0')
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Глобальный экземпляр CSRF менеджера
|
||||||
|
var csrfManager *CSRFManager
|
||||||
|
|
||||||
|
// InitCSRFManager инициализирует глобальный CSRF менеджер
|
||||||
|
func InitCSRFManager() error {
|
||||||
|
var err error
|
||||||
|
csrfManager, err = NewCSRFManager()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCSRFManager возвращает глобальный CSRF менеджер
|
||||||
|
func GetCSRFManager() *CSRFManager {
|
||||||
|
return csrfManager
|
||||||
|
}
|
||||||
56
serve/debug.go
Normal file
56
serve/debug.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package serve
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PrintWebDebugInfo выводит отладочную информацию для веб-запросов
|
||||||
|
func PrintWebDebugInfo(operation, prompt, systemPrompt, model string, timeout int) {
|
||||||
|
if !config.AppConfig.MainFlags.Debug {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\n🔍 DEBUG WEB %s:\n", operation)
|
||||||
|
fmt.Printf("💬 Запрос: %s\n", prompt)
|
||||||
|
fmt.Printf("🤖 Системный промпт: %s\n", systemPrompt)
|
||||||
|
fmt.Printf("⏱️ Таймаут: %d сек\n", timeout)
|
||||||
|
fmt.Printf("🌐 Провайдер: %s\n", config.AppConfig.ProviderType)
|
||||||
|
fmt.Printf("🏠 Хост: %s\n", config.AppConfig.Host)
|
||||||
|
fmt.Printf("🧠 Модель: %s\n", model)
|
||||||
|
fmt.Printf("📝 История: %t\n", !config.AppConfig.MainFlags.NoHistory)
|
||||||
|
fmt.Printf("────────────────────────────────────────\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrintWebVerboseDebugInfo выводит отладочную информацию для verbose запросов
|
||||||
|
func PrintWebVerboseDebugInfo(operation, prompt, verbosePrompt, model string, level int, timeout int) {
|
||||||
|
if !config.AppConfig.MainFlags.Debug {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\n🔍 DEBUG WEB %s (v%d):\n", operation, level)
|
||||||
|
fmt.Printf("💬 Запрос: %s\n", prompt)
|
||||||
|
fmt.Printf("📝 Системный промпт подробности:\n%s\n", verbosePrompt)
|
||||||
|
fmt.Printf("⏱️ Таймаут: %d сек\n", timeout)
|
||||||
|
fmt.Printf("🌐 Провайдер: %s\n", config.AppConfig.ProviderType)
|
||||||
|
fmt.Printf("🏠 Хост: %s\n", config.AppConfig.Host)
|
||||||
|
fmt.Printf("🧠 Модель: %s\n", model)
|
||||||
|
fmt.Printf("🎯 Уровень подробности: %d\n", level)
|
||||||
|
fmt.Printf("────────────────────────────────────────\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrintWebSaveDebugInfo выводит отладочную информацию для сохранения
|
||||||
|
func PrintWebSaveDebugInfo(operation, prompt, command, explanation, model, file string) {
|
||||||
|
if !config.AppConfig.MainFlags.Debug {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\n🔍 DEBUG WEB %s:\n", operation)
|
||||||
|
fmt.Printf("💬 Запрос: %s\n", prompt)
|
||||||
|
fmt.Printf("⚡ Команда: %s\n", command)
|
||||||
|
fmt.Printf("📖 Объяснение: %s\n", explanation)
|
||||||
|
fmt.Printf("🧠 Модель: %s\n", model)
|
||||||
|
fmt.Printf("📁 Файл: %s\n", file)
|
||||||
|
fmt.Printf("────────────────────────────────────────\n")
|
||||||
|
}
|
||||||
200
serve/execute.go
Normal file
200
serve/execute.go
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
package serve
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"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 представляет запрос на выполнение
|
||||||
|
type ExecuteRequest struct {
|
||||||
|
Prompt string `json:"prompt"` // Пользовательский промпт
|
||||||
|
SystemID int `json:"system_id"` // ID системного промпта (1-5)
|
||||||
|
SystemText string `json:"system"` // Текст системного промпта (альтернатива system_id)
|
||||||
|
Verbose string `json:"verbose"` // Степень подробности: "v", "vv", "vvv" или пустая строка
|
||||||
|
Timeout int `json:"timeout"` // Таймаут в секундах (опционально)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecuteResponse представляет ответ
|
||||||
|
type ExecuteResponse struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Command string `json:"command,omitempty"`
|
||||||
|
Explanation string `json:"explanation,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
Model string `json:"model,omitempty"`
|
||||||
|
Elapsed float64 `json:"elapsed,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleExecute обрабатывает POST запросы на выполнение
|
||||||
|
func handleExecute(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Проверяем User-Agent - только curl
|
||||||
|
userAgent := r.Header.Get("User-Agent")
|
||||||
|
if !strings.Contains(strings.ToLower(userAgent), "curl") {
|
||||||
|
http.Error(w, "Only curl requests are allowed", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем метод
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Парсим JSON
|
||||||
|
var req ExecuteRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Валидация обязательных полей
|
||||||
|
if req.Prompt == "" {
|
||||||
|
http.Error(w, "Prompt is required", http.StatusBadRequest)
|
||||||
|
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
|
||||||
|
pm := gpt.NewPromptManager(config.AppConfig.PromptFolder)
|
||||||
|
prompt, err := pm.GetPromptByID(req.SystemID)
|
||||||
|
if err != nil {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Устанавливаем таймаут
|
||||||
|
timeout := req.Timeout
|
||||||
|
if timeout <= 0 {
|
||||||
|
timeout = 120 // По умолчанию 2 минуты
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем GPT клиент
|
||||||
|
gpt3 := gpt.NewGpt3(
|
||||||
|
config.AppConfig.ProviderType,
|
||||||
|
config.AppConfig.Host,
|
||||||
|
config.AppConfig.JwtToken,
|
||||||
|
config.AppConfig.Model,
|
||||||
|
systemPrompt,
|
||||||
|
0.01,
|
||||||
|
timeout,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Выполняем запрос
|
||||||
|
response, elapsed := getCommand(*gpt3, req.Prompt)
|
||||||
|
if response == "" {
|
||||||
|
jsonResponse(w, ExecuteResponse{
|
||||||
|
Success: false,
|
||||||
|
Error: "Failed to get response from AI",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если запрошено подробное объяснение
|
||||||
|
if req.Verbose != "" {
|
||||||
|
explanation, err := getDetailedExplanation(req.Prompt, req.Verbose, timeout)
|
||||||
|
if err != nil {
|
||||||
|
jsonResponse(w, ExecuteResponse{
|
||||||
|
Success: false,
|
||||||
|
Error: fmt.Sprintf("Failed to get explanation: %v", err),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonResponse(w, ExecuteResponse{
|
||||||
|
Success: true,
|
||||||
|
Command: response,
|
||||||
|
Explanation: explanation,
|
||||||
|
Model: config.AppConfig.Model,
|
||||||
|
Elapsed: elapsed,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
jsonResponse(w, ExecuteResponse{
|
||||||
|
Success: true,
|
||||||
|
Command: response,
|
||||||
|
Model: config.AppConfig.Model,
|
||||||
|
Elapsed: elapsed,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getCommand выполняет запрос к AI
|
||||||
|
func getCommand(gpt3 gpt.Gpt3, prompt string) (string, float64) {
|
||||||
|
gpt3.InitKey()
|
||||||
|
start := time.Now()
|
||||||
|
response := gpt3.Completions(prompt)
|
||||||
|
elapsed := time.Since(start).Seconds()
|
||||||
|
return response, elapsed
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDetailedExplanation получает подробное объяснение
|
||||||
|
func getDetailedExplanation(prompt, verbose string, timeout int) (string, error) {
|
||||||
|
level := len(verbose) // 1, 2, 3
|
||||||
|
|
||||||
|
// Получаем системный промпт для подробного объяснения
|
||||||
|
detailedSystem := gpt.GetVerbosePromptByLevel(level)
|
||||||
|
if detailedSystem == "" {
|
||||||
|
return "", fmt.Errorf("invalid verbose level: %s", verbose)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем GPT клиент для объяснения
|
||||||
|
explanationGpt := gpt.NewGpt3(
|
||||||
|
config.AppConfig.ProviderType,
|
||||||
|
config.AppConfig.Host,
|
||||||
|
config.AppConfig.JwtToken,
|
||||||
|
config.AppConfig.Model,
|
||||||
|
detailedSystem,
|
||||||
|
0.2,
|
||||||
|
timeout,
|
||||||
|
)
|
||||||
|
|
||||||
|
explanationGpt.InitKey()
|
||||||
|
explanation := explanationGpt.Completions(prompt)
|
||||||
|
|
||||||
|
if explanation == "" {
|
||||||
|
return "", fmt.Errorf("failed to get explanation")
|
||||||
|
}
|
||||||
|
|
||||||
|
return explanation, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// jsonResponse отправляет JSON ответ
|
||||||
|
func jsonResponse(w http.ResponseWriter, response ExecuteResponse) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(response)
|
||||||
|
}
|
||||||
355
serve/execute_page.go
Normal file
355
serve/execute_page.go
Normal file
@@ -0,0 +1,355 @@
|
|||||||
|
package serve
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
"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"
|
||||||
|
"github.com/russross/blackfriday/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ExecutePageData содержит данные для страницы выполнения
|
||||||
|
type ExecutePageData struct {
|
||||||
|
Title string
|
||||||
|
Header string
|
||||||
|
CurrentPrompt string
|
||||||
|
SystemOptions []SystemPromptOption
|
||||||
|
ResultSection template.HTML
|
||||||
|
VerboseButtons template.HTML
|
||||||
|
ActionButtons template.HTML
|
||||||
|
CSRFToken string
|
||||||
|
ProviderType string
|
||||||
|
Model string
|
||||||
|
Host string
|
||||||
|
BasePath string
|
||||||
|
AppName string
|
||||||
|
// Поля конфигурации для валидации
|
||||||
|
MaxUserMessageLength int
|
||||||
|
}
|
||||||
|
|
||||||
|
// SystemPromptOption представляет опцию системного промпта
|
||||||
|
type SystemPromptOption struct {
|
||||||
|
ID int
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecuteResultData содержит результат выполнения
|
||||||
|
type ExecuteResultData struct {
|
||||||
|
Success bool
|
||||||
|
Command string
|
||||||
|
Explanation string
|
||||||
|
Error string
|
||||||
|
Model string
|
||||||
|
Elapsed float64
|
||||||
|
Verbose string
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleExecutePage обрабатывает страницу выполнения
|
||||||
|
func handleExecutePage(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
// Показываем форму
|
||||||
|
showExecuteForm(w, r)
|
||||||
|
case http.MethodPost:
|
||||||
|
// Обрабатываем выполнение
|
||||||
|
handleExecuteRequest(w, r)
|
||||||
|
default:
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// showExecuteForm показывает форму выполнения
|
||||||
|
func showExecuteForm(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Генерируем CSRF токен
|
||||||
|
csrfManager := GetCSRFManager()
|
||||||
|
if csrfManager == nil {
|
||||||
|
http.Error(w, "CSRF manager not initialized", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем сессионный ID
|
||||||
|
sessionID := getSessionID(r)
|
||||||
|
csrfToken, err := csrfManager.GenerateToken(sessionID)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Failed to generate CSRF token", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Устанавливаем CSRF токен в cookie
|
||||||
|
setCSRFCookie(w, csrfToken)
|
||||||
|
|
||||||
|
// Получаем системные промпты
|
||||||
|
pm := gpt.NewPromptManager(config.AppConfig.PromptFolder)
|
||||||
|
|
||||||
|
var systemOptions []SystemPromptOption
|
||||||
|
for i := 1; i <= 5; i++ {
|
||||||
|
prompt, err := pm.GetPromptByID(i)
|
||||||
|
if err == nil {
|
||||||
|
systemOptions = append(systemOptions, SystemPromptOption{
|
||||||
|
ID: prompt.ID,
|
||||||
|
Name: prompt.Name,
|
||||||
|
Description: prompt.Description,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data := ExecutePageData{
|
||||||
|
Title: "Выполнение запроса",
|
||||||
|
Header: "Выполнение запроса",
|
||||||
|
CurrentPrompt: "",
|
||||||
|
SystemOptions: systemOptions,
|
||||||
|
ResultSection: template.HTML(""),
|
||||||
|
VerboseButtons: template.HTML(""),
|
||||||
|
ActionButtons: template.HTML(""),
|
||||||
|
CSRFToken: csrfToken,
|
||||||
|
ProviderType: config.AppConfig.ProviderType,
|
||||||
|
Model: config.AppConfig.Model,
|
||||||
|
Host: config.AppConfig.Host,
|
||||||
|
BasePath: getBasePath(),
|
||||||
|
AppName: config.AppConfig.AppName,
|
||||||
|
MaxUserMessageLength: config.AppConfig.Validation.MaxUserMessageLength,
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
templates.ExecutePageTemplate.Execute(w, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleExecuteRequest обрабатывает запрос на выполнение
|
||||||
|
func handleExecuteRequest(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Парсим форму
|
||||||
|
prompt := r.FormValue("prompt")
|
||||||
|
systemIDStr := r.FormValue("system_id")
|
||||||
|
verbose := r.FormValue("verbose")
|
||||||
|
|
||||||
|
// Получаем системные промпты
|
||||||
|
pm := gpt.NewPromptManager(config.AppConfig.PromptFolder)
|
||||||
|
|
||||||
|
if prompt == "" {
|
||||||
|
http.Error(w, "Prompt is required", http.StatusBadRequest)
|
||||||
|
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 {
|
||||||
|
systemID = id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем системный промпт
|
||||||
|
systemPrompt, err := pm.GetPromptByID(systemID)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Failed to get system prompt", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Валидация длины системного промпта
|
||||||
|
if err := validation.ValidateSystemPrompt(systemPrompt.Content); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем GPT клиент
|
||||||
|
gpt3 := gpt.NewGpt3(
|
||||||
|
config.AppConfig.ProviderType,
|
||||||
|
config.AppConfig.Host,
|
||||||
|
config.AppConfig.JwtToken,
|
||||||
|
config.AppConfig.Model,
|
||||||
|
systemPrompt.Content,
|
||||||
|
0.01,
|
||||||
|
120,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Debug вывод для основного запроса
|
||||||
|
PrintWebDebugInfo("EXECUTE", prompt, systemPrompt.Content, config.AppConfig.Model, 120)
|
||||||
|
|
||||||
|
// Выполняем запрос
|
||||||
|
response, elapsed := getCommand(*gpt3, prompt)
|
||||||
|
|
||||||
|
var result ExecuteResultData
|
||||||
|
if response == "" {
|
||||||
|
result = ExecuteResultData{
|
||||||
|
Success: false,
|
||||||
|
Error: "Failed to get response from AI",
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result = ExecuteResultData{
|
||||||
|
Success: true,
|
||||||
|
Command: response,
|
||||||
|
Model: config.AppConfig.Model,
|
||||||
|
Elapsed: elapsed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если запрошено подробное объяснение
|
||||||
|
if verbose != "" {
|
||||||
|
level := len(verbose)
|
||||||
|
verbosePrompt := gpt.GetVerbosePromptByLevel(level)
|
||||||
|
|
||||||
|
// Debug вывод для verbose запроса
|
||||||
|
PrintWebVerboseDebugInfo("VERBOSE", prompt, verbosePrompt, config.AppConfig.Model, level, 120)
|
||||||
|
|
||||||
|
explanation, err := getDetailedExplanation(prompt, verbose, 120)
|
||||||
|
if err == nil {
|
||||||
|
// Конвертируем Markdown в HTML
|
||||||
|
explanationHTML := blackfriday.Run([]byte(explanation))
|
||||||
|
result.Explanation = string(explanationHTML)
|
||||||
|
result.Verbose = verbose
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем системные промпты для dropdown
|
||||||
|
var systemOptions []SystemPromptOption
|
||||||
|
for i := 1; i <= 5; i++ {
|
||||||
|
prompt, err := pm.GetPromptByID(i)
|
||||||
|
if err == nil {
|
||||||
|
systemOptions = append(systemOptions, SystemPromptOption{
|
||||||
|
ID: prompt.ID,
|
||||||
|
Name: prompt.Name,
|
||||||
|
Description: prompt.Description,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Генерируем CSRF токен для результата
|
||||||
|
csrfManager := GetCSRFManager()
|
||||||
|
sessionID := getSessionID(r)
|
||||||
|
csrfToken, err := csrfManager.GenerateToken(sessionID)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Failed to generate CSRF token", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data := ExecutePageData{
|
||||||
|
Title: "Результат выполнения",
|
||||||
|
Header: "Результат выполнения",
|
||||||
|
CurrentPrompt: prompt,
|
||||||
|
SystemOptions: systemOptions,
|
||||||
|
ResultSection: template.HTML(formatResultSection(result)),
|
||||||
|
VerboseButtons: template.HTML(formatVerboseButtons(result)),
|
||||||
|
ActionButtons: template.HTML(formatActionButtons(result)),
|
||||||
|
CSRFToken: csrfToken,
|
||||||
|
ProviderType: config.AppConfig.ProviderType,
|
||||||
|
Model: config.AppConfig.Model,
|
||||||
|
Host: config.AppConfig.Host,
|
||||||
|
BasePath: getBasePath(),
|
||||||
|
AppName: config.AppConfig.AppName,
|
||||||
|
MaxUserMessageLength: config.AppConfig.Validation.MaxUserMessageLength,
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
templates.ExecutePageTemplate.Execute(w, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatResultSection форматирует секцию результата
|
||||||
|
func formatResultSection(result ExecuteResultData) string {
|
||||||
|
if !result.Success {
|
||||||
|
return fmt.Sprintf(`
|
||||||
|
<div class="result-section">
|
||||||
|
<div class="error-message">
|
||||||
|
<h3>❌ Ошибка</h3>
|
||||||
|
<p>%s</p>
|
||||||
|
</div>
|
||||||
|
</div>`, result.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
explanationSection := ""
|
||||||
|
if result.Explanation != "" {
|
||||||
|
explanationSection = fmt.Sprintf(`
|
||||||
|
<div class="explanation-section">
|
||||||
|
<h3>📖 Подробное объяснение (%s):</h3>
|
||||||
|
<div class="explanation-content">%s</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
// Показываем кнопку "Наверх" после загрузки объяснения
|
||||||
|
showScrollToTopButton();
|
||||||
|
</script>`, result.Verbose, result.Explanation)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Определяем, содержит ли результат Markdown/многострочный текст
|
||||||
|
useMarkdown := false
|
||||||
|
if strings.Contains(result.Command, "```") || strings.Contains(result.Command, "\n") || strings.Contains(result.Command, "#") || strings.Contains(result.Command, "*") || strings.Contains(result.Command, "_") {
|
||||||
|
useMarkdown = true
|
||||||
|
}
|
||||||
|
|
||||||
|
commandBlock := ""
|
||||||
|
if useMarkdown {
|
||||||
|
// Рендерим Markdown в HTML
|
||||||
|
cmdHTML := blackfriday.Run([]byte(result.Command))
|
||||||
|
commandBlock = fmt.Sprintf(`<div class="command-md">%s</div>`, string(cmdHTML))
|
||||||
|
} else {
|
||||||
|
// Оставляем как простой однострочный вывод команды
|
||||||
|
commandBlock = fmt.Sprintf(`<div class="command-code">%s</div>`, result.Command)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf(`
|
||||||
|
<div class="result-section">
|
||||||
|
<div class="command-result">
|
||||||
|
<h3>✅ Команда:</h3>
|
||||||
|
%s
|
||||||
|
<div class="result-meta">
|
||||||
|
<span>Модель: %s</span>
|
||||||
|
<span>Время: %.2f сек</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
%s
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
// Сохраняем результаты в скрытое поле
|
||||||
|
(function() {
|
||||||
|
const resultData = {
|
||||||
|
command: %s,
|
||||||
|
explanation: %s,
|
||||||
|
model: %s
|
||||||
|
};
|
||||||
|
const resultDataField = document.getElementById('resultData');
|
||||||
|
if (resultDataField) {
|
||||||
|
resultDataField.value = JSON.stringify(resultData);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>`,
|
||||||
|
commandBlock, result.Model, result.Elapsed, explanationSection,
|
||||||
|
fmt.Sprintf(`"%s"`, strings.ReplaceAll(result.Command, `"`, `\"`)),
|
||||||
|
fmt.Sprintf(`"%s"`, strings.ReplaceAll(result.Explanation, `"`, `\"`)),
|
||||||
|
fmt.Sprintf(`"%s"`, result.Model))
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatVerboseButtons форматирует кнопки подробности
|
||||||
|
func formatVerboseButtons(result ExecuteResultData) string {
|
||||||
|
if !result.Success || result.Explanation != "" {
|
||||||
|
return "" // Скрываем кнопки если есть ошибка или уже есть объяснение
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="verbose-buttons">
|
||||||
|
<button onclick="requestExplanation('v')" class="verbose-btn v-btn">v - Краткое объяснение</button>
|
||||||
|
<button onclick="requestExplanation('vv')" class="verbose-btn vv-btn">vv - Подробное объяснение</button>
|
||||||
|
<button onclick="requestExplanation('vvv')" class="verbose-btn vvv-btn">vvv - Максимально подробное</button>
|
||||||
|
</div>`
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatActionButtons форматирует кнопки действий
|
||||||
|
func formatActionButtons(result ExecuteResultData) string {
|
||||||
|
if !result.Success {
|
||||||
|
return "" // Скрываем кнопки если есть ошибка
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="action-buttons">
|
||||||
|
<button onclick="saveResult()" class="action-btn">💾 Сохранить результат</button>
|
||||||
|
<button onclick="addToHistory()" class="action-btn">📝 Добавить в историю</button>
|
||||||
|
</div>`
|
||||||
|
}
|
||||||
206
serve/history.go
Normal file
206
serve/history.go
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
package serve
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||||
|
"github.com/direct-dev-ru/linux-command-gpt/serve/templates"
|
||||||
|
"github.com/russross/blackfriday/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HistoryEntryInfo содержит информацию о записи истории для отображения
|
||||||
|
type HistoryEntryInfo struct {
|
||||||
|
Index int
|
||||||
|
Command string
|
||||||
|
Response string
|
||||||
|
Timestamp string
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleHistoryPage обрабатывает страницу истории запросов
|
||||||
|
func handleHistoryPage(w http.ResponseWriter, r *http.Request) {
|
||||||
|
historyEntries, err := readHistoryEntries()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("Ошибка чтения истории: %v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl := templates.HistoryPageTemplate
|
||||||
|
|
||||||
|
t, err := template.New("history").Parse(tmpl)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Ошибка шаблона", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data := struct {
|
||||||
|
Entries []HistoryEntryInfo
|
||||||
|
BasePath string
|
||||||
|
AppName string
|
||||||
|
}{
|
||||||
|
Entries: historyEntries,
|
||||||
|
BasePath: getBasePath(),
|
||||||
|
AppName: config.AppConfig.AppName,
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
t.Execute(w, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// readHistoryEntries читает записи истории
|
||||||
|
func readHistoryEntries() ([]HistoryEntryInfo, error) {
|
||||||
|
entries, err := Read(config.AppConfig.ResultHistory)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сортируем записи по времени в убывающем порядке (новые сначала)
|
||||||
|
sort.Slice(entries, func(i, j int) bool {
|
||||||
|
return entries[i].Timestamp.After(entries[j].Timestamp)
|
||||||
|
})
|
||||||
|
|
||||||
|
var result []HistoryEntryInfo
|
||||||
|
for _, entry := range entries {
|
||||||
|
result = append(result, HistoryEntryInfo{
|
||||||
|
Index: entry.Index,
|
||||||
|
Command: entry.Command,
|
||||||
|
Response: entry.Response,
|
||||||
|
Timestamp: entry.Timestamp.Format("02.01.2006 15:04:05"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleDeleteHistoryEntry обрабатывает удаление записи истории
|
||||||
|
func handleDeleteHistoryEntry(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "DELETE" {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Убираем BasePath из URL перед извлечением индекса
|
||||||
|
basePath := config.AppConfig.Server.BasePath
|
||||||
|
var indexStr string
|
||||||
|
if basePath != "" && basePath != "/" {
|
||||||
|
basePath = strings.TrimSuffix(basePath, "/")
|
||||||
|
indexStr = strings.TrimPrefix(r.URL.Path, basePath+"/history/delete/")
|
||||||
|
} else {
|
||||||
|
indexStr = strings.TrimPrefix(r.URL.Path, "/history/delete/")
|
||||||
|
}
|
||||||
|
index, err := strconv.Atoi(indexStr)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Invalid index", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = DeleteHistoryEntry(config.AppConfig.ResultHistory, index)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("Ошибка удаления: %v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte("Запись успешно удалена"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleClearHistory обрабатывает очистку всей истории
|
||||||
|
func handleClearHistory(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "DELETE" {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := os.WriteFile(config.AppConfig.ResultHistory, []byte("[]"), 0644)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("Ошибка очистки: %v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte("История успешно очищена"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleHistoryView обрабатывает просмотр записи истории
|
||||||
|
func handleHistoryView(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Получаем индекс из URL, учитывая BasePath
|
||||||
|
basePath := config.AppConfig.Server.BasePath
|
||||||
|
var indexStr string
|
||||||
|
if basePath != "" && basePath != "/" {
|
||||||
|
basePath = strings.TrimSuffix(basePath, "/")
|
||||||
|
indexStr = strings.TrimPrefix(r.URL.Path, basePath+"/history/view/")
|
||||||
|
} else {
|
||||||
|
indexStr = strings.TrimPrefix(r.URL.Path, "/history/view/")
|
||||||
|
}
|
||||||
|
index, err := strconv.Atoi(indexStr)
|
||||||
|
if err != nil {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Читаем записи истории
|
||||||
|
entries, err := Read(config.AppConfig.ResultHistory)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("Ошибка чтения истории: %v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ищем запись с нужным индексом
|
||||||
|
var targetEntry *HistoryEntry
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.Index == index {
|
||||||
|
targetEntry = &entry
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if targetEntry == nil {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Формируем объяснение, если оно есть
|
||||||
|
explanationSection := ""
|
||||||
|
if strings.TrimSpace(targetEntry.Explanation) != "" {
|
||||||
|
// Конвертируем Markdown в HTML
|
||||||
|
explanationHTML := blackfriday.Run([]byte(targetEntry.Explanation))
|
||||||
|
explanationSection = fmt.Sprintf(`
|
||||||
|
<div class="history-explanation">
|
||||||
|
<h3>📖 Подробное объяснение:</h3>
|
||||||
|
<div class="history-explanation-content">%s</div>
|
||||||
|
</div>`, string(explanationHTML))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем данные для шаблона
|
||||||
|
data := struct {
|
||||||
|
Index int
|
||||||
|
Timestamp string
|
||||||
|
Command string
|
||||||
|
Response string
|
||||||
|
ExplanationHTML template.HTML
|
||||||
|
BasePath string
|
||||||
|
}{
|
||||||
|
Index: index,
|
||||||
|
Timestamp: targetEntry.Timestamp.Format("02.01.2006 15:04:05"),
|
||||||
|
Command: targetEntry.Command,
|
||||||
|
Response: targetEntry.Response,
|
||||||
|
ExplanationHTML: template.HTML(explanationSection),
|
||||||
|
BasePath: getBasePath(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Парсим и выполняем шаблон
|
||||||
|
tmpl := templates.HistoryViewTemplate
|
||||||
|
t, err := template.New("history_view").Parse(tmpl)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Ошибка шаблона", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
t.Execute(w, data)
|
||||||
|
}
|
||||||
78
serve/history_utils.go
Normal file
78
serve/history_utils.go
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
package serve
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HistoryEntry представляет запись в истории
|
||||||
|
type HistoryEntry struct {
|
||||||
|
Index int `json:"index"`
|
||||||
|
Command string `json:"command"`
|
||||||
|
Response string `json:"response"`
|
||||||
|
Explanation string `json:"explanation,omitempty"`
|
||||||
|
System string `json:"system_prompt"`
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// read читает записи истории из файла
|
||||||
|
func Read(historyPath string) ([]HistoryEntry, error) {
|
||||||
|
data, err := os.ReadFile(historyPath)
|
||||||
|
if err != nil {
|
||||||
|
// Если файл не существует, создаем пустой файл истории
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
emptyHistory := []HistoryEntry{}
|
||||||
|
if writeErr := Write(historyPath, emptyHistory); writeErr != nil {
|
||||||
|
return nil, fmt.Errorf("не удалось создать файл истории: %v", writeErr)
|
||||||
|
}
|
||||||
|
return emptyHistory, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(data) == 0 {
|
||||||
|
return []HistoryEntry{}, nil
|
||||||
|
}
|
||||||
|
var items []HistoryEntry
|
||||||
|
if err := json.Unmarshal(data, &items); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// write записывает записи истории в файл
|
||||||
|
func Write(historyPath string, entries []HistoryEntry) error {
|
||||||
|
for i := range entries {
|
||||||
|
entries[i].Index = i + 1
|
||||||
|
}
|
||||||
|
out, err := json.MarshalIndent(entries, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(filepath.Dir(historyPath), 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.WriteFile(historyPath, out, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteHistoryEntry удаляет запись из истории по индексу
|
||||||
|
func DeleteHistoryEntry(historyPath string, id int) error {
|
||||||
|
items, err := Read(historyPath)
|
||||||
|
if err != nil || len(items) == 0 {
|
||||||
|
return fmt.Errorf("история пуста или недоступна")
|
||||||
|
}
|
||||||
|
pos := -1
|
||||||
|
for i := range items {
|
||||||
|
if items[i].Index == id {
|
||||||
|
pos = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pos == -1 {
|
||||||
|
return fmt.Errorf("запись не найдена")
|
||||||
|
}
|
||||||
|
items = append(items[:pos], items[pos+1:]...)
|
||||||
|
return Write(historyPath, items)
|
||||||
|
}
|
||||||
103
serve/login.go
Normal file
103
serve/login.go
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
package serve
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||||
|
"github.com/direct-dev-ru/linux-command-gpt/serve/templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
// handleLoginPage обрабатывает страницу входа
|
||||||
|
func handleLoginPage(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Если пользователь уже авторизован, перенаправляем на главную
|
||||||
|
if isAuthenticated(r) {
|
||||||
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Генерируем CSRF токен
|
||||||
|
csrfManager := GetCSRFManager()
|
||||||
|
if csrfManager == nil {
|
||||||
|
http.Error(w, "CSRF manager not initialized", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Для неавторизованных пользователей используем сессионный ID
|
||||||
|
sessionID := getSessionID(r)
|
||||||
|
csrfToken, err := csrfManager.GenerateToken(sessionID)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Failed to generate CSRF token", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Устанавливаем CSRF токен в cookie
|
||||||
|
setCSRFCookie(w, csrfToken)
|
||||||
|
|
||||||
|
data := LoginPageData{
|
||||||
|
Title: "Авторизация - LCG",
|
||||||
|
Message: "",
|
||||||
|
Error: "",
|
||||||
|
CSRFToken: csrfToken,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := RenderLoginPage(w, data); err != nil {
|
||||||
|
http.Error(w, "Failed to render login page", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// isAuthenticated проверяет, авторизован ли пользователь
|
||||||
|
func isAuthenticated(r *http.Request) bool {
|
||||||
|
// Проверяем, требуется ли аутентификация
|
||||||
|
if !config.AppConfig.Server.RequireAuth {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем токен из cookie
|
||||||
|
token, err := getTokenFromCookie(r)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем валидность токена
|
||||||
|
_, err = validateJWTToken(token)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoginPageData представляет данные для страницы входа
|
||||||
|
type LoginPageData struct {
|
||||||
|
Title string
|
||||||
|
Message string
|
||||||
|
Error string
|
||||||
|
CSRFToken string
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderLoginPage рендерит страницу входа
|
||||||
|
func RenderLoginPage(w http.ResponseWriter, data LoginPageData) error {
|
||||||
|
tmpl, err := template.New("login").Parse(templates.LoginPageTemplate)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
return tmpl.Execute(w, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getSessionID получает или создает сессионный ID для пользователя
|
||||||
|
func getSessionID(r *http.Request) string {
|
||||||
|
// Пытаемся получить из cookie
|
||||||
|
if cookie, err := r.Cookie("session_id"); err == nil {
|
||||||
|
return cookie.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если нет cookie, генерируем новый ID на основе IP и User-Agent
|
||||||
|
ip := r.RemoteAddr
|
||||||
|
userAgent := r.Header.Get("User-Agent")
|
||||||
|
|
||||||
|
// Создаем простой хеш для сессии
|
||||||
|
hash := sha256.Sum256([]byte(ip + userAgent))
|
||||||
|
return hex.EncodeToString(hash[:])[:16]
|
||||||
|
}
|
||||||
112
serve/middleware.go
Normal file
112
serve/middleware.go
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
package serve
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuthMiddleware проверяет аутентификацию для всех запросов
|
||||||
|
func AuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Проверяем, требуется ли аутентификация
|
||||||
|
if !config.AppConfig.Server.RequireAuth {
|
||||||
|
next(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Исключаем страницу входа и API логина из проверки
|
||||||
|
if r.URL.Path == "/login" || r.URL.Path == "/api/login" || r.URL.Path == "/api/validate-token" {
|
||||||
|
next(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем аутентификацию
|
||||||
|
if !isAuthenticated(r) {
|
||||||
|
// Для API запросов возвращаем JSON ошибку
|
||||||
|
if isAPIRequest(r) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
w.Write([]byte(`{"success": false, "error": "Authentication required"}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Для веб-запросов перенаправляем на страницу входа
|
||||||
|
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Пользователь аутентифицирован, продолжаем
|
||||||
|
next(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CSRFMiddleware проверяет CSRF токены для POST/PUT/DELETE запросов
|
||||||
|
func CSRFMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Проверяем только изменяющие запросы
|
||||||
|
if r.Method == "GET" || r.Method == "HEAD" || r.Method == "OPTIONS" {
|
||||||
|
next(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Исключаем некоторые API endpoints
|
||||||
|
if r.URL.Path == "/api/login" || r.URL.Path == "/api/logout" {
|
||||||
|
next(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем CSRF токен из заголовка или формы
|
||||||
|
csrfToken := r.Header.Get("X-CSRF-Token")
|
||||||
|
if csrfToken == "" {
|
||||||
|
csrfToken = r.FormValue("csrf_token")
|
||||||
|
}
|
||||||
|
|
||||||
|
if csrfToken == "" {
|
||||||
|
// Для API запросов возвращаем JSON ошибку
|
||||||
|
if isAPIRequest(r) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusForbidden)
|
||||||
|
w.Write([]byte(`{"success": false, "error": "CSRF token required"}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Для веб-запросов возвращаем ошибку
|
||||||
|
http.Error(w, "CSRF token required", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем сессионный ID
|
||||||
|
sessionID := getSessionID(r)
|
||||||
|
|
||||||
|
// Проверяем CSRF токен
|
||||||
|
csrfManager := GetCSRFManager()
|
||||||
|
if csrfManager == nil || !csrfManager.ValidateToken(csrfToken, sessionID) {
|
||||||
|
// Для API запросов возвращаем JSON ошибку
|
||||||
|
if isAPIRequest(r) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusForbidden)
|
||||||
|
w.Write([]byte(`{"success": false, "error": "Invalid CSRF token"}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Для веб-запросов возвращаем ошибку
|
||||||
|
http.Error(w, "Invalid CSRF token", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// CSRF токен валиден, продолжаем
|
||||||
|
next(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// isAPIRequest проверяет, является ли запрос API запросом
|
||||||
|
func isAPIRequest(r *http.Request) bool {
|
||||||
|
path := r.URL.Path
|
||||||
|
return len(path) >= 4 && path[:4] == "/api"
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequireAuth обертка для requireAuth из auth.go
|
||||||
|
func RequireAuth(next http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return requireAuth(next)
|
||||||
|
}
|
||||||
531
serve/prompts.go
Normal file
531
serve/prompts.go
Normal file
@@ -0,0 +1,531 @@
|
|||||||
|
package serve
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"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 структура для промптов подробности
|
||||||
|
type VerbosePrompt struct {
|
||||||
|
Mode string
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
Content string
|
||||||
|
IsDefault bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlePromptsPage обрабатывает страницу управления промптами
|
||||||
|
func handlePromptsPage(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Получаем домашнюю директорию пользователя
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Ошибка получения домашней директории", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем менеджер промптов (использует конфигурацию из config.AppConfig.PromptFolder)
|
||||||
|
pm := gpt.NewPromptManager(homeDir)
|
||||||
|
|
||||||
|
// Получаем язык из параметра запроса, если не указан - берем из файла
|
||||||
|
lang := r.URL.Query().Get("lang")
|
||||||
|
if lang == "" {
|
||||||
|
lang = pm.GetCurrentLanguage()
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl := templates.PromptsPageTemplate
|
||||||
|
|
||||||
|
t, err := template.New("prompts").Parse(tmpl)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Ошибка шаблона", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем структуру с дополнительным полем IsDefault
|
||||||
|
type PromptWithDefault struct {
|
||||||
|
gpt.SystemPrompt
|
||||||
|
IsDefault bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем текущий язык из файла
|
||||||
|
currentLang := pm.GetCurrentLanguage()
|
||||||
|
|
||||||
|
// Если язык не указан в URL, используем язык из файла
|
||||||
|
if lang == "" {
|
||||||
|
lang = currentLang
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем системные промпты с учетом языка
|
||||||
|
systemPrompts := getSystemPromptsWithLang(pm.Prompts, lang)
|
||||||
|
|
||||||
|
var promptsWithDefault []PromptWithDefault
|
||||||
|
for _, prompt := range systemPrompts {
|
||||||
|
// Показываем только системные промпты (ID 1-5) на первой вкладке
|
||||||
|
if prompt.ID >= 1 && prompt.ID <= 5 {
|
||||||
|
// Проверяем, является ли промпт встроенным и неизмененным
|
||||||
|
isDefault := gpt.IsBuiltinPrompt(prompt)
|
||||||
|
promptsWithDefault = append(promptsWithDefault, PromptWithDefault{
|
||||||
|
SystemPrompt: prompt,
|
||||||
|
IsDefault: isDefault,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем промпты подробности из файла sys_prompts
|
||||||
|
verbosePrompts := getVerbosePromptsFromFile(pm.Prompts, lang)
|
||||||
|
|
||||||
|
data := struct {
|
||||||
|
Prompts []PromptWithDefault
|
||||||
|
VerbosePrompts []VerbosePrompt
|
||||||
|
Lang string
|
||||||
|
MaxSystemPromptLength int
|
||||||
|
MaxPromptNameLength int
|
||||||
|
MaxPromptDescLength int
|
||||||
|
BasePath string
|
||||||
|
AppName string
|
||||||
|
}{
|
||||||
|
Prompts: promptsWithDefault,
|
||||||
|
VerbosePrompts: verbosePrompts,
|
||||||
|
Lang: lang,
|
||||||
|
MaxSystemPromptLength: config.AppConfig.Validation.MaxSystemPromptLength,
|
||||||
|
MaxPromptNameLength: config.AppConfig.Validation.MaxPromptNameLength,
|
||||||
|
MaxPromptDescLength: config.AppConfig.Validation.MaxPromptDescLength,
|
||||||
|
BasePath: getBasePath(),
|
||||||
|
AppName: config.AppConfig.AppName,
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
t.Execute(w, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleAddPrompt обрабатывает добавление нового промпта
|
||||||
|
func handleAddPrompt(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "POST" {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем домашнюю директорию пользователя
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Ошибка получения домашней директории", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем менеджер промптов (использует конфигурацию из config.AppConfig.PromptFolder)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем промпт
|
||||||
|
if err := pm.AddPrompt(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("Промпт успешно добавлен"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleEditPrompt обрабатывает редактирование промпта
|
||||||
|
func handleEditPrompt(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "PUT" {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем ID из URL
|
||||||
|
idStr := strings.TrimPrefix(r.URL.Path, "/prompts/edit/")
|
||||||
|
id, err := strconv.Atoi(idStr)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Неверный ID промпта", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем домашнюю директорию пользователя
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Ошибка получения домашней директории", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем менеджер промптов (использует конфигурацию из config.AppConfig.PromptFolder)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем промпт
|
||||||
|
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("Промпт успешно обновлен"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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" {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем ID из URL
|
||||||
|
idStr := strings.TrimPrefix(r.URL.Path, "/prompts/delete/")
|
||||||
|
id, err := strconv.Atoi(idStr)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Неверный ID промпта", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем домашнюю директорию пользователя
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Ошибка получения домашней директории", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем менеджер промптов (использует конфигурацию из config.AppConfig.PromptFolder)
|
||||||
|
pm := gpt.NewPromptManager(homeDir)
|
||||||
|
|
||||||
|
// Удаляем промпт
|
||||||
|
if err := pm.DeletePrompt(id); err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("Ошибка удаления промпта: %v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte("Промпт успешно удален"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleRestorePrompt восстанавливает системный промпт к значению по умолчанию
|
||||||
|
func handleRestorePrompt(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "POST" {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем ID из URL
|
||||||
|
idStr := strings.TrimPrefix(r.URL.Path, "/prompts/restore/")
|
||||||
|
id, err := strconv.Atoi(idStr)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Invalid prompt ID", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем домашнюю директорию пользователя
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Ошибка получения домашней директории", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем менеджер промптов
|
||||||
|
pm := gpt.NewPromptManager(homeDir)
|
||||||
|
|
||||||
|
// Получаем текущий язык
|
||||||
|
currentLang := pm.GetCurrentLanguage()
|
||||||
|
|
||||||
|
// Получаем встроенный промпт для текущего языка
|
||||||
|
builtinPrompt := gpt.GetBuiltinPromptByIDAndLanguage(id, currentLang)
|
||||||
|
if builtinPrompt == nil {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
|
"success": false,
|
||||||
|
"error": "Промпт не найден в встроенных",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем промпт в списке
|
||||||
|
for i, prompt := range pm.Prompts {
|
||||||
|
if prompt.ID == id {
|
||||||
|
pm.Prompts[i] = *builtinPrompt
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохраняем изменения
|
||||||
|
if err := pm.SaveAllPrompts(); err != nil {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
|
"success": false,
|
||||||
|
"error": "Ошибка сохранения: " + err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
|
"success": true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleRestoreVerbosePrompt восстанавливает verbose промпт к значению по умолчанию
|
||||||
|
func handleRestoreVerbosePrompt(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "POST" {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем режим из URL
|
||||||
|
mode := strings.TrimPrefix(r.URL.Path, "/prompts/restore-verbose/")
|
||||||
|
|
||||||
|
// Получаем домашнюю директорию пользователя
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Ошибка получения домашней директории", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем менеджер промптов
|
||||||
|
pm := gpt.NewPromptManager(homeDir)
|
||||||
|
|
||||||
|
// Получаем текущий язык
|
||||||
|
currentLang := pm.GetCurrentLanguage()
|
||||||
|
|
||||||
|
// Определяем ID по режиму
|
||||||
|
var id int
|
||||||
|
switch mode {
|
||||||
|
case "v":
|
||||||
|
id = 6
|
||||||
|
case "vv":
|
||||||
|
id = 7
|
||||||
|
case "vvv":
|
||||||
|
id = 8
|
||||||
|
default:
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
|
"success": false,
|
||||||
|
"error": "Неверный режим промпта",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем встроенный промпт для текущего языка
|
||||||
|
builtinPrompt := gpt.GetBuiltinPromptByIDAndLanguage(id, currentLang)
|
||||||
|
if builtinPrompt == nil {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
|
"success": false,
|
||||||
|
"error": "Промпт не найден в встроенных",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем промпт в списке
|
||||||
|
for i, prompt := range pm.Prompts {
|
||||||
|
if prompt.ID == id {
|
||||||
|
pm.Prompts[i] = *builtinPrompt
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохраняем изменения
|
||||||
|
if err := pm.SaveAllPrompts(); err != nil {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
|
"success": false,
|
||||||
|
"error": "Ошибка сохранения: " + err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
|
"success": true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleSaveLang обрабатывает сохранение промптов при переключении языка
|
||||||
|
func handleSaveLang(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "POST" {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем домашнюю директорию пользователя
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Ошибка получения домашней директории", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем менеджер промптов
|
||||||
|
pm := gpt.NewPromptManager(homeDir)
|
||||||
|
|
||||||
|
// Парсим JSON данные
|
||||||
|
var langData struct {
|
||||||
|
Lang string `json:"lang"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&langData); err != nil {
|
||||||
|
http.Error(w, "Ошибка парсинга JSON", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Устанавливаем язык файла
|
||||||
|
pm.SetLanguage(langData.Lang)
|
||||||
|
|
||||||
|
// Переводим только встроенные промпты (по ID), а пользовательские оставляем как есть
|
||||||
|
var translatedPrompts []gpt.SystemPrompt
|
||||||
|
for _, p := range pm.Prompts {
|
||||||
|
// Проверяем, является ли промпт встроенным по ID (1-8)
|
||||||
|
if pm.IsDefaultPromptByID(p) {
|
||||||
|
// System (1-5) и Verbose (6-8)
|
||||||
|
if p.ID >= 1 && p.ID <= 5 {
|
||||||
|
translatedPrompts = append(translatedPrompts, translateSystemPrompt(p, langData.Lang))
|
||||||
|
} else if p.ID >= 6 && p.ID <= 8 {
|
||||||
|
translatedPrompts = append(translatedPrompts, translateVerbosePrompt(p, langData.Lang))
|
||||||
|
} else {
|
||||||
|
translatedPrompts = append(translatedPrompts, p)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Пользовательские промпты (ID > 8) не трогаем
|
||||||
|
translatedPrompts = append(translatedPrompts, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем в pm и сохраняем
|
||||||
|
pm.Prompts = translatedPrompts
|
||||||
|
if err := pm.SaveAllPrompts(); err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("Ошибка сохранения: %v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte("Промпты сохранены"))
|
||||||
|
}
|
||||||
147
serve/prompts_helpers.go
Normal file
147
serve/prompts_helpers.go
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
package serve
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/direct-dev-ru/linux-command-gpt/gpt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// getVerbosePromptsFromFile возвращает промпты подробности из файла sys_prompts
|
||||||
|
func getVerbosePromptsFromFile(prompts []gpt.SystemPrompt, lang string) []VerbosePrompt {
|
||||||
|
var verbosePrompts []VerbosePrompt
|
||||||
|
|
||||||
|
// Ищем промпты подробности в загруженных промптах (ID 6, 7, 8)
|
||||||
|
for _, prompt := range prompts {
|
||||||
|
if prompt.ID >= 6 && prompt.ID <= 8 {
|
||||||
|
// Определяем режим по ID
|
||||||
|
var mode string
|
||||||
|
switch prompt.ID {
|
||||||
|
case 6:
|
||||||
|
mode = "v"
|
||||||
|
case 7:
|
||||||
|
mode = "vv"
|
||||||
|
case 8:
|
||||||
|
mode = "vvv"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Переводим на нужный язык если необходимо
|
||||||
|
translatedPrompt := translateVerbosePrompt(prompt, lang)
|
||||||
|
|
||||||
|
verbosePrompts = append(verbosePrompts, VerbosePrompt{
|
||||||
|
Mode: mode,
|
||||||
|
Name: translatedPrompt.Name,
|
||||||
|
Description: translatedPrompt.Description,
|
||||||
|
Content: translatedPrompt.Content,
|
||||||
|
IsDefault: gpt.IsBuiltinPrompt(translatedPrompt), // Проверяем, является ли промпт встроенным
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если промпты подробности не найдены в файле, используем встроенные
|
||||||
|
if len(verbosePrompts) == 0 {
|
||||||
|
return getVerbosePrompts(lang)
|
||||||
|
}
|
||||||
|
|
||||||
|
return verbosePrompts
|
||||||
|
}
|
||||||
|
|
||||||
|
// translateVerbosePrompt переводит промпт подробности на указанный язык
|
||||||
|
func translateVerbosePrompt(prompt gpt.SystemPrompt, lang string) gpt.SystemPrompt {
|
||||||
|
// Получаем встроенный промпт для указанного языка из YAML
|
||||||
|
if builtinPrompt := gpt.GetBuiltinPromptByIDAndLanguage(prompt.ID, lang); builtinPrompt != nil {
|
||||||
|
return *builtinPrompt
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если перевод не найден, возвращаем оригинал
|
||||||
|
return prompt
|
||||||
|
}
|
||||||
|
|
||||||
|
// getVerbosePrompts возвращает промпты для режимов v/vv/vvv (fallback)
|
||||||
|
func getVerbosePrompts(lang string) []VerbosePrompt {
|
||||||
|
// Английские версии (по умолчанию)
|
||||||
|
enPrompts := []VerbosePrompt{
|
||||||
|
{
|
||||||
|
Mode: "v",
|
||||||
|
Name: "Verbose Mode",
|
||||||
|
Description: "Detailed explanation of the command",
|
||||||
|
Content: "Provide a brief explanation of what this Linux command does, including what each flag and option means, and give examples of usage.",
|
||||||
|
IsDefault: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Mode: "vv",
|
||||||
|
Name: "Very Verbose Mode",
|
||||||
|
Description: "Comprehensive explanation with alternatives",
|
||||||
|
Content: "Provide a comprehensive explanation of this Linux command, including detailed descriptions of all flags and options, alternative approaches, common use cases, and potential pitfalls to avoid.",
|
||||||
|
IsDefault: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Mode: "vvv",
|
||||||
|
Name: "Maximum Verbose Mode",
|
||||||
|
Description: "Complete guide with examples and best practices",
|
||||||
|
Content: "Provide a complete guide for this Linux command, including detailed explanations of all options, multiple examples with different scenarios, alternative commands that achieve similar results, best practices, troubleshooting tips, and related commands that work well together.",
|
||||||
|
IsDefault: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Русские версии
|
||||||
|
ruPrompts := []VerbosePrompt{
|
||||||
|
{
|
||||||
|
Mode: "v",
|
||||||
|
Name: "Подробный режим",
|
||||||
|
Description: "Подробное объяснение команды",
|
||||||
|
Content: "Предоставь краткое объяснение того, что делает эта Linux команда, включая значение каждого флага и опции, и приведи примеры использования.",
|
||||||
|
IsDefault: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Mode: "vv",
|
||||||
|
Name: "Очень подробный режим",
|
||||||
|
Description: "Исчерпывающее объяснение с альтернативами",
|
||||||
|
Content: "Предоставь исчерпывающее объяснение этой Linux команды, включая подробные описания всех флагов и опций, альтернативные подходы, распространенные случаи использования и потенциальные подводные камни, которых следует избегать.",
|
||||||
|
IsDefault: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Mode: "vvv",
|
||||||
|
Name: "Максимально подробный режим",
|
||||||
|
Description: "Полное руководство с примерами и лучшими практиками",
|
||||||
|
Content: "Предоставь полное руководство по этой Linux команде, включая подробные объяснения всех опций, множественные примеры с различными сценариями, альтернативные команды, которые дают аналогичные результаты, лучшие практики, советы по устранению неполадок и связанные команды, которые хорошо работают вместе.",
|
||||||
|
IsDefault: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if lang == "ru" {
|
||||||
|
return ruPrompts
|
||||||
|
}
|
||||||
|
return enPrompts
|
||||||
|
}
|
||||||
|
|
||||||
|
// getSystemPromptsWithLang возвращает системные промпты с учетом языка
|
||||||
|
func getSystemPromptsWithLang(prompts []gpt.SystemPrompt, lang string) []gpt.SystemPrompt {
|
||||||
|
// Если язык английский, возвращаем оригинальные промпты
|
||||||
|
if lang == "en" {
|
||||||
|
return prompts
|
||||||
|
}
|
||||||
|
|
||||||
|
// Для русского языка переводим только встроенные промпты
|
||||||
|
var translatedPrompts []gpt.SystemPrompt
|
||||||
|
for _, prompt := range prompts {
|
||||||
|
// Проверяем, является ли это встроенным промптом
|
||||||
|
if gpt.IsBuiltinPrompt(prompt) {
|
||||||
|
// Переводим встроенные промпты на русский
|
||||||
|
translated := translateSystemPrompt(prompt, lang)
|
||||||
|
translatedPrompts = append(translatedPrompts, translated)
|
||||||
|
} else {
|
||||||
|
translatedPrompts = append(translatedPrompts, prompt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return translatedPrompts
|
||||||
|
}
|
||||||
|
|
||||||
|
// translateSystemPrompt переводит системный промпт на указанный язык
|
||||||
|
func translateSystemPrompt(prompt gpt.SystemPrompt, lang string) gpt.SystemPrompt {
|
||||||
|
// Получаем встроенный промпт для указанного языка из YAML
|
||||||
|
if builtinPrompt := gpt.GetBuiltinPromptByIDAndLanguage(prompt.ID, lang); builtinPrompt != nil {
|
||||||
|
return *builtinPrompt
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если перевод не найден, возвращаем оригинал
|
||||||
|
return prompt
|
||||||
|
}
|
||||||
253
serve/results.go
Normal file
253
serve/results.go
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
package serve
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
|
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||||
|
"github.com/direct-dev-ru/linux-command-gpt/serve/templates"
|
||||||
|
"github.com/russross/blackfriday/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// generateAbbreviation создает аббревиатуру из первых букв слов в названии приложения
|
||||||
|
func generateAbbreviation(appName string) string {
|
||||||
|
words := strings.Fields(appName)
|
||||||
|
var abbreviation strings.Builder
|
||||||
|
|
||||||
|
for _, word := range words {
|
||||||
|
if len(word) > 0 {
|
||||||
|
// Берем первую букву слова, если это буква
|
||||||
|
firstRune := []rune(word)[0]
|
||||||
|
if unicode.IsLetter(firstRune) {
|
||||||
|
abbreviation.WriteRune(unicode.ToUpper(firstRune))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result := abbreviation.String()
|
||||||
|
if result == "" {
|
||||||
|
return "LCG" // Fallback если не удалось сгенерировать аббревиатуру
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileInfo содержит информацию о файле
|
||||||
|
type FileInfo struct {
|
||||||
|
Name string
|
||||||
|
Size string
|
||||||
|
ModTime string
|
||||||
|
Preview template.HTML
|
||||||
|
Content string // Полное содержимое для поиска
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleResultsPage обрабатывает главную страницу со списком файлов
|
||||||
|
func handleResultsPage(w http.ResponseWriter, r *http.Request) {
|
||||||
|
files, err := getResultFiles()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("Ошибка чтения папки: %v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl := templates.ResultsPageTemplate
|
||||||
|
|
||||||
|
t, err := template.New("results").Parse(tmpl)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Ошибка шаблона", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Подсчитываем статистику
|
||||||
|
recentCount := 0
|
||||||
|
weekAgo := time.Now().AddDate(0, 0, -7)
|
||||||
|
for _, file := range files {
|
||||||
|
// Парсим время из строки для сравнения
|
||||||
|
if modTime, err := time.Parse("02.01.2006 15:04", file.ModTime); err == nil {
|
||||||
|
if modTime.After(weekAgo) {
|
||||||
|
recentCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data := struct {
|
||||||
|
Files []FileInfo
|
||||||
|
TotalFiles int
|
||||||
|
RecentFiles int
|
||||||
|
BasePath string
|
||||||
|
AppName string
|
||||||
|
AppAbbreviation string
|
||||||
|
}{
|
||||||
|
Files: files,
|
||||||
|
TotalFiles: len(files),
|
||||||
|
RecentFiles: recentCount,
|
||||||
|
BasePath: getBasePath(),
|
||||||
|
AppName: config.AppConfig.AppName,
|
||||||
|
AppAbbreviation: generateAbbreviation(config.AppConfig.AppName),
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
t.Execute(w, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getResultFiles возвращает список файлов из папки результатов
|
||||||
|
func getResultFiles() ([]FileInfo, error) {
|
||||||
|
entries, err := os.ReadDir(config.AppConfig.ResultFolder)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var files []FileInfo
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".md") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := entry.Info()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Читаем превью файла (первые 200 символов) как обычный текст
|
||||||
|
preview := ""
|
||||||
|
fullContent := ""
|
||||||
|
if content, err := os.ReadFile(filepath.Join(config.AppConfig.ResultFolder, entry.Name())); err == nil {
|
||||||
|
// Сохраняем полное содержимое для поиска
|
||||||
|
fullContent = string(content)
|
||||||
|
|
||||||
|
// Берем первые 200 символов как превью
|
||||||
|
preview = string(content)
|
||||||
|
// Очищаем от лишних пробелов и переносов
|
||||||
|
preview = strings.ReplaceAll(preview, "\n", " ")
|
||||||
|
preview = strings.ReplaceAll(preview, "\r", "")
|
||||||
|
preview = strings.ReplaceAll(preview, " ", " ")
|
||||||
|
preview = strings.TrimSpace(preview)
|
||||||
|
|
||||||
|
if len(preview) > 200 {
|
||||||
|
preview = preview[:200] + "..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
files = append(files, FileInfo{
|
||||||
|
Name: entry.Name(),
|
||||||
|
Size: formatFileSize(info.Size()),
|
||||||
|
ModTime: info.ModTime().Format("02.01.2006 15:04"),
|
||||||
|
Preview: template.HTML(preview),
|
||||||
|
Content: fullContent,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сортируем по времени изменения (новые сверху)
|
||||||
|
for i := 0; i < len(files)-1; i++ {
|
||||||
|
for j := i + 1; j < len(files); j++ {
|
||||||
|
if files[i].ModTime < files[j].ModTime {
|
||||||
|
files[i], files[j] = files[j], files[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return files, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatFileSize форматирует размер файла в читаемый вид
|
||||||
|
func formatFileSize(size int64) string {
|
||||||
|
const unit = 1024
|
||||||
|
if size < unit {
|
||||||
|
return fmt.Sprintf("%d B", size)
|
||||||
|
}
|
||||||
|
div, exp := int64(unit), 0
|
||||||
|
for n := size / unit; n >= unit; n /= unit {
|
||||||
|
div *= unit
|
||||||
|
exp++
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%.1f %cB", float64(size)/float64(div), "KMGTPE"[exp])
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleFileView обрабатывает просмотр конкретного файла
|
||||||
|
func handleFileView(w http.ResponseWriter, r *http.Request) {
|
||||||
|
filename := strings.TrimPrefix(r.URL.Path, "/file/")
|
||||||
|
if filename == "" {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, что файл существует и находится в папке результатов
|
||||||
|
filePath := filepath.Join(config.AppConfig.ResultFolder, filename)
|
||||||
|
if !strings.HasPrefix(filePath, config.AppConfig.ResultFolder) {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := os.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Конвертируем Markdown в HTML
|
||||||
|
htmlContent := blackfriday.Run(content)
|
||||||
|
|
||||||
|
// Создаем данные для шаблона
|
||||||
|
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")
|
||||||
|
t.Execute(w, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleDeleteFile обрабатывает удаление файла
|
||||||
|
func handleDeleteFile(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Проверяем метод запроса
|
||||||
|
if r.Method != "DELETE" {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := strings.TrimPrefix(r.URL.Path, "/delete/")
|
||||||
|
if filename == "" {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, что файл существует и находится в папке результатов
|
||||||
|
filePath := filepath.Join(config.AppConfig.ResultFolder, filename)
|
||||||
|
if !strings.HasPrefix(filePath, config.AppConfig.ResultFolder) {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, что файл существует
|
||||||
|
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Удаляем файл
|
||||||
|
err := os.Remove(filePath)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("Ошибка удаления файла: %v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Возвращаем успешный ответ
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte("Файл успешно удален"))
|
||||||
|
}
|
||||||
254
serve/serve.go
Normal file
254
serve/serve.go
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
package serve
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||||
|
"github.com/direct-dev-ru/linux-command-gpt/ssl"
|
||||||
|
)
|
||||||
|
|
||||||
|
// makePath создает путь с учетом BasePath
|
||||||
|
func makePath(path string) string {
|
||||||
|
basePath := config.AppConfig.Server.BasePath
|
||||||
|
if basePath == "" || basePath == "/" {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
// Убираем слэш в конце basePath если есть
|
||||||
|
basePath = strings.TrimSuffix(basePath, "/")
|
||||||
|
|
||||||
|
// Убираем слэш в начале path если есть
|
||||||
|
path = strings.TrimPrefix(path, "/")
|
||||||
|
|
||||||
|
// Если path пустой, возвращаем basePath с слэшем в конце
|
||||||
|
if path == "" {
|
||||||
|
return basePath + "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
return basePath + "/" + path
|
||||||
|
}
|
||||||
|
|
||||||
|
// getBasePath возвращает BasePath для использования в шаблонах
|
||||||
|
func getBasePath() string {
|
||||||
|
basePath := config.AppConfig.Server.BasePath
|
||||||
|
if basePath == "" || basePath == "/" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.TrimSuffix(basePath, "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartResultServer запускает HTTP/HTTPS сервер для просмотра сохраненных результатов
|
||||||
|
func StartResultServer(host, port string) error {
|
||||||
|
// Инициализируем CSRF менеджер
|
||||||
|
if err := InitCSRFManager(); err != nil {
|
||||||
|
return fmt.Errorf("failed to initialize CSRF manager: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
addr := fmt.Sprintf("%s:%s", host, port)
|
||||||
|
|
||||||
|
// Проверяем, нужно ли использовать HTTPS
|
||||||
|
useHTTPS := ssl.ShouldUseHTTPS(host)
|
||||||
|
|
||||||
|
if useHTTPS {
|
||||||
|
// Регистрируем HTTPS маршруты (включая редирект)
|
||||||
|
registerHTTPSRoutes()
|
||||||
|
|
||||||
|
// Создаем директорию для SSL сертификатов
|
||||||
|
sslDir := fmt.Sprintf("%s/server/ssl", config.AppConfig.Server.ConfigFolder)
|
||||||
|
if err := os.MkdirAll(sslDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create SSL directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Загружаем или генерируем SSL сертификат
|
||||||
|
cert, err := ssl.LoadOrGenerateCert(host)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load/generate SSL certificate: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Настраиваем TLS
|
||||||
|
tlsConfig := &tls.Config{
|
||||||
|
Certificates: []tls.Certificate{*cert},
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
MaxVersion: tls.VersionTLS13,
|
||||||
|
// Отключаем проверку клиентских сертификатов
|
||||||
|
ClientAuth: tls.NoClientCert,
|
||||||
|
// Добавляем логирование для отладки
|
||||||
|
GetCertificate: func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||||
|
if config.AppConfig.MainFlags.Debug {
|
||||||
|
fmt.Printf("🔍 TLS запрос от %s (SNI: %s)\n", clientHello.Conn.RemoteAddr(), clientHello.ServerName)
|
||||||
|
}
|
||||||
|
return cert, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем HTTPS сервер
|
||||||
|
server := &http.Server{
|
||||||
|
Addr: addr,
|
||||||
|
TLSConfig: tlsConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("🔒 Сервер запущен на https://%s (SSL включен)\n", addr)
|
||||||
|
fmt.Println("Нажмите Ctrl+C для остановки")
|
||||||
|
|
||||||
|
// Тестовое логирование для проверки debug флага
|
||||||
|
if config.AppConfig.MainFlags.Debug {
|
||||||
|
fmt.Printf("🔍 DEBUG РЕЖИМ ВКЛЮЧЕН - веб-операции будут логироваться\n")
|
||||||
|
} else {
|
||||||
|
fmt.Printf("🔍 DEBUG РЕЖИМ ОТКЛЮЧЕН - веб-операции не будут логироваться\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return server.ListenAndServeTLS("", "")
|
||||||
|
} else {
|
||||||
|
// Регистрируем обычные маршруты для HTTP
|
||||||
|
registerRoutes()
|
||||||
|
|
||||||
|
fmt.Printf("🌐 Сервер запущен на http://%s (HTTP режим)\n", addr)
|
||||||
|
fmt.Println("Нажмите Ctrl+C для остановки")
|
||||||
|
|
||||||
|
// Тестовое логирование для проверки debug флага
|
||||||
|
if config.AppConfig.MainFlags.Debug {
|
||||||
|
fmt.Printf("🔍 DEBUG РЕЖИМ ВКЛЮЧЕН - веб-операции будут логироваться\n")
|
||||||
|
} else {
|
||||||
|
fmt.Printf("🔍 DEBUG РЕЖИМ ОТКЛЮЧЕН - веб-операции не будут логироваться\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.ListenAndServe(addr, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleHTTPSRedirect обрабатывает редирект с HTTP на HTTPS
|
||||||
|
func handleHTTPSRedirect(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Определяем протокол и хост
|
||||||
|
host := r.Host
|
||||||
|
if host == "" {
|
||||||
|
host = r.Header.Get("Host")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Редиректим на HTTPS
|
||||||
|
httpsURL := fmt.Sprintf("https://%s%s", host, r.RequestURI)
|
||||||
|
http.Redirect(w, r, httpsURL, http.StatusMovedPermanently)
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerHTTPSRoutes регистрирует маршруты для HTTPS сервера
|
||||||
|
func registerHTTPSRoutes() {
|
||||||
|
// Регистрируем все маршруты кроме главной страницы
|
||||||
|
registerRoutesExceptHome()
|
||||||
|
|
||||||
|
// Регистрируем главную страницу с проверкой HTTPS
|
||||||
|
http.HandleFunc(makePath("/"), func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Проверяем, пришел ли запрос по HTTP (не HTTPS)
|
||||||
|
if r.TLS == nil {
|
||||||
|
handleHTTPSRedirect(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Если уже HTTPS, обрабатываем как обычно
|
||||||
|
AuthMiddleware(handleResultsPage)(w, r)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Регистрируем главную страницу без слэша в конце для BasePath
|
||||||
|
basePath := config.AppConfig.Server.BasePath
|
||||||
|
if basePath != "" && basePath != "/" {
|
||||||
|
basePath = strings.TrimSuffix(basePath, "/")
|
||||||
|
http.HandleFunc(basePath, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Проверяем, пришел ли запрос по HTTP (не HTTPS)
|
||||||
|
if r.TLS == nil {
|
||||||
|
handleHTTPSRedirect(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Если уже HTTPS, обрабатываем как обычно
|
||||||
|
AuthMiddleware(handleResultsPage)(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerRoutesExceptHome регистрирует все маршруты кроме главной страницы
|
||||||
|
func registerRoutesExceptHome() {
|
||||||
|
// Страница входа (без аутентификации)
|
||||||
|
http.HandleFunc(makePath("/login"), handleLoginPage)
|
||||||
|
|
||||||
|
// API для аутентификации (без аутентификации)
|
||||||
|
http.HandleFunc(makePath("/api/login"), handleLogin)
|
||||||
|
http.HandleFunc(makePath("/api/logout"), handleLogout)
|
||||||
|
http.HandleFunc(makePath("/api/validate-token"), handleValidateToken)
|
||||||
|
|
||||||
|
// Файлы
|
||||||
|
http.HandleFunc(makePath("/file/"), AuthMiddleware(handleFileView))
|
||||||
|
http.HandleFunc(makePath("/delete/"), AuthMiddleware(handleDeleteFile))
|
||||||
|
|
||||||
|
// История запросов
|
||||||
|
http.HandleFunc(makePath("/history"), AuthMiddleware(handleHistoryPage))
|
||||||
|
http.HandleFunc(makePath("/history/view/"), AuthMiddleware(handleHistoryView))
|
||||||
|
http.HandleFunc(makePath("/history/delete/"), AuthMiddleware(handleDeleteHistoryEntry))
|
||||||
|
http.HandleFunc(makePath("/history/clear"), AuthMiddleware(handleClearHistory))
|
||||||
|
|
||||||
|
// Управление промптами
|
||||||
|
http.HandleFunc(makePath("/prompts"), AuthMiddleware(handlePromptsPage))
|
||||||
|
http.HandleFunc(makePath("/prompts/add"), AuthMiddleware(handleAddPrompt))
|
||||||
|
http.HandleFunc(makePath("/prompts/edit/"), AuthMiddleware(handleEditPrompt))
|
||||||
|
http.HandleFunc(makePath("/prompts/edit-verbose/"), AuthMiddleware(handleEditVerbosePrompt))
|
||||||
|
http.HandleFunc(makePath("/prompts/delete/"), AuthMiddleware(handleDeletePrompt))
|
||||||
|
http.HandleFunc(makePath("/prompts/restore/"), AuthMiddleware(handleRestorePrompt))
|
||||||
|
http.HandleFunc(makePath("/prompts/restore-verbose/"), AuthMiddleware(handleRestoreVerbosePrompt))
|
||||||
|
http.HandleFunc(makePath("/prompts/save-lang"), AuthMiddleware(handleSaveLang))
|
||||||
|
|
||||||
|
// Веб-страница для выполнения запросов
|
||||||
|
http.HandleFunc(makePath("/run"), AuthMiddleware(CSRFMiddleware(handleExecutePage)))
|
||||||
|
|
||||||
|
// API для выполнения запросов
|
||||||
|
http.HandleFunc(makePath("/api/execute"), AuthMiddleware(CSRFMiddleware(handleExecute)))
|
||||||
|
// API для сохранения результатов и истории
|
||||||
|
http.HandleFunc(makePath("/api/save-result"), AuthMiddleware(CSRFMiddleware(handleSaveResult)))
|
||||||
|
http.HandleFunc(makePath("/api/add-to-history"), AuthMiddleware(CSRFMiddleware(handleAddToHistory)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerRoutes регистрирует все маршруты сервера
|
||||||
|
func registerRoutes() {
|
||||||
|
// Страница входа (без аутентификации)
|
||||||
|
http.HandleFunc(makePath("/login"), handleLoginPage)
|
||||||
|
|
||||||
|
// API для аутентификации (без аутентификации)
|
||||||
|
http.HandleFunc(makePath("/api/login"), handleLogin)
|
||||||
|
http.HandleFunc(makePath("/api/logout"), handleLogout)
|
||||||
|
http.HandleFunc(makePath("/api/validate-token"), handleValidateToken)
|
||||||
|
|
||||||
|
// Главная страница и файлы
|
||||||
|
http.HandleFunc(makePath("/"), AuthMiddleware(handleResultsPage))
|
||||||
|
http.HandleFunc(makePath("/file/"), AuthMiddleware(handleFileView))
|
||||||
|
http.HandleFunc(makePath("/delete/"), AuthMiddleware(handleDeleteFile))
|
||||||
|
|
||||||
|
// История запросов
|
||||||
|
http.HandleFunc(makePath("/history"), AuthMiddleware(handleHistoryPage))
|
||||||
|
http.HandleFunc(makePath("/history/view/"), AuthMiddleware(handleHistoryView))
|
||||||
|
http.HandleFunc(makePath("/history/delete/"), AuthMiddleware(handleDeleteHistoryEntry))
|
||||||
|
http.HandleFunc(makePath("/history/clear"), AuthMiddleware(handleClearHistory))
|
||||||
|
|
||||||
|
// Управление промптами
|
||||||
|
http.HandleFunc(makePath("/prompts"), AuthMiddleware(handlePromptsPage))
|
||||||
|
http.HandleFunc(makePath("/prompts/add"), AuthMiddleware(handleAddPrompt))
|
||||||
|
http.HandleFunc(makePath("/prompts/edit/"), AuthMiddleware(handleEditPrompt))
|
||||||
|
http.HandleFunc(makePath("/prompts/edit-verbose/"), AuthMiddleware(handleEditVerbosePrompt))
|
||||||
|
http.HandleFunc(makePath("/prompts/delete/"), AuthMiddleware(handleDeletePrompt))
|
||||||
|
http.HandleFunc(makePath("/prompts/restore/"), AuthMiddleware(handleRestorePrompt))
|
||||||
|
http.HandleFunc(makePath("/prompts/restore-verbose/"), AuthMiddleware(handleRestoreVerbosePrompt))
|
||||||
|
http.HandleFunc(makePath("/prompts/save-lang"), AuthMiddleware(handleSaveLang))
|
||||||
|
|
||||||
|
// Веб-страница для выполнения запросов
|
||||||
|
http.HandleFunc(makePath("/run"), AuthMiddleware(CSRFMiddleware(handleExecutePage)))
|
||||||
|
|
||||||
|
// API для выполнения запросов
|
||||||
|
http.HandleFunc(makePath("/api/execute"), AuthMiddleware(CSRFMiddleware(handleExecute)))
|
||||||
|
// API для сохранения результатов и истории
|
||||||
|
http.HandleFunc(makePath("/api/save-result"), AuthMiddleware(CSRFMiddleware(handleSaveResult)))
|
||||||
|
http.HandleFunc(makePath("/api/add-to-history"), AuthMiddleware(CSRFMiddleware(handleAddToHistory)))
|
||||||
|
|
||||||
|
// Регистрируем главную страницу без слэша в конце для BasePath
|
||||||
|
basePath := config.AppConfig.Server.BasePath
|
||||||
|
if basePath != "" && basePath != "/" {
|
||||||
|
basePath = strings.TrimSuffix(basePath, "/")
|
||||||
|
http.HandleFunc(basePath, AuthMiddleware(handleResultsPage))
|
||||||
|
}
|
||||||
|
}
|
||||||
608
serve/templates/execute.css.go
Normal file
608
serve/templates/execute.css.go
Normal file
@@ -0,0 +1,608 @@
|
|||||||
|
package templates
|
||||||
|
|
||||||
|
import "html/template"
|
||||||
|
|
||||||
|
// ExecutePageCSSTemplate - CSS стили для страницы выполнения запросов
|
||||||
|
var ExecutePageCSSTemplate = template.Must(template.New("execute_css").Parse(`
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
/* Динамичный плавный градиент (современный стиль) */
|
||||||
|
background: linear-gradient(135deg, #5b86e5, #36d1dc, #4a7c59, #764ba2);
|
||||||
|
background-size: 400% 400%;
|
||||||
|
animation: gradientShift 18s ease infinite;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Анимация плавного перелива фона */
|
||||||
|
@keyframes gradientShift {
|
||||||
|
0% { background-position: 0% 50%; }
|
||||||
|
50% { background-position: 100% 50%; }
|
||||||
|
100% { background-position: 0% 50%; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Учитываем системные настройки доступности */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
body { animation: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Улучшения для touch-устройств */
|
||||||
|
.nav-btn, .submit-btn, .reset-btn, .verbose-btn, .action-btn {
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
touch-action: manipulation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Оптимизация производительности */
|
||||||
|
.container {
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Улучшение читаемости на мобильных */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.command-result code {
|
||||||
|
font-size: 0.9em;
|
||||||
|
padding: 1px 4px;
|
||||||
|
}
|
||||||
|
.command-result pre {
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
.explanation-content {
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
background: linear-gradient(135deg, #2d5016 0%, #4a7c59 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.header h1 {
|
||||||
|
font-size: 2.5em;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.header p {
|
||||||
|
opacity: 0.9;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-info {
|
||||||
|
margin: 5px 0 0 0 !important;
|
||||||
|
opacity: 0.7 !important;
|
||||||
|
font-size: 0.9em !important;
|
||||||
|
font-style: italic;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
.nav-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.nav-btn {
|
||||||
|
background: #3498db;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1em;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: background 0.3s ease;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.nav-btn:hover {
|
||||||
|
background: #2980b9;
|
||||||
|
}
|
||||||
|
.form-section {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 25px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2d5016;
|
||||||
|
}
|
||||||
|
.form-group select,
|
||||||
|
.form-group textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 16px;
|
||||||
|
transition: border-color 0.3s ease;
|
||||||
|
}
|
||||||
|
.form-group select:focus,
|
||||||
|
.form-group textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #2d5016;
|
||||||
|
box-shadow: 0 0 0 3px rgba(45, 80, 22, 0.1);
|
||||||
|
}
|
||||||
|
.form-group textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 120px;
|
||||||
|
}
|
||||||
|
.submit-btn {
|
||||||
|
background: linear-gradient(135deg, #2d5016 0%, #4a7c59 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 15px 30px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.submit-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 10px 20px rgba(45, 80, 22, 0.3);
|
||||||
|
}
|
||||||
|
.submit-btn:disabled {
|
||||||
|
background: #6c757d;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.form-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.reset-btn {
|
||||||
|
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 15px 30px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.reset-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 10px 20px rgba(231, 76, 60, 0.3);
|
||||||
|
}
|
||||||
|
.reset-btn:disabled {
|
||||||
|
background: #6c757d;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.result-section {
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
.command-result {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 4px solid #2d5016;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.command-result h3 {
|
||||||
|
color: #2d5016;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
/* Заголовки внутри результата команды */
|
||||||
|
.command-result h1,
|
||||||
|
.command-result h2,
|
||||||
|
.command-result h3,
|
||||||
|
.command-result h4,
|
||||||
|
.command-result h5,
|
||||||
|
.command-result h6 {
|
||||||
|
margin-top: 18px; /* отделяем сверху */
|
||||||
|
margin-bottom: 10px;/* и немного снизу */
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
/* Ритм текста внутри markdown-блока команды */
|
||||||
|
.command-result .command-md { line-height: 1.7; }
|
||||||
|
.command-result p { margin: 10px 0 14px; line-height: 1.7; }
|
||||||
|
.command-result ul,
|
||||||
|
.command-result ol { margin: 10px 0 14px 24px; line-height: 1.7; }
|
||||||
|
.command-result li { margin: 6px 0; }
|
||||||
|
.command-result hr { margin: 18px 0; border: 0; border-top: 1px solid #e1e5e9; }
|
||||||
|
/* Подсветка code внутри результата команды */
|
||||||
|
.command-result code {
|
||||||
|
background: #e6f4ea; /* светло-зеленый фон */
|
||||||
|
color: #2e7d32; /* зеленый текст */
|
||||||
|
border: 1px solid #b7dfb9;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.98em; /* немного крупнее базового */
|
||||||
|
}
|
||||||
|
.command-result pre {
|
||||||
|
background: #eaf7ee; /* мягкий зеленоватый фон */
|
||||||
|
border-left: 4px solid #2e7d32; /* зеленая полоса слева */
|
||||||
|
padding: 14px;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 12px 0 16px; /* вертикальные отступы вокруг кода */
|
||||||
|
}
|
||||||
|
.command-result pre code {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #2e7d32;
|
||||||
|
font-size: 16px; /* увеличить размер шрифта в блоках */
|
||||||
|
}
|
||||||
|
.command-code {
|
||||||
|
background: #2d5016;
|
||||||
|
color: white;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-family: 'Monaco', 'Menlo', monospace;
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.result-meta {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
color: #6c757d;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.explanation-section {
|
||||||
|
background: #f0f8f0;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 4px solid #4a7c59;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.explanation-section h3 {
|
||||||
|
color: #2d5016;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
.explanation-content {
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.explanation-content h1,
|
||||||
|
.explanation-content h2,
|
||||||
|
.explanation-content h3,
|
||||||
|
.explanation-content h4,
|
||||||
|
.explanation-content h5,
|
||||||
|
.explanation-content h6 {
|
||||||
|
color: #2d5016;
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.explanation-content h1 {
|
||||||
|
border-bottom: 2px solid #2d5016;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
}
|
||||||
|
.explanation-content h2 {
|
||||||
|
border-bottom: 1px solid #4a7c59;
|
||||||
|
padding-bottom: 3px;
|
||||||
|
}
|
||||||
|
.explanation-content code {
|
||||||
|
background: #f0f8f0;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: 'Monaco', 'Menlo', monospace;
|
||||||
|
color: #2d5016;
|
||||||
|
border: 1px solid #a8e6cf;
|
||||||
|
}
|
||||||
|
.explanation-content pre {
|
||||||
|
background: #f0f8f0;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 4px solid #2d5016;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
.explanation-content pre code {
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
color: #2d5016;
|
||||||
|
}
|
||||||
|
.explanation-content blockquote {
|
||||||
|
border-left: 4px solid #4a7c59;
|
||||||
|
margin: 15px 0;
|
||||||
|
padding: 10px 20px;
|
||||||
|
background: #f0f8f0;
|
||||||
|
border-radius: 0 8px 8px 0;
|
||||||
|
}
|
||||||
|
.explanation-content ul,
|
||||||
|
.explanation-content ol {
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
.explanation-content li {
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
.explanation-content strong {
|
||||||
|
color: #2d5016;
|
||||||
|
}
|
||||||
|
.explanation-content em {
|
||||||
|
color: #4a7c59;
|
||||||
|
}
|
||||||
|
.verbose-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.verbose-btn {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
color: #495057;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.v-btn {
|
||||||
|
background: #e3f2fd;
|
||||||
|
border: 1px solid #bbdefb;
|
||||||
|
color: #1976d2;
|
||||||
|
}
|
||||||
|
.v-btn:hover {
|
||||||
|
background: #bbdefb;
|
||||||
|
border-color: #90caf9;
|
||||||
|
}
|
||||||
|
.v-btn:disabled {
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-color: #e0e0e0;
|
||||||
|
color: #9e9e9e;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.vv-btn {
|
||||||
|
background: #e1f5fe;
|
||||||
|
border: 1px solid #b3e5fc;
|
||||||
|
color: #0277bd;
|
||||||
|
}
|
||||||
|
.vv-btn:hover {
|
||||||
|
background: #b3e5fc;
|
||||||
|
border-color: #81d4fa;
|
||||||
|
}
|
||||||
|
.vv-btn:disabled {
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-color: #e0e0e0;
|
||||||
|
color: #9e9e9e;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.vvv-btn {
|
||||||
|
background: #e8eaf6;
|
||||||
|
border: 1px solid #c5cae9;
|
||||||
|
color: #3f51b5;
|
||||||
|
}
|
||||||
|
.vvv-btn:hover {
|
||||||
|
background: #c5cae9;
|
||||||
|
border-color: #9fa8da;
|
||||||
|
}
|
||||||
|
.vvv-btn:disabled {
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-color: #e0e0e0;
|
||||||
|
color: #9e9e9e;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 20px;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.action-btn {
|
||||||
|
background: #e8f5e8;
|
||||||
|
border: 1px solid #c8e6c9;
|
||||||
|
color: #2e7d32;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-size: 14px;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.action-btn:hover {
|
||||||
|
background: #c8e6c9;
|
||||||
|
border-color: #a5d6a7;
|
||||||
|
color: #1b5e20;
|
||||||
|
}
|
||||||
|
.error-message {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 4px solid #dc3545;
|
||||||
|
}
|
||||||
|
.error-message h3 {
|
||||||
|
color: #721c24;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.loading {
|
||||||
|
display: none;
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.loading.show {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.spinner {
|
||||||
|
border: 3px solid #f3f3f3;
|
||||||
|
border-top: 3px solid #2d5016;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin: 0 auto 10px;
|
||||||
|
}
|
||||||
|
.verbose-loading {
|
||||||
|
display: none;
|
||||||
|
text-align: center;
|
||||||
|
padding: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.verbose-loading.show {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.verbose-spinner {
|
||||||
|
border: 2px solid #f3f3f3;
|
||||||
|
border-top: 2px solid #1976d2;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin: 0 auto 5px;
|
||||||
|
}
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
.scroll-to-top {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background: #3498db;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
font-size: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
.scroll-to-top:hover {
|
||||||
|
background: #2980b9;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 12px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Мобильная оптимизация */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
body {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
margin: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.header h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.nav-buttons {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.nav-btn {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
.form-buttons {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.submit-btn, .reset-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 16px 20px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.verbose-buttons {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.verbose-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.action-buttons {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.action-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.command-result {
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
.command-code {
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 12px;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.explanation-section {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
.result-meta {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.scroll-to-top {
|
||||||
|
bottom: 15px;
|
||||||
|
right: 15px;
|
||||||
|
width: 45px;
|
||||||
|
height: 45px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Очень маленькие экраны */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.header h1 {
|
||||||
|
font-size: 1.8em;
|
||||||
|
}
|
||||||
|
.header p {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
.form-group textarea {
|
||||||
|
min-height: 100px;
|
||||||
|
font-size: 16px; /* Предотвращает зум на iOS */
|
||||||
|
}
|
||||||
|
.form-group select {
|
||||||
|
font-size: 16px; /* Предотвращает зум на iOS */
|
||||||
|
}
|
||||||
|
.command-result h3 {
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
.explanation-content h1,
|
||||||
|
.explanation-content h2,
|
||||||
|
.explanation-content h3 {
|
||||||
|
font-size: 1.3em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`))
|
||||||
91
serve/templates/execute.go
Normal file
91
serve/templates/execute.go
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
package templates
|
||||||
|
|
||||||
|
import "html/template"
|
||||||
|
|
||||||
|
// ExecutePageTemplate - шаблон страницы выполнения запросов
|
||||||
|
var ExecutePageTemplate = template.Must(template.New("execute").Parse(`<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{{.Title}} - {{.AppName}}</title>
|
||||||
|
<style>
|
||||||
|
{{template "execute_css" .}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>{{.Header}}</h1>
|
||||||
|
<p>Выполнение запросов к {{.AppName}} через веб-интерфейс</p>
|
||||||
|
<p class="config-info">({{.ProviderType}} • {{.Model}} • {{.Host}})</p>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<div class="nav-buttons">
|
||||||
|
<a href="{{.BasePath}}/" class="nav-btn">🏠 Главная</a>
|
||||||
|
<a href="{{.BasePath}}/history" class="nav-btn">📝 История</a>
|
||||||
|
<a href="{{.BasePath}}/prompts" class="nav-btn">⚙️ Промпты</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="POST" id="executeForm">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
||||||
|
|
||||||
|
<div class="form-section">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="system_id">🤖 Системный промпт:</label>
|
||||||
|
<select name="system_id" id="system_id" required>
|
||||||
|
{{range .SystemOptions}}
|
||||||
|
<option value="{{.ID}}">{{.ID}}. {{.Name}}</option>
|
||||||
|
{{end}}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="prompt">💬 Ваш запрос:</label>
|
||||||
|
<textarea name="prompt" id="prompt" placeholder="Опишите, что вы хотите сделать..." required>{{.CurrentPrompt}}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Скрытое поле для хранения результатов -->
|
||||||
|
<input type="hidden" id="resultData" name="resultData" value="">
|
||||||
|
|
||||||
|
<div class="form-buttons">
|
||||||
|
<button type="submit" class="submit-btn" id="submitBtn">
|
||||||
|
🚀 Выполнить запрос
|
||||||
|
</button>
|
||||||
|
<button type="button" class="reset-btn" id="resetBtn" onclick="resetForm()">
|
||||||
|
🔄 Сброс
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="loading" id="loading">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<p>Обрабатываю запрос...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{.ResultSection}}
|
||||||
|
|
||||||
|
{{.VerboseButtons}}
|
||||||
|
|
||||||
|
<div class="verbose-loading" id="verboseLoading">
|
||||||
|
<div class="verbose-spinner"></div>
|
||||||
|
<p>Получаю подробное объяснение...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{.ActionButtons}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Кнопка "Наверх" -->
|
||||||
|
<button class="scroll-to-top" id="scrollToTop" onclick="scrollToTop()" style="display: none;">↑</button>
|
||||||
|
|
||||||
|
{{template "execute_scripts" .}}
|
||||||
|
</body>
|
||||||
|
</html>`))
|
||||||
|
|
||||||
|
// Объединяем шаблоны
|
||||||
|
func init() {
|
||||||
|
template.Must(ExecutePageTemplate.AddParseTree("execute_css", ExecutePageCSSTemplate.Tree))
|
||||||
|
template.Must(ExecutePageTemplate.AddParseTree("execute_scripts", ExecutePageScriptsTemplate.Tree))
|
||||||
|
}
|
||||||
297
serve/templates/execute.js.go
Normal file
297
serve/templates/execute.js.go
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
package templates
|
||||||
|
|
||||||
|
import "html/template"
|
||||||
|
|
||||||
|
var ExecutePageScriptsTemplate = template.Must(template.New("execute_scripts").Parse(`
|
||||||
|
<script>
|
||||||
|
// Обработка отправки формы (блокируем все кнопки кроме навигации)
|
||||||
|
document.getElementById('executeForm').addEventListener('submit', function(e) {
|
||||||
|
// Предотвращаем множественные отправки на мобильных устройствах
|
||||||
|
if (this.dataset.submitting === 'true') {
|
||||||
|
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');
|
||||||
|
const actionButtons = document.querySelectorAll('.action-btn');
|
||||||
|
const verboseButtons = document.querySelectorAll('.verbose-btn');
|
||||||
|
const scrollBtn = document.getElementById('scrollToTop');
|
||||||
|
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
submitBtn.textContent = '⏳ Выполняется...';
|
||||||
|
loading.classList.add('show');
|
||||||
|
|
||||||
|
// Блокируем кнопки действий (сохранение/история)
|
||||||
|
actionButtons.forEach(btn => {
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.style.opacity = '0.5';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Блокируем кнопки подробностей (v/vv/vvv)
|
||||||
|
verboseButtons.forEach(btn => {
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.style.opacity = '0.5';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Прячем кнопку "Наверх" до получения нового ответа
|
||||||
|
if (scrollBtn) {
|
||||||
|
scrollBtn.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Запрос подробного объяснения
|
||||||
|
function requestExplanation(verbose) {
|
||||||
|
const form = document.getElementById('executeForm');
|
||||||
|
const prompt = document.getElementById('prompt').value;
|
||||||
|
const systemId = document.getElementById('system_id').value;
|
||||||
|
const verboseLoading = document.getElementById('verboseLoading');
|
||||||
|
const verboseButtons = document.querySelectorAll('.verbose-btn');
|
||||||
|
const actionButtons = document.querySelectorAll('.action-btn');
|
||||||
|
|
||||||
|
if (!prompt.trim()) {
|
||||||
|
alert('Сначала выполните основной запрос');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Показываем лоадер и блокируем ВСЕ кнопки кроме навигации
|
||||||
|
verboseLoading.classList.add('show');
|
||||||
|
verboseButtons.forEach(btn => {
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.style.opacity = '0.5';
|
||||||
|
});
|
||||||
|
actionButtons.forEach(btn => {
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.style.opacity = '0.5';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Создаем скрытое поле для verbose
|
||||||
|
const verboseInput = document.createElement('input');
|
||||||
|
verboseInput.type = 'hidden';
|
||||||
|
verboseInput.name = 'verbose';
|
||||||
|
verboseInput.value = verbose;
|
||||||
|
form.appendChild(verboseInput);
|
||||||
|
|
||||||
|
// Отправляем форму
|
||||||
|
form.submit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохранение результата
|
||||||
|
function saveResult() {
|
||||||
|
const resultDataField = document.getElementById('resultData');
|
||||||
|
const prompt = document.getElementById('prompt').value;
|
||||||
|
const csrfToken = document.querySelector('input[name="csrf_token"]').value;
|
||||||
|
|
||||||
|
if (!resultDataField.value || !prompt.trim()) {
|
||||||
|
alert('Нет данных для сохранения');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resultData = JSON.parse(resultDataField.value);
|
||||||
|
const requestData = {
|
||||||
|
prompt: prompt,
|
||||||
|
command: resultData.command,
|
||||||
|
explanation: resultData.explanation || '',
|
||||||
|
model: resultData.model || 'Unknown'
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch('{{.BasePath}}/api/save-result', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-Token': csrfToken,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(requestData)
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
alert('✅ Результат сохранен: ' + data.file);
|
||||||
|
} else {
|
||||||
|
alert('❌ Ошибка сохранения: ' + data.error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('❌ Ошибка при сохранении результата');
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error parsing result data:', error);
|
||||||
|
alert('❌ Ошибка при чтении данных результата');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавление в историю
|
||||||
|
function addToHistory() {
|
||||||
|
const resultDataField = document.getElementById('resultData');
|
||||||
|
const prompt = document.getElementById('prompt').value;
|
||||||
|
const systemId = document.getElementById('system_id').value;
|
||||||
|
const csrfToken = document.querySelector('input[name="csrf_token"]').value;
|
||||||
|
|
||||||
|
if (!resultDataField.value || !prompt.trim()) {
|
||||||
|
alert('Нет данных для сохранения в историю');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resultData = JSON.parse(resultDataField.value);
|
||||||
|
const systemName = document.querySelector('option[value="' + systemId + '"]')?.textContent || 'Unknown';
|
||||||
|
|
||||||
|
const requestData = {
|
||||||
|
prompt: prompt,
|
||||||
|
command: resultData.command,
|
||||||
|
response: resultData.command,
|
||||||
|
explanation: resultData.explanation || '',
|
||||||
|
system: systemName
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch('{{.BasePath}}/api/add-to-history', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-Token': csrfToken,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(requestData)
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
alert('✅ ' + data.message);
|
||||||
|
} else {
|
||||||
|
alert('❌ Ошибка: ' + data.error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('❌ Ошибка при добавлении в историю');
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error parsing result data:', error);
|
||||||
|
alert('❌ Ошибка при чтении данных результата');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция прокрутки наверх
|
||||||
|
function scrollToTop() {
|
||||||
|
window.scrollTo({
|
||||||
|
top: 0,
|
||||||
|
behavior: 'smooth'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Показываем кнопку "Наверх" при появлении объяснения
|
||||||
|
function showScrollToTopButton() {
|
||||||
|
const scrollBtn = document.getElementById('scrollToTop');
|
||||||
|
if (scrollBtn) {
|
||||||
|
scrollBtn.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Скрываем кнопку "Наверх" при загрузке страницы
|
||||||
|
function hideScrollToTopButton() {
|
||||||
|
const scrollBtn = document.getElementById('scrollToTop');
|
||||||
|
if (scrollBtn) {
|
||||||
|
scrollBtn.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сброс формы к начальному состоянию
|
||||||
|
function resetForm() {
|
||||||
|
// Очищаем поля формы
|
||||||
|
document.getElementById('prompt').value = '';
|
||||||
|
document.getElementById('resultData').value = '';
|
||||||
|
|
||||||
|
// Сбрасываем флаг отправки формы
|
||||||
|
const form = document.getElementById('executeForm');
|
||||||
|
if (form) {
|
||||||
|
form.dataset.submitting = 'false';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Скрываем все секции результатов
|
||||||
|
const resultSection = document.querySelector('.result-section');
|
||||||
|
const verboseButtons = document.querySelector('.verbose-buttons');
|
||||||
|
const actionButtons = document.querySelector('.action-buttons');
|
||||||
|
const explanationSection = document.querySelector('.explanation-section');
|
||||||
|
const loading = document.getElementById('loading');
|
||||||
|
const verboseLoading = document.getElementById('verboseLoading');
|
||||||
|
const scrollBtn = document.getElementById('scrollToTop');
|
||||||
|
|
||||||
|
if (resultSection) resultSection.style.display = 'none';
|
||||||
|
if (verboseButtons) verboseButtons.style.display = 'none';
|
||||||
|
if (actionButtons) actionButtons.style.display = 'none';
|
||||||
|
if (explanationSection) explanationSection.style.display = 'none';
|
||||||
|
if (loading) loading.classList.remove('show');
|
||||||
|
if (verboseLoading) verboseLoading.classList.remove('show');
|
||||||
|
if (scrollBtn) scrollBtn.style.display = 'none';
|
||||||
|
|
||||||
|
// Разблокируем кнопки
|
||||||
|
const submitBtn = document.getElementById('submitBtn');
|
||||||
|
const resetBtn = document.getElementById('resetBtn');
|
||||||
|
const allButtons = document.querySelectorAll('.action-btn, .verbose-btn');
|
||||||
|
|
||||||
|
if (submitBtn) {
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
submitBtn.textContent = '🚀 Выполнить запрос';
|
||||||
|
}
|
||||||
|
if (resetBtn) resetBtn.disabled = false;
|
||||||
|
|
||||||
|
allButtons.forEach(btn => {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.style.opacity = '1';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Прокручиваем к началу формы (с проверкой поддержки smooth)
|
||||||
|
const formSection = document.querySelector('.form-section');
|
||||||
|
if (formSection) {
|
||||||
|
if ('scrollBehavior' in document.documentElement.style) {
|
||||||
|
formSection.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
} else {
|
||||||
|
formSection.scrollIntoView();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохранение результатов в скрытое поле
|
||||||
|
function saveResultToHiddenField() {
|
||||||
|
const resultDataField = document.getElementById('resultData');
|
||||||
|
const commandElement = document.querySelector('.command-code, .command-md');
|
||||||
|
const explanationElement = document.querySelector('.explanation-content');
|
||||||
|
const modelElement = document.querySelector('.result-meta span:first-child');
|
||||||
|
|
||||||
|
if (commandElement) {
|
||||||
|
const command = commandElement.textContent.trim();
|
||||||
|
const explanation = explanationElement ? explanationElement.innerHTML.trim() : '';
|
||||||
|
const model = modelElement ? modelElement.textContent.replace('Модель: ', '') : 'Unknown';
|
||||||
|
|
||||||
|
const resultData = {
|
||||||
|
command: command,
|
||||||
|
explanation: explanation,
|
||||||
|
model: model
|
||||||
|
};
|
||||||
|
|
||||||
|
resultDataField.value = JSON.stringify(resultData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Показываем кнопку при появлении объяснения и сохраняем результаты
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const explanationSection = document.querySelector('.explanation-section');
|
||||||
|
if (explanationSection) {
|
||||||
|
showScrollToTopButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохраняем результаты в скрытое поле при загрузке страницы
|
||||||
|
saveResultToHiddenField();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
`))
|
||||||
136
serve/templates/file.go
Normal file
136
serve/templates/file.go
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
package templates
|
||||||
|
|
||||||
|
// FileViewTemplate шаблон для просмотра файла результата
|
||||||
|
const FileViewTemplate = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<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%);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
background: linear-gradient(135deg, #2d5016 0%, #4a7c59 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 20px 30px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.header h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.5em;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
.back-btn {
|
||||||
|
background: rgba(255,255,255,0.2);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: background 0.3s ease;
|
||||||
|
}
|
||||||
|
.back-btn:hover {
|
||||||
|
background: rgba(255,255,255,0.3);
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
padding: 30px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.content h1 {
|
||||||
|
color: #2d5016;
|
||||||
|
border-bottom: 2px solid #2d5016;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
.content h2 {
|
||||||
|
color: #4a7c59;
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
.content h3 {
|
||||||
|
color: #2d5016;
|
||||||
|
}
|
||||||
|
.content code {
|
||||||
|
background: #f0f8f0;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: 'Monaco', 'Menlo', monospace;
|
||||||
|
color: #2d5016;
|
||||||
|
border: 1px solid #a8e6cf;
|
||||||
|
}
|
||||||
|
.content pre {
|
||||||
|
background: #f0f8f0;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 4px solid #2d5016;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
.content pre code {
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
color: #2d5016;
|
||||||
|
}
|
||||||
|
.content blockquote {
|
||||||
|
border-left: 4px solid #4a7c59;
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 10px 20px;
|
||||||
|
background: #f0f8f0;
|
||||||
|
border-radius: 0 8px 8px 0;
|
||||||
|
}
|
||||||
|
.content ul, .content ol {
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
.content li {
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
.content strong {
|
||||||
|
color: #2d5016;
|
||||||
|
}
|
||||||
|
.content em {
|
||||||
|
color: #4a7c59;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Мобильная адаптация */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
body { padding: 10px; }
|
||||||
|
.container { margin: 0; border-radius: 8px; box-shadow: 0 10px 20px rgba(0,0,0,0.1); }
|
||||||
|
.header { padding: 16px; }
|
||||||
|
.header h1 { font-size: 1.2em; }
|
||||||
|
.back-btn { padding: 6px 12px; font-size: 0.9em; }
|
||||||
|
.content { padding: 20px; }
|
||||||
|
.content pre { font-size: 14px; }
|
||||||
|
}
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.header h1 { font-size: 1em; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>📄 {{.Filename}}</h1>
|
||||||
|
<a href="/" class="back-btn">← Назад к списку</a>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
{{.Content}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
310
serve/templates/history.go
Normal file
310
serve/templates/history.go
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
package templates
|
||||||
|
|
||||||
|
// HistoryPageTemplate шаблон страницы истории запросов
|
||||||
|
const HistoryPageTemplate = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>История запросов - 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%);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
background: linear-gradient(135deg, #2d5016 0%, #4a7c59 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.header h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 2.5em;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
.nav-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.nav-btn {
|
||||||
|
background: #3498db;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1em;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: background 0.3s ease;
|
||||||
|
display: inline-block;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.nav-btn:hover {
|
||||||
|
background: #2980b9;
|
||||||
|
}
|
||||||
|
.clear-btn {
|
||||||
|
background: #e74c3c;
|
||||||
|
}
|
||||||
|
.clear-btn:hover {
|
||||||
|
background: #c0392b;
|
||||||
|
}
|
||||||
|
.history-item {
|
||||||
|
background: #f0f8f0;
|
||||||
|
border: 1px solid #a8e6cf;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
.history-item:hover {
|
||||||
|
border-color: #2d5016;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 25px rgba(45,80,22,0.2);
|
||||||
|
}
|
||||||
|
.history-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.history-index {
|
||||||
|
background: #2d5016;
|
||||||
|
color: white;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.history-timestamp {
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
.history-command {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.history-response {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: 'Monaco', 'Menlo', monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #2d5016;
|
||||||
|
border-left: 3px solid #2d5016;
|
||||||
|
}
|
||||||
|
.delete-btn {
|
||||||
|
background: #e74c3c;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.8em;
|
||||||
|
transition: background 0.3s ease;
|
||||||
|
}
|
||||||
|
.delete-btn:hover {
|
||||||
|
background: #c0392b;
|
||||||
|
}
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 60px 20px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Мобильная адаптация */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
body { padding: 10px; }
|
||||||
|
.container { margin: 0; border-radius: 8px; box-shadow: 0 10px 20px rgba(0,0,0,0.1); }
|
||||||
|
.header { padding: 20px; }
|
||||||
|
.header h1 { font-size: 2em; }
|
||||||
|
.content { padding: 20px; }
|
||||||
|
.nav-buttons { flex-direction: column; gap: 8px; }
|
||||||
|
.nav-btn { text-align: center; padding: 12px 16px; font-size: 14px; }
|
||||||
|
.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) {
|
||||||
|
.header h1 { font-size: 1.8em; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>📝 История запросов</h1>
|
||||||
|
<p>Управление историей запросов {{.AppName}}</p>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<div class="nav-buttons">
|
||||||
|
<a href="{{.BasePath}}/" class="nav-btn">🏠 Главная</a>
|
||||||
|
<a href="{{.BasePath}}/run" class="nav-btn">🚀 Выполнение</a>
|
||||||
|
<a href="{{.BasePath}}/prompts" class="nav-btn">⚙️ Промпты</a>
|
||||||
|
<button class="nav-btn clear-btn" onclick="clearHistory()">🗑️ Очистить всю историю</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Поиск -->
|
||||||
|
<div class="search-container" style="margin: 20px 0;">
|
||||||
|
<input type="text" id="searchInput" placeholder="🔍 Поиск по командам, ответам и объяснениям..."
|
||||||
|
style="width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 16px;">
|
||||||
|
<div id="searchResults" style="margin-top: 10px; color: #666; font-size: 14px;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{if .Entries}}
|
||||||
|
{{range .Entries}}
|
||||||
|
<div class="history-item" onclick="viewHistoryEntry({{.Index}})">
|
||||||
|
<div class="history-header">
|
||||||
|
<div>
|
||||||
|
<span class="history-index">#{{.Index}}</span>
|
||||||
|
<span class="history-timestamp">{{.Timestamp}}</span>
|
||||||
|
</div>
|
||||||
|
<button class="delete-btn" onclick="event.stopPropagation(); deleteHistoryEntry({{.Index}})">🗑️ Удалить</button>
|
||||||
|
</div>
|
||||||
|
<div class="history-command">{{.Command}}</div>
|
||||||
|
<div class="history-response">{{.Response}}</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{else}}
|
||||||
|
<div class="empty-state">
|
||||||
|
<h3>📝 История пуста</h3>
|
||||||
|
<p>Здесь будут отображаться запросы после использования команды lcg</p>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function viewHistoryEntry(index) {
|
||||||
|
window.location.href = '{{.BasePath}}/history/view/' + index;
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteHistoryEntry(index) {
|
||||||
|
if (confirm('Вы уверены, что хотите удалить запись #' + index + '?')) {
|
||||||
|
fetch('{{.BasePath}}/history/delete/' + index, {
|
||||||
|
method: 'DELETE'
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.ok) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Ошибка при удалении записи');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('Ошибка при удалении записи');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearHistory() {
|
||||||
|
if (confirm('Вы уверены, что хотите очистить всю историю?\\n\\nЭто действие нельзя отменить.')) {
|
||||||
|
fetch('{{.BasePath}}/history/clear', {
|
||||||
|
method: 'DELETE'
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.ok) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Ошибка при очистке истории');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('Ошибка при очистке истории');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Поиск по истории
|
||||||
|
function performSearch() {
|
||||||
|
const searchTerm = document.getElementById('searchInput').value.trim();
|
||||||
|
const searchResults = document.getElementById('searchResults');
|
||||||
|
const historyItems = document.querySelectorAll('.history-item');
|
||||||
|
|
||||||
|
if (searchTerm === '') {
|
||||||
|
// Показать все записи
|
||||||
|
historyItems.forEach(item => {
|
||||||
|
item.style.display = 'block';
|
||||||
|
});
|
||||||
|
searchResults.textContent = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let visibleCount = 0;
|
||||||
|
let totalCount = historyItems.length;
|
||||||
|
|
||||||
|
historyItems.forEach(item => {
|
||||||
|
const command = item.querySelector('.history-command').textContent.toLowerCase();
|
||||||
|
const response = item.querySelector('.history-response').textContent.toLowerCase();
|
||||||
|
|
||||||
|
// Объединяем команду и ответ для поиска
|
||||||
|
const searchContent = command + ' ' + response;
|
||||||
|
|
||||||
|
let matches = false;
|
||||||
|
|
||||||
|
// Проверяем, есть ли фраза в кавычках
|
||||||
|
if (searchTerm.startsWith("'") && searchTerm.endsWith("'")) {
|
||||||
|
// Поиск точной фразы
|
||||||
|
const phrase = searchTerm.slice(1, -1).toLowerCase();
|
||||||
|
matches = searchContent.includes(phrase);
|
||||||
|
} else {
|
||||||
|
// Поиск по отдельным словам
|
||||||
|
const words = searchTerm.toLowerCase().split(/\s+/);
|
||||||
|
matches = words.every(word => searchContent.includes(word));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matches) {
|
||||||
|
item.style.display = 'block';
|
||||||
|
visibleCount++;
|
||||||
|
} else {
|
||||||
|
item.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обновляем информацию о результатах
|
||||||
|
if (visibleCount === 0) {
|
||||||
|
searchResults.textContent = '🔍 Ничего не найдено';
|
||||||
|
searchResults.style.color = '#e74c3c';
|
||||||
|
} else if (visibleCount === totalCount) {
|
||||||
|
searchResults.textContent = '';
|
||||||
|
} else {
|
||||||
|
searchResults.textContent = '🔍 Найдено: ' + visibleCount + ' из ' + totalCount + ' записей';
|
||||||
|
searchResults.style.color = '#27ae60';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработчик ввода в поле поиска
|
||||||
|
document.getElementById('searchInput').addEventListener('input', performSearch);
|
||||||
|
|
||||||
|
// Обработчик Enter в поле поиска
|
||||||
|
document.getElementById('searchInput').addEventListener('keypress', function(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
performSearch();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
279
serve/templates/history_view.go
Normal file
279
serve/templates/history_view.go
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
package templates
|
||||||
|
|
||||||
|
// HistoryViewTemplate шаблон для просмотра записи истории
|
||||||
|
const HistoryViewTemplate = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<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%);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
background: linear-gradient(135deg, #2d5016 0%, #4a7c59 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 20px 30px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.header h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.5em;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
.back-btn {
|
||||||
|
background: rgba(255,255,255,0.2);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: background 0.3s ease;
|
||||||
|
}
|
||||||
|
.back-btn:hover {
|
||||||
|
background: rgba(255,255,255,0.3);
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
padding: 30px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.history-meta {
|
||||||
|
background: #f0f8f0;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-left: 4px solid #2d5016;
|
||||||
|
}
|
||||||
|
.history-meta-item {
|
||||||
|
margin: 5px 0;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.history-meta-label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2d5016;
|
||||||
|
}
|
||||||
|
.history-command {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-left: 4px solid #4a7c59;
|
||||||
|
}
|
||||||
|
.history-command h3 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
color: #2d5016;
|
||||||
|
}
|
||||||
|
.history-command-text {
|
||||||
|
font-family: 'Monaco', 'Menlo', monospace;
|
||||||
|
font-size: 1.1em;
|
||||||
|
color: #333;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
.history-response {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 4px solid #2d5016;
|
||||||
|
}
|
||||||
|
.history-response h3 {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
color: #2d5016;
|
||||||
|
}
|
||||||
|
.history-response-content {
|
||||||
|
font-family: 'Monaco', 'Menlo', monospace;
|
||||||
|
font-size: 0.95em;
|
||||||
|
color: #333;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.history-explanation {
|
||||||
|
background: #f0f8f0;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-top: 20px;
|
||||||
|
border-left: 4px solid #4a7c59;
|
||||||
|
}
|
||||||
|
.history-explanation h3 {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
color: #2d5016;
|
||||||
|
}
|
||||||
|
.history-explanation-content {
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.history-explanation-content h1,
|
||||||
|
.history-explanation-content h2,
|
||||||
|
.history-explanation-content h3,
|
||||||
|
.history-explanation-content h4,
|
||||||
|
.history-explanation-content h5,
|
||||||
|
.history-explanation-content h6 {
|
||||||
|
color: #2d5016;
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.history-explanation-content h1 {
|
||||||
|
border-bottom: 2px solid #2d5016;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
}
|
||||||
|
.history-explanation-content h2 {
|
||||||
|
border-bottom: 1px solid #4a7c59;
|
||||||
|
padding-bottom: 3px;
|
||||||
|
}
|
||||||
|
.history-explanation-content code {
|
||||||
|
background: #f0f8f0;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: 'Monaco', 'Menlo', monospace;
|
||||||
|
color: #2d5016;
|
||||||
|
border: 1px solid #a8e6cf;
|
||||||
|
}
|
||||||
|
.history-explanation-content pre {
|
||||||
|
background: #f0f8f0;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 4px solid #2d5016;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
.history-explanation-content pre code {
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
color: #2d5016;
|
||||||
|
}
|
||||||
|
.history-explanation-content blockquote {
|
||||||
|
border-left: 4px solid #4a7c59;
|
||||||
|
margin: 15px 0;
|
||||||
|
padding: 10px 20px;
|
||||||
|
background: #f0f8f0;
|
||||||
|
border-radius: 0 8px 8px 0;
|
||||||
|
}
|
||||||
|
.history-explanation-content ul,
|
||||||
|
.history-explanation-content ol {
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
.history-explanation-content li {
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
.history-explanation-content strong {
|
||||||
|
color: #2d5016;
|
||||||
|
}
|
||||||
|
.history-explanation-content em {
|
||||||
|
color: #4a7c59;
|
||||||
|
}
|
||||||
|
.actions {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.action-btn {
|
||||||
|
background: #3498db;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: background 0.3s ease;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.action-btn:hover {
|
||||||
|
background: #2980b9;
|
||||||
|
}
|
||||||
|
.delete-btn {
|
||||||
|
background: #e74c3c;
|
||||||
|
}
|
||||||
|
.delete-btn:hover {
|
||||||
|
background: #c0392b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Мобильная адаптация */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
body { padding: 10px; }
|
||||||
|
.container { margin: 0; border-radius: 8px; box-shadow: 0 10px 20px rgba(0,0,0,0.1); }
|
||||||
|
.header { padding: 16px; }
|
||||||
|
.header h1 { font-size: 1.2em; }
|
||||||
|
.back-btn { padding: 6px 12px; font-size: 0.9em; }
|
||||||
|
.content { padding: 20px; }
|
||||||
|
.actions { flex-direction: column; }
|
||||||
|
.action-btn { text-align: center; }
|
||||||
|
.history-response-content { font-size: 0.9em; }
|
||||||
|
}
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.header h1 { font-size: 1em; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>📝 Запись #{{.Index}}</h1>
|
||||||
|
<a href="{{.BasePath}}/history" class="back-btn">← Назад к истории</a>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<div class="history-meta">
|
||||||
|
<div class="history-meta-item">
|
||||||
|
<span class="history-meta-label">📅 Время:</span> {{.Timestamp}}
|
||||||
|
</div>
|
||||||
|
<div class="history-meta-item">
|
||||||
|
<span class="history-meta-label">🔢 Индекс:</span> #{{.Index}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="history-command">
|
||||||
|
<h3>💬 Запрос пользователя:</h3>
|
||||||
|
<div class="history-command-text">{{.Command}}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="history-response">
|
||||||
|
<h3>🤖 Ответ Модели:</h3>
|
||||||
|
<div class="history-response-content">{{.Response}}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{.ExplanationHTML}}
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<a href="{{.BasePath}}/history" class="action-btn">📝 К истории</a>
|
||||||
|
<button class="action-btn delete-btn" onclick="deleteHistoryEntry({{.Index}})">🗑️ Удалить запись</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function deleteHistoryEntry(index) {
|
||||||
|
if (confirm('Вы уверены, что хотите удалить запись #' + index + '?')) {
|
||||||
|
fetch('{{.BasePath}}/history/delete/' + index, {
|
||||||
|
method: 'DELETE'
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.ok) {
|
||||||
|
window.location.href = '{{.BasePath}}/history';
|
||||||
|
} else {
|
||||||
|
alert('Ошибка при удалении записи');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('Ошибка при удалении записи');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
323
serve/templates/login.go
Normal file
323
serve/templates/login.go
Normal file
@@ -0,0 +1,323 @@
|
|||||||
|
package templates
|
||||||
|
|
||||||
|
// LoginPageTemplate шаблон страницы авторизации
|
||||||
|
const LoginPageTemplate = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{{.Title}}</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: linear-gradient(-45deg, #667eea, #764ba2, #f093fb, #f5576c, #4facfe, #00f2fe);
|
||||||
|
background-size: 400% 400%;
|
||||||
|
animation: gradientShift 15s ease infinite;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes gradientShift {
|
||||||
|
0% { background-position: 0% 50%; }
|
||||||
|
50% { background-position: 100% 50%; }
|
||||||
|
100% { background-position: 0% 50%; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Плавающие элементы */
|
||||||
|
.floating-elements {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-element {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0.1;
|
||||||
|
animation: float 20s infinite linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-element:nth-child(1) { left: 10%; animation-delay: 0s; animation-duration: 25s; }
|
||||||
|
.floating-element:nth-child(2) { left: 20%; animation-delay: 2s; animation-duration: 30s; }
|
||||||
|
.floating-element:nth-child(3) { left: 30%; animation-delay: 4s; animation-duration: 20s; }
|
||||||
|
.floating-element:nth-child(4) { left: 40%; animation-delay: 6s; animation-duration: 35s; }
|
||||||
|
.floating-element:nth-child(5) { left: 50%; animation-delay: 8s; animation-duration: 28s; }
|
||||||
|
.floating-element:nth-child(6) { left: 60%; animation-delay: 10s; animation-duration: 22s; }
|
||||||
|
.floating-element:nth-child(7) { left: 70%; animation-delay: 12s; animation-duration: 32s; }
|
||||||
|
.floating-element:nth-child(8) { left: 80%; animation-delay: 14s; animation-duration: 26s; }
|
||||||
|
.floating-element:nth-child(9) { left: 90%; animation-delay: 16s; animation-duration: 24s; }
|
||||||
|
|
||||||
|
@keyframes float {
|
||||||
|
0% { transform: translateY(100vh) rotate(0deg); opacity: 0; }
|
||||||
|
10% { opacity: 0.1; }
|
||||||
|
90% { opacity: 0.1; }
|
||||||
|
100% { transform: translateY(-100px) rotate(360deg); opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.lock-icon {
|
||||||
|
font-size: 2rem;
|
||||||
|
color: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-icon {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shield-icon {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
color: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.star-icon {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-container {
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.2);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header h1 {
|
||||||
|
color: #333;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header p {
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 2px solid #e1e5e9;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.2);
|
||||||
|
background: rgba(255, 255, 255, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||||
|
transition: left 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 10px 25px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button:hover::before {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button:active {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.success {
|
||||||
|
background-color: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.error {
|
||||||
|
background-color: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
display: none;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
border: 2px solid #f3f3f3;
|
||||||
|
border-top: 2px solid #667eea;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Плавающие элементы фона -->
|
||||||
|
<div class="floating-elements">
|
||||||
|
<div class="floating-element lock-icon">🔒</div>
|
||||||
|
<div class="floating-element key-icon">🔑</div>
|
||||||
|
<div class="floating-element shield-icon">🛡️</div>
|
||||||
|
<div class="floating-element star-icon">⭐</div>
|
||||||
|
<div class="floating-element lock-icon">🔐</div>
|
||||||
|
<div class="floating-element key-icon">🗝️</div>
|
||||||
|
<div class="floating-element shield-icon">🔒</div>
|
||||||
|
<div class="floating-element star-icon">✨</div>
|
||||||
|
<div class="floating-element lock-icon">🔒</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="login-container">
|
||||||
|
<div class="login-header">
|
||||||
|
<h1>🔐 Авторизация</h1>
|
||||||
|
<p>Войдите в систему для доступа к LCG</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="loginForm">
|
||||||
|
<input type="hidden" id="csrf_token" name="csrf_token" value="{{.CSRFToken}}">
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username">Имя пользователя:</label>
|
||||||
|
<input type="text" id="username" name="username" required placeholder="Введите имя пользователя">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Пароль:</label>
|
||||||
|
<input type="password" id="password" name="password" required placeholder="Введите пароль">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="login-button">Войти</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="loading" id="loading">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<p>Проверка авторизации...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="message"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.getElementById('loginForm').addEventListener('submit', async function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const form = e.target;
|
||||||
|
const formData = new FormData(form);
|
||||||
|
const username = formData.get('username');
|
||||||
|
const password = formData.get('password');
|
||||||
|
|
||||||
|
// Показываем загрузку
|
||||||
|
document.getElementById('loading').style.display = 'block';
|
||||||
|
document.getElementById('message').innerHTML = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const csrfToken = document.getElementById('csrf_token').value;
|
||||||
|
const response = await fetch('/api/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-Token': csrfToken
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
csrf_token: csrfToken
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
// Успешная авторизация, перенаправляем на главную страницу
|
||||||
|
window.location.href = '/';
|
||||||
|
} else {
|
||||||
|
// Ошибка авторизации
|
||||||
|
showMessage(data.error || 'Ошибка авторизации', 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showMessage('Ошибка соединения с сервером', 'error');
|
||||||
|
} finally {
|
||||||
|
document.getElementById('loading').style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function showMessage(text, type) {
|
||||||
|
const messageDiv = document.getElementById('message');
|
||||||
|
messageDiv.innerHTML = '<div class="message ' + type + '">' + text + '</div>';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
552
serve/templates/prompts.go
Normal file
552
serve/templates/prompts.go
Normal file
@@ -0,0 +1,552 @@
|
|||||||
|
package templates
|
||||||
|
|
||||||
|
// PromptsPageTemplate шаблон страницы управления промптами
|
||||||
|
const PromptsPageTemplate = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Системные промпты - 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%);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
background: linear-gradient(135deg, #2d5016 0%, #4a7c59 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.header h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 2.5em;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
.nav-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.nav-btn {
|
||||||
|
background: #3498db;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1em;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: background 0.3s ease;
|
||||||
|
display: inline-block;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.nav-btn:hover {
|
||||||
|
background: #2980b9;
|
||||||
|
}
|
||||||
|
.add-btn {
|
||||||
|
background: #27ae60;
|
||||||
|
}
|
||||||
|
.add-btn:hover {
|
||||||
|
background: #229954;
|
||||||
|
}
|
||||||
|
.prompt-item {
|
||||||
|
background: #f0f8f0;
|
||||||
|
border: 1px solid #a8e6cf;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.prompt-item:hover {
|
||||||
|
border-color: #2d5016;
|
||||||
|
}
|
||||||
|
.prompt-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.prompt-id {
|
||||||
|
background: #2d5016;
|
||||||
|
color: white;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.prompt-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
.prompt-description {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.prompt-content {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: 'Monaco', 'Menlo', monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #2d5016;
|
||||||
|
border-left: 3px solid #2d5016;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
.prompt-actions {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.action-btn {
|
||||||
|
background: #4a7c59;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.8em;
|
||||||
|
transition: background 0.3s ease;
|
||||||
|
}
|
||||||
|
.action-btn:hover {
|
||||||
|
background: #2d5016;
|
||||||
|
}
|
||||||
|
.delete-btn {
|
||||||
|
background: #e74c3c;
|
||||||
|
}
|
||||||
|
.delete-btn:hover {
|
||||||
|
background: #c0392b;
|
||||||
|
}
|
||||||
|
.restore-btn {
|
||||||
|
background: #3498db;
|
||||||
|
}
|
||||||
|
.restore-btn:hover {
|
||||||
|
background: #2980b9;
|
||||||
|
}
|
||||||
|
.default-badge {
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 0.7em;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 60px 20px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.lang-switcher {
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
.lang-btn {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9em;
|
||||||
|
transition: background 0.3s ease;
|
||||||
|
}
|
||||||
|
.lang-btn:hover {
|
||||||
|
background: #5a6268;
|
||||||
|
}
|
||||||
|
.lang-btn.active {
|
||||||
|
background: #3498db;
|
||||||
|
}
|
||||||
|
.lang-btn.active:hover {
|
||||||
|
background: #2980b9;
|
||||||
|
}
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-bottom: 2px solid #e9ecef;
|
||||||
|
}
|
||||||
|
.tab-btn {
|
||||||
|
background: #f8f9fa;
|
||||||
|
color: #6c757d;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 6px 6px 0 0;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1em;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border-bottom: 3px solid transparent;
|
||||||
|
}
|
||||||
|
.tab-btn:hover {
|
||||||
|
background: #e9ecef;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
.tab-btn.active {
|
||||||
|
background: #3498db;
|
||||||
|
color: white;
|
||||||
|
border-bottom-color: #2980b9;
|
||||||
|
}
|
||||||
|
.tab-content {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.tab-content.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Мобильная адаптация */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
body { padding: 10px; }
|
||||||
|
.container { margin: 0; border-radius: 8px; box-shadow: 0 10px 20px rgba(0,0,0,0.1); }
|
||||||
|
.header { padding: 20px; }
|
||||||
|
.header h1 { font-size: 2em; }
|
||||||
|
.content { padding: 20px; }
|
||||||
|
.nav-buttons { flex-direction: column; gap: 8px; }
|
||||||
|
.nav-btn { text-align: center; padding: 12px 16px; font-size: 14px; }
|
||||||
|
.lang-switcher { margin-left: 0; }
|
||||||
|
.tabs { flex-direction: column; gap: 8px; }
|
||||||
|
.tab-btn { text-align: center; }
|
||||||
|
.prompt-item { padding: 15px; }
|
||||||
|
.prompt-content { font-size: 0.85em; }
|
||||||
|
}
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.header h1 { font-size: 1.8em; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>⚙️ Системные промпты</h1>
|
||||||
|
<p>Управление системными промптами {{.AppName}}</p>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<div class="nav-buttons">
|
||||||
|
<a href="{{.BasePath}}/" class="nav-btn">🏠 Главная</a>
|
||||||
|
<a href="{{.BasePath}}/run" class="nav-btn">🚀 Выполнение</a>
|
||||||
|
<a href="{{.BasePath}}/history" class="nav-btn">📝 История</a>
|
||||||
|
<button class="nav-btn add-btn" onclick="showAddForm()">➕ Добавить промпт</button>
|
||||||
|
<div class="lang-switcher">
|
||||||
|
<button class="lang-btn {{if eq .Lang "ru"}}active{{end}}" onclick="switchLang('ru')">🇷🇺 RU</button>
|
||||||
|
<button class="lang-btn {{if eq .Lang "en"}}active{{end}}" onclick="switchLang('en')">🇺🇸 EN</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Вкладки -->
|
||||||
|
<div class="tabs">
|
||||||
|
<button class="tab-btn active" onclick="switchTab('system')">⚙️ Системные промпты</button>
|
||||||
|
<button class="tab-btn" onclick="switchTab('verbose')">📝 Промпты подробности (v/vv/vvv)</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Вкладка системных промптов -->
|
||||||
|
<div id="system-tab" class="tab-content active">
|
||||||
|
{{if .Prompts}}
|
||||||
|
{{range .Prompts}}
|
||||||
|
<div class="prompt-item">
|
||||||
|
<div class="prompt-actions">
|
||||||
|
<button class="action-btn" onclick="editPrompt({{.ID}}, '{{.Name}}', '{{.Description}}', '{{.Content}}')">✏️</button>
|
||||||
|
<button class="action-btn restore-btn" onclick="restorePrompt({{.ID}})" title="Восстановить к значению по умолчанию">🔄</button>
|
||||||
|
<button class="action-btn delete-btn" onclick="deletePrompt({{.ID}})">🗑️</button>
|
||||||
|
</div>
|
||||||
|
<div class="prompt-header">
|
||||||
|
<div>
|
||||||
|
<span class="prompt-id">#{{.ID}}</span>
|
||||||
|
<span class="prompt-name">{{.Name}}</span>
|
||||||
|
{{if .IsDefault}}<span class="default-badge">Встроенный</span>{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="prompt-description">{{.Description}}</div>
|
||||||
|
<div class="prompt-content">{{.Content}}</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{else}}
|
||||||
|
<div class="empty-state">
|
||||||
|
<h3>⚙️ Промпты не найдены</h3>
|
||||||
|
<p>Добавьте пользовательские промпты для настройки поведения системы</p>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Вкладка промптов подробности -->
|
||||||
|
<div id="verbose-tab" class="tab-content">
|
||||||
|
{{if .VerbosePrompts}}
|
||||||
|
{{range .VerbosePrompts}}
|
||||||
|
<div class="prompt-item">
|
||||||
|
<div class="prompt-actions">
|
||||||
|
<button class="action-btn" onclick="editVerbosePrompt('{{.Mode}}', '{{.Content}}')">✏️</button>
|
||||||
|
<button class="action-btn restore-btn" onclick="restoreVerbosePrompt('{{.Mode}}')" title="Восстановить к значению по умолчанию">🔄</button>
|
||||||
|
</div>
|
||||||
|
<div class="prompt-header">
|
||||||
|
<div>
|
||||||
|
<span class="prompt-id">#{{.Mode}}</span>
|
||||||
|
<span class="prompt-name">{{.Name}}</span>
|
||||||
|
{{if .IsDefault}}<span class="default-badge">Встроенный</span>{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="prompt-description">{{.Description}}</div>
|
||||||
|
<div class="prompt-content">{{.Content}}</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{else}}
|
||||||
|
<div class="empty-state">
|
||||||
|
<h3>📝 Промпты подробности</h3>
|
||||||
|
<p>Промпты для режимов v, vv, vvv</p>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Форма добавления/редактирования -->
|
||||||
|
<div id="promptForm" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000;">
|
||||||
|
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 30px; border-radius: 12px; max-width: 600px; width: 90%;">
|
||||||
|
<h3 id="formTitle">Добавить промпт</h3>
|
||||||
|
<form id="promptFormData">
|
||||||
|
<input type="hidden" id="promptId" name="id">
|
||||||
|
<div style="margin-bottom: 15px;">
|
||||||
|
<label style="display: block; margin-bottom: 5px; font-weight: 600;">Название:</label>
|
||||||
|
<input type="text" id="promptName" name="name" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;" required>
|
||||||
|
</div>
|
||||||
|
<div style="margin-bottom: 15px;">
|
||||||
|
<label style="display: block; margin-bottom: 5px; font-weight: 600;">Описание:</label>
|
||||||
|
<input type="text" id="promptDescription" name="description" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;" required>
|
||||||
|
</div>
|
||||||
|
<div style="margin-bottom: 20px;">
|
||||||
|
<label style="display: block; margin-bottom: 5px; font-weight: 600;">Содержание:</label>
|
||||||
|
<textarea id="promptContent" name="content" rows="6" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-family: monospace;" required></textarea>
|
||||||
|
</div>
|
||||||
|
<div style="text-align: right;">
|
||||||
|
<button type="button" onclick="hideForm()" style="background: #6c757d; color: white; border: none; padding: 8px 16px; border-radius: 4px; margin-right: 10px; cursor: pointer;">Отмена</button>
|
||||||
|
<button type="submit" style="background: #2d5016; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer;">Сохранить</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function showAddForm() {
|
||||||
|
document.getElementById('formTitle').textContent = 'Добавить промпт';
|
||||||
|
document.getElementById('promptFormData').reset();
|
||||||
|
document.getElementById('promptId').value = '';
|
||||||
|
document.getElementById('promptForm').style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
function editPrompt(id, name, description, content) {
|
||||||
|
document.getElementById('formTitle').textContent = 'Редактировать промпт';
|
||||||
|
document.getElementById('promptId').value = id;
|
||||||
|
document.getElementById('promptName').value = name;
|
||||||
|
document.getElementById('promptDescription').value = description;
|
||||||
|
document.getElementById('promptContent').value = content;
|
||||||
|
document.getElementById('promptForm').style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideForm() {
|
||||||
|
document.getElementById('promptForm').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchTab(tabName) {
|
||||||
|
// Скрываем все вкладки
|
||||||
|
document.querySelectorAll('.tab-content').forEach(tab => {
|
||||||
|
tab.classList.remove('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Убираем активный класс с кнопок
|
||||||
|
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||||
|
btn.classList.remove('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Показываем нужную вкладку
|
||||||
|
document.getElementById(tabName + '-tab').classList.add('active');
|
||||||
|
|
||||||
|
// Активируем нужную кнопку
|
||||||
|
event.target.classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchLang(lang) {
|
||||||
|
// Сохраняем текущие промпты перед переключением языка
|
||||||
|
saveCurrentPrompts(lang);
|
||||||
|
|
||||||
|
// Перезагружаем страницу с новым языком
|
||||||
|
const url = new URL(window.location);
|
||||||
|
url.searchParams.set('lang', lang);
|
||||||
|
window.location.href = url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveCurrentPrompts(lang) {
|
||||||
|
// Отправляем запрос для сохранения текущих промптов с новым языком
|
||||||
|
fetch('{{.BasePath}}/prompts/save-lang', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
lang: lang
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error saving prompts:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function editVerbosePrompt(mode, content) {
|
||||||
|
// Редактирование промпта подробности
|
||||||
|
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) {
|
||||||
|
if (confirm('Вы уверены, что хотите удалить промпт #' + id + '?')) {
|
||||||
|
fetch('{{.BasePath}}/prompts/delete/' + id, {
|
||||||
|
method: 'DELETE'
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.ok) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Ошибка при удалении промпта');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('Ошибка при удалении промпта');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 isVerbosePrompt = ['v', 'vv', 'vvv'].includes(id);
|
||||||
|
|
||||||
|
let url, method;
|
||||||
|
if (isVerbosePrompt) {
|
||||||
|
url = '{{.BasePath}}/prompts/edit-verbose/' + id;
|
||||||
|
method = 'PUT';
|
||||||
|
} else {
|
||||||
|
url = id ? '{{.BasePath}}/prompts/edit/' + id : '{{.BasePath}}/prompts/add';
|
||||||
|
method = id ? 'PUT' : 'POST';
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(url, {
|
||||||
|
method: method,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: formData.get('name'),
|
||||||
|
description: formData.get('description'),
|
||||||
|
content: formData.get('content')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.ok) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Ошибка при сохранении промпта');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('Ошибка при сохранении промпта');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Функция восстановления системного промпта
|
||||||
|
function restorePrompt(id) {
|
||||||
|
if (confirm('Восстановить промпт к значению по умолчанию?')) {
|
||||||
|
fetch('{{.BasePath}}/prompts/restore/' + id, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
alert('Промпт восстановлен');
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Ошибка: ' + data.error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('Ошибка при восстановлении промпта');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция восстановления verbose промпта
|
||||||
|
function restoreVerbosePrompt(mode) {
|
||||||
|
if (confirm('Восстановить промпт к значению по умолчанию?')) {
|
||||||
|
fetch('{{.BasePath}}/prompts/restore-verbose/' + mode, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
alert('Промпт восстановлен');
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Ошибка: ' + data.error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('Ошибка при восстановлении промпта');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
329
serve/templates/results.go
Normal file
329
serve/templates/results.go
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
package templates
|
||||||
|
|
||||||
|
// ResultsPageTemplate шаблон главной страницы со списком файлов
|
||||||
|
const ResultsPageTemplate = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{{.AppAbbreviation}} Результаты - {{.AppName}}</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%);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
background: linear-gradient(135deg, #2d5016 0%, #4a7c59 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.header h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 2.5em;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
.header p {
|
||||||
|
margin: 10px 0 0 0;
|
||||||
|
opacity: 0.9;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
.nav-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.stat-card {
|
||||||
|
background: #f0f8f0;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-align: center;
|
||||||
|
border-left: 4px solid #2d5016;
|
||||||
|
}
|
||||||
|
.stat-number {
|
||||||
|
font-size: 2em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2d5016;
|
||||||
|
}
|
||||||
|
.stat-label {
|
||||||
|
color: #666;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
.files-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
.file-card {
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e1e5e9;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.file-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 25px rgba(45,80,22,0.2);
|
||||||
|
border-color: #2d5016;
|
||||||
|
}
|
||||||
|
.file-card-content {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.file-actions {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.delete-btn {
|
||||||
|
background: #e74c3c;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.8em;
|
||||||
|
transition: background 0.3s ease;
|
||||||
|
}
|
||||||
|
.delete-btn:hover {
|
||||||
|
background: #c0392b;
|
||||||
|
}
|
||||||
|
.file-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
.file-info {
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.9em;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.file-preview {
|
||||||
|
background: #f0f8f0;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: 'Monaco', 'Menlo', monospace;
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: #2d5016;
|
||||||
|
max-height: 100px;
|
||||||
|
overflow: hidden;
|
||||||
|
border-left: 3px solid #2d5016;
|
||||||
|
}
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 60px 20px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.empty-state h3 {
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.nav-btn, .nav-button {
|
||||||
|
background: #3498db;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1em;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: background 0.3s ease;
|
||||||
|
display: inline-block;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.nav-btn:hover, .nav-button:hover {
|
||||||
|
background: #2980b9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Мобильная адаптация */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
body { padding: 10px; }
|
||||||
|
.container { margin: 0; border-radius: 8px; box-shadow: 0 10px 20px rgba(0,0,0,0.1); }
|
||||||
|
.header { padding: 20px; }
|
||||||
|
.header h1 { font-size: 2em; }
|
||||||
|
.content { padding: 20px; }
|
||||||
|
.files-grid { grid-template-columns: 1fr; }
|
||||||
|
.stats { grid-template-columns: 1fr 1fr; }
|
||||||
|
.nav-buttons { flex-direction: column; gap: 8px; }
|
||||||
|
.nav-btn, .nav-button { text-align: center; padding: 12px 16px; font-size: 14px; }
|
||||||
|
.search-container input { font-size: 16px; width: 96% !important; }
|
||||||
|
}
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.header h1 { font-size: 1.8em; }
|
||||||
|
.stats { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>🚀 {{.AppAbbreviation}} - {{.AppName}}</h1>
|
||||||
|
<p>Просмотр сохраненных результатов {{.AppName}}</p>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<div class="nav-buttons">
|
||||||
|
<button class="nav-btn" onclick="location.reload()">🔄 Обновить</button>
|
||||||
|
<a href="{{.BasePath}}/run" class="nav-btn">🚀 Выполнение</a>
|
||||||
|
<a href="{{.BasePath}}/history" class="nav-btn">📝 История</a>
|
||||||
|
<a href="{{.BasePath}}/prompts" class="nav-btn">⚙️ Промпты</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Поиск -->
|
||||||
|
<div class="search-container">
|
||||||
|
<input type="text" id="searchInput" placeholder="🔍 Поиск по содержимому файлов..."
|
||||||
|
style="width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 16px;">
|
||||||
|
<div id="searchResults" style="margin-top: 10px; color: #666; font-size: 14px;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stats">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-number">{{.TotalFiles}}</div>
|
||||||
|
<div class="stat-label">Всего файлов</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-number">{{.RecentFiles}}</div>
|
||||||
|
<div class="stat-label">За последние 7 дней</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{if .Files}}
|
||||||
|
<div class="files-grid">
|
||||||
|
{{range .Files}}
|
||||||
|
<div class="file-card" data-content="{{.Content}}">
|
||||||
|
<div class="file-actions">
|
||||||
|
<button class="delete-btn" onclick="deleteFile('{{.Name}}')" title="Удалить файл">🗑️</button>
|
||||||
|
</div>
|
||||||
|
<div class="file-card-content" onclick="window.location.href='{{$.BasePath}}/file/{{.Name}}'">
|
||||||
|
<div class="file-name">{{.Name}}</div>
|
||||||
|
<div class="file-info">
|
||||||
|
📅 {{.ModTime}} | 📏 {{.Size}}
|
||||||
|
</div>
|
||||||
|
<div class="file-preview">{{.Preview}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<div class="empty-state">
|
||||||
|
<h3>📁 Папка пуста</h3>
|
||||||
|
<p>Здесь будут отображаться сохраненные результаты после использования команды lcg</p>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function deleteFile(filename) {
|
||||||
|
if (confirm('Вы уверены, что хотите удалить файл "' + filename + '"?\\n\\nЭто действие нельзя отменить.')) {
|
||||||
|
fetch('{{.BasePath}}/delete/' + encodeURIComponent(filename), {
|
||||||
|
method: 'DELETE'
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.ok) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Ошибка при удалении файла');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('Ошибка при удалении файла');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Поиск по содержимому файлов
|
||||||
|
function performSearch() {
|
||||||
|
const searchTerm = document.getElementById('searchInput').value.trim();
|
||||||
|
const searchResults = document.getElementById('searchResults');
|
||||||
|
const fileCards = document.querySelectorAll('.file-card');
|
||||||
|
|
||||||
|
if (searchTerm === '') {
|
||||||
|
// Показать все файлы
|
||||||
|
fileCards.forEach(card => {
|
||||||
|
card.style.display = 'block';
|
||||||
|
});
|
||||||
|
searchResults.textContent = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let visibleCount = 0;
|
||||||
|
let totalCount = fileCards.length;
|
||||||
|
|
||||||
|
fileCards.forEach(card => {
|
||||||
|
const fileName = card.querySelector('.file-name').textContent.toLowerCase();
|
||||||
|
const fullContent = card.getAttribute('data-content').toLowerCase();
|
||||||
|
|
||||||
|
// Проверяем поиск по полному содержимому файла
|
||||||
|
const fileContent = fileName + ' ' + fullContent;
|
||||||
|
|
||||||
|
let matches = false;
|
||||||
|
|
||||||
|
// Проверяем, есть ли фраза в кавычках
|
||||||
|
if (searchTerm.startsWith("'") && searchTerm.endsWith("'")) {
|
||||||
|
// Поиск точной фразы
|
||||||
|
const phrase = searchTerm.slice(1, -1).toLowerCase();
|
||||||
|
matches = fileContent.includes(phrase);
|
||||||
|
} else {
|
||||||
|
// Поиск по отдельным словам
|
||||||
|
const words = searchTerm.toLowerCase().split(/\s+/);
|
||||||
|
matches = words.every(word => fileContent.includes(word));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matches) {
|
||||||
|
card.style.display = 'block';
|
||||||
|
visibleCount++;
|
||||||
|
} else {
|
||||||
|
card.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обновляем информацию о результатах
|
||||||
|
if (visibleCount === 0) {
|
||||||
|
searchResults.textContent = '🔍 Ничего не найдено';
|
||||||
|
searchResults.style.color = '#e74c3c';
|
||||||
|
} else if (visibleCount === totalCount) {
|
||||||
|
searchResults.textContent = '';
|
||||||
|
} else {
|
||||||
|
searchResults.textContent = '🔍 Найдено: ' + visibleCount + ' из ' + totalCount + ' файлов';
|
||||||
|
searchResults.style.color = '#27ae60';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработчик ввода в поле поиска
|
||||||
|
document.getElementById('searchInput').addEventListener('input', performSearch);
|
||||||
|
|
||||||
|
// Обработчик Enter в поле поиска
|
||||||
|
document.getElementById('searchInput').addEventListener('keypress', function(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
performSearch();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
10
shell-code/curl.sh
Normal file
10
shell-code/curl.sh
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
execute_command() {
|
||||||
|
curl -s -X POST "http://localhost:8085/api/execute" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"prompt\": \"$1\", \"verbose\": \"$2\"}" | \
|
||||||
|
jq -r '.'
|
||||||
|
}
|
||||||
|
|
||||||
|
execute_command "$1" "$2"
|
||||||
16
shell-code/docker-proxy-max.sh
Normal file
16
shell-code/docker-proxy-max.sh
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#! /usr/bin/bash
|
||||||
|
|
||||||
|
VERSION=$1
|
||||||
|
if [ -z "$VERSION" ]; then
|
||||||
|
VERSION=latest
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker pull kuznetcovay/lcg:"${VERSION}"
|
||||||
|
|
||||||
|
docker run -p 8080:8080 \
|
||||||
|
-e LCG_PROVIDER=proxy \
|
||||||
|
-e LCG_HOST=https://direct-dev.ru \
|
||||||
|
-e LCG_MODEL=GigaChat-2-Max \
|
||||||
|
-e LCG_JWT_TOKEN="$(go-ansible-vault --key "$(cat ~/.config/gak)" \
|
||||||
|
-i ~/.config/jwt.direct-dev.ru get -m JWT_TOKEN -q)" \
|
||||||
|
kuznetcovay/lcg:"${VERSION}"
|
||||||
@@ -2,4 +2,4 @@
|
|||||||
|
|
||||||
go-ansible-vault -i shell-code/build.env -a get -m GITHUB_TOKEN > /tmp/source && source /tmp/source
|
go-ansible-vault -i shell-code/build.env -a get -m GITHUB_TOKEN > /tmp/source && source /tmp/source
|
||||||
|
|
||||||
#GITHUB_TOKEN=$GITHUB_TOKEN python3 shell-code/release.py
|
GITHUB_TOKEN=$GITHUB_TOKEN python3 shell-code/release.py
|
||||||
@@ -11,6 +11,8 @@ import requests
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Цвета для вывода
|
# Цвета для вывода
|
||||||
|
|
||||||
|
|
||||||
class Colors:
|
class Colors:
|
||||||
RED = '\033[0;31m'
|
RED = '\033[0;31m'
|
||||||
GREEN = '\033[0;32m'
|
GREEN = '\033[0;32m'
|
||||||
@@ -18,23 +20,29 @@ class Colors:
|
|||||||
BLUE = '\033[0;34m'
|
BLUE = '\033[0;34m'
|
||||||
NC = '\033[0m' # No Color
|
NC = '\033[0m' # No Color
|
||||||
|
|
||||||
|
|
||||||
def log(message):
|
def log(message):
|
||||||
print(f"{Colors.GREEN}[INFO]{Colors.NC} {message}")
|
print(f"{Colors.GREEN}[INFO]{Colors.NC} {message}")
|
||||||
|
|
||||||
|
|
||||||
def error(message):
|
def error(message):
|
||||||
print(f"{Colors.RED}[ERROR]{Colors.NC} {message}", file=sys.stderr)
|
print(f"{Colors.RED}[ERROR]{Colors.NC} {message}", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
def warn(message):
|
def warn(message):
|
||||||
print(f"{Colors.YELLOW}[WARN]{Colors.NC} {message}")
|
print(f"{Colors.YELLOW}[WARN]{Colors.NC} {message}")
|
||||||
|
|
||||||
|
|
||||||
def debug(message):
|
def debug(message):
|
||||||
print(f"{Colors.BLUE}[DEBUG]{Colors.NC} {message}")
|
print(f"{Colors.BLUE}[DEBUG]{Colors.NC} {message}")
|
||||||
|
|
||||||
|
|
||||||
# Конфигурация
|
# Конфигурация
|
||||||
REPO = "direct-dev-ru/go-lcg"
|
REPO = "direct-dev-ru/go-lcg"
|
||||||
VERSION_FILE = "VERSION.txt"
|
VERSION_FILE = "VERSION.txt"
|
||||||
BINARIES_DIR = "binaries-for-upload"
|
BINARIES_DIR = "binaries-for-upload"
|
||||||
|
|
||||||
|
|
||||||
def check_environment():
|
def check_environment():
|
||||||
"""Проверка переменных окружения"""
|
"""Проверка переменных окружения"""
|
||||||
token = os.getenv('GITHUB_TOKEN')
|
token = os.getenv('GITHUB_TOKEN')
|
||||||
@@ -44,6 +52,7 @@ def check_environment():
|
|||||||
log(f"GITHUB_TOKEN установлен (длина: {len(token)} символов)")
|
log(f"GITHUB_TOKEN установлен (длина: {len(token)} символов)")
|
||||||
return token
|
return token
|
||||||
|
|
||||||
|
|
||||||
def get_version():
|
def get_version():
|
||||||
"""Получение версии из файла"""
|
"""Получение версии из файла"""
|
||||||
version_file = Path(VERSION_FILE)
|
version_file = Path(VERSION_FILE)
|
||||||
@@ -57,6 +66,7 @@ def get_version():
|
|||||||
log(f"Тег: {tag}")
|
log(f"Тег: {tag}")
|
||||||
return tag
|
return tag
|
||||||
|
|
||||||
|
|
||||||
def check_files():
|
def check_files():
|
||||||
"""Проверка файлов для загрузки"""
|
"""Проверка файлов для загрузки"""
|
||||||
binaries_path = Path(BINARIES_DIR)
|
binaries_path = Path(BINARIES_DIR)
|
||||||
@@ -77,6 +87,7 @@ def check_files():
|
|||||||
|
|
||||||
return files
|
return files
|
||||||
|
|
||||||
|
|
||||||
def create_github_session(token):
|
def create_github_session(token):
|
||||||
"""Создание сессии для GitHub API"""
|
"""Создание сессии для GitHub API"""
|
||||||
session = requests.Session()
|
session = requests.Session()
|
||||||
@@ -87,6 +98,7 @@ def create_github_session(token):
|
|||||||
})
|
})
|
||||||
return session
|
return session
|
||||||
|
|
||||||
|
|
||||||
def check_existing_release(session, tag):
|
def check_existing_release(session, tag):
|
||||||
"""Проверка существующего релиза"""
|
"""Проверка существующего релиза"""
|
||||||
log("Проверяем существующий релиз...")
|
log("Проверяем существующий релиз...")
|
||||||
@@ -95,16 +107,17 @@ def check_existing_release(session, tag):
|
|||||||
response = session.get(url)
|
response = session.get(url)
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
release_data = response.json()
|
release_data = response.json()
|
||||||
log(f"Реліз {tag} уже существует")
|
log(f"Релиз с тегом {tag} уже существует")
|
||||||
return release_data
|
return release_data
|
||||||
elif response.status_code == 404:
|
elif response.status_code == 404:
|
||||||
log(f"Реліз {tag} не найден, создаем новый")
|
log(f"Релиз с тегом {tag} не найден, создаем новый")
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
error(f"Ошибка проверки релиза: {response.status_code}")
|
error(f"Ошибка проверки релиза: {response.status_code}")
|
||||||
debug(f"Ответ: {response.text}")
|
debug(f"Ответ: {response.text}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def create_release(session, tag):
|
def create_release(session, tag):
|
||||||
"""Создание нового релиза"""
|
"""Создание нового релиза"""
|
||||||
log(f"Создаем новый релиз {tag}...")
|
log(f"Создаем новый релиз {tag}...")
|
||||||
@@ -122,21 +135,17 @@ def create_release(session, tag):
|
|||||||
|
|
||||||
if response.status_code == 201:
|
if response.status_code == 201:
|
||||||
release_data = response.json()
|
release_data = response.json()
|
||||||
log("Реліз создан успешно")
|
log("Релиз с тегом {tag} создан успешно")
|
||||||
return release_data
|
return release_data
|
||||||
else:
|
else:
|
||||||
error(f"Ошибка создания релиза: {response.status_code}")
|
error(f"Ошибка создания релиза с тегом {tag}: {response.status_code}")
|
||||||
debug(f"Ответ: {response.text}")
|
debug(f"Ответ: {response.text}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def upload_file(session, upload_url, file_path):
|
def upload_file(session, upload_url, file_path):
|
||||||
"""Загрузка файла в релиз"""
|
"""Загрузка файла в релиз"""
|
||||||
filename = file_path.name
|
filename = file_path.name
|
||||||
log(f"Загружаем: {filename}")
|
|
||||||
|
|
||||||
# Убираем {?name,label} из URL
|
|
||||||
upload_url = upload_url.replace("{?name,label}", "")
|
|
||||||
|
|
||||||
with open(file_path, 'rb') as f:
|
with open(file_path, 'rb') as f:
|
||||||
headers = {'Content-Type': 'application/octet-stream'}
|
headers = {'Content-Type': 'application/octet-stream'}
|
||||||
params = {'name': filename}
|
params = {'name': filename}
|
||||||
@@ -156,6 +165,7 @@ def upload_file(session, upload_url, file_path):
|
|||||||
debug(f"Ответ: {response.text}")
|
debug(f"Ответ: {response.text}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Основная функция"""
|
"""Основная функция"""
|
||||||
log("=== НАЧАЛО РАБОТЫ СКРИПТА ===")
|
log("=== НАЧАЛО РАБОТЫ СКРИПТА ===")
|
||||||
@@ -199,5 +209,6 @@ def main():
|
|||||||
log(f"Реліз доступен: https://github.com/{REPO}/releases/tag/{tag}")
|
log(f"Реліз доступен: https://github.com/{REPO}/releases/tag/{tag}")
|
||||||
log("=== СКРИПТ ЗАВЕРШЕН ===")
|
log("=== СКРИПТ ЗАВЕРШЕН ===")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
158
test_csrf.sh
Executable file
158
test_csrf.sh
Executable file
@@ -0,0 +1,158 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 🛡️ CSRF Protection Test Script
|
||||||
|
# Тестирует CSRF защиту LCG приложения
|
||||||
|
|
||||||
|
echo "🛡️ Тестирование CSRF защиты LCG"
|
||||||
|
echo "=================================="
|
||||||
|
|
||||||
|
# Цвета для вывода
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Функция для вывода результатов
|
||||||
|
print_result() {
|
||||||
|
local test_name="$1"
|
||||||
|
local status="$2"
|
||||||
|
local message="$3"
|
||||||
|
|
||||||
|
if [ "$status" = "PASS" ]; then
|
||||||
|
echo -e "${GREEN}✅ $test_name: PASS${NC} - $message"
|
||||||
|
elif [ "$status" = "FAIL" ]; then
|
||||||
|
echo -e "${RED}❌ $test_name: FAIL${NC} - $message"
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}⚠️ $test_name: $status${NC} - $message"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Проверяем, запущен ли сервер
|
||||||
|
echo -e "${BLUE}🔍 Проверяем доступность сервера...${NC}"
|
||||||
|
if ! curl -s http://localhost:8080/login > /dev/null 2>&1; then
|
||||||
|
echo -e "${RED}❌ Сервер не доступен на localhost:8080${NC}"
|
||||||
|
echo "Запустите сервер командой: LCG_SERVER_REQUIRE_AUTH=true ./lcg serve -p 8080"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${GREEN}✅ Сервер доступен${NC}"
|
||||||
|
|
||||||
|
# Тест 1: Попытка выполнения команды без CSRF токена
|
||||||
|
echo -e "\n${BLUE}🧪 Тест 1: Выполнение команды без CSRF токена${NC}"
|
||||||
|
response=$(curl -s -w "%{http_code}" -X POST http://localhost:8080/api/execute \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"prompt":"whoami","system_id":"1"}' \
|
||||||
|
-o /dev/null)
|
||||||
|
|
||||||
|
if [ "$response" = "403" ]; then
|
||||||
|
print_result "CSRF защита /api/execute" "PASS" "Запрос заблокирован (403 Forbidden)"
|
||||||
|
else
|
||||||
|
print_result "CSRF защита /api/execute" "FAIL" "Запрос прошел (HTTP $response)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Тест 2: Попытка сохранения результата без CSRF токена
|
||||||
|
echo -e "\n${BLUE}🧪 Тест 2: Сохранение результата без CSRF токена${NC}"
|
||||||
|
response=$(curl -s -w "%{http_code}" -X POST http://localhost:8080/api/save-result \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"result":"test result","command":"test command"}' \
|
||||||
|
-o /dev/null)
|
||||||
|
|
||||||
|
if [ "$response" = "403" ]; then
|
||||||
|
print_result "CSRF защита /api/save-result" "PASS" "Запрос заблокирован (403 Forbidden)"
|
||||||
|
else
|
||||||
|
print_result "CSRF защита /api/save-result" "FAIL" "Запрос прошел (HTTP $response)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Тест 3: Попытка добавления в историю без CSRF токена
|
||||||
|
echo -e "\n${BLUE}🧪 Тест 3: Добавление в историю без CSRF токена${NC}"
|
||||||
|
response=$(curl -s -w "%{http_code}" -X POST http://localhost:8080/api/add-to-history \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"prompt":"test prompt","result":"test result"}' \
|
||||||
|
-o /dev/null)
|
||||||
|
|
||||||
|
if [ "$response" = "403" ]; then
|
||||||
|
print_result "CSRF защита /api/add-to-history" "PASS" "Запрос заблокирован (403 Forbidden)"
|
||||||
|
else
|
||||||
|
print_result "CSRF защита /api/add-to-history" "FAIL" "Запрос прошел (HTTP $response)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Тест 4: Проверка GET запросов (должны работать)
|
||||||
|
echo -e "\n${BLUE}🧪 Тест 4: GET запросы (должны работать)${NC}"
|
||||||
|
response=$(curl -s -w "%{http_code}" http://localhost:8080/login -o /dev/null)
|
||||||
|
|
||||||
|
if [ "$response" = "200" ]; then
|
||||||
|
print_result "GET запросы" "PASS" "GET запросы работают (HTTP $response)"
|
||||||
|
else
|
||||||
|
print_result "GET запросы" "FAIL" "GET запросы не работают (HTTP $response)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Тест 5: Проверка наличия CSRF токена на странице входа
|
||||||
|
echo -e "\n${BLUE}🧪 Тест 5: Наличие CSRF токена на странице входа${NC}"
|
||||||
|
csrf_token=$(curl -s http://localhost:8080/login | grep -o 'name="csrf_token"[^>]*value="[^"]*"' | sed 's/.*value="\([^"]*\)".*/\1/')
|
||||||
|
|
||||||
|
if [ -n "$csrf_token" ]; then
|
||||||
|
print_result "CSRF токен на странице входа" "PASS" "Токен найден: ${csrf_token:0:20}..."
|
||||||
|
else
|
||||||
|
print_result "CSRF токен на странице входа" "FAIL" "Токен не найден"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Тест 6: Попытка атаки с поддельным CSRF токеном
|
||||||
|
echo -e "\n${BLUE}🧪 Тест 6: Атака с поддельным CSRF токеном${NC}"
|
||||||
|
response=$(curl -s -w "%{http_code}" -X POST http://localhost:8080/api/execute \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-CSRF-Token: fake_token" \
|
||||||
|
-d '{"prompt":"whoami","system_id":"1"}' \
|
||||||
|
-o /dev/null)
|
||||||
|
|
||||||
|
if [ "$response" = "403" ]; then
|
||||||
|
print_result "CSRF защита от поддельного токена" "PASS" "Поддельный токен заблокирован (403 Forbidden)"
|
||||||
|
else
|
||||||
|
print_result "CSRF защита от поддельного токена" "FAIL" "Поддельный токен принят (HTTP $response)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Итоговый отчет
|
||||||
|
echo -e "\n${BLUE}📊 Итоговый отчет:${NC}"
|
||||||
|
echo "=================================="
|
||||||
|
|
||||||
|
# Подсчитываем результаты
|
||||||
|
total_tests=6
|
||||||
|
passed_tests=0
|
||||||
|
|
||||||
|
# Проверяем каждый тест
|
||||||
|
if curl -s -w "%{http_code}" -X POST http://localhost:8080/api/execute -H "Content-Type: application/json" -d '{"prompt":"test"}' -o /dev/null | grep -q "403"; then
|
||||||
|
((passed_tests++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
if curl -s -w "%{http_code}" -X POST http://localhost:8080/api/save-result -H "Content-Type: application/json" -d '{"result":"test"}' -o /dev/null | grep -q "403"; then
|
||||||
|
((passed_tests++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
if curl -s -w "%{http_code}" -X POST http://localhost:8080/api/add-to-history -H "Content-Type: application/json" -d '{"prompt":"test"}' -o /dev/null | grep -q "403"; then
|
||||||
|
((passed_tests++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
if curl -s -w "%{http_code}" http://localhost:8080/login -o /dev/null | grep -q "200"; then
|
||||||
|
((passed_tests++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
if curl -s http://localhost:8080/login | grep -q 'name="csrf_token"'; then
|
||||||
|
((passed_tests++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
if curl -s -w "%{http_code}" -X POST http://localhost:8080/api/execute -H "Content-Type: application/json" -H "X-CSRF-Token: fake" -d '{"prompt":"test"}' -o /dev/null | grep -q "403"; then
|
||||||
|
((passed_tests++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "Пройдено тестов: ${GREEN}$passed_tests${NC} из ${BLUE}$total_tests${NC}"
|
||||||
|
|
||||||
|
if [ $passed_tests -eq $total_tests ]; then
|
||||||
|
echo -e "${GREEN}🎉 Все тесты пройдены! CSRF защита работает корректно.${NC}"
|
||||||
|
exit 0
|
||||||
|
elif [ $passed_tests -ge 4 ]; then
|
||||||
|
echo -e "${YELLOW}⚠️ Большинство тестов пройдено, но есть проблемы с CSRF защитой.${NC}"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo -e "${RED}❌ Критические проблемы с CSRF защитой!${NC}"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
154
validation/validation.go
Normal file
154
validation/validation.go
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
package validation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ValidationError представляет ошибку валидации
|
||||||
|
type ValidationError struct {
|
||||||
|
Field string
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ValidationError) Error() string {
|
||||||
|
return fmt.Sprintf("%s: %s", e.Field, e.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateSystemPrompt проверяет длину системного промпта
|
||||||
|
func ValidateSystemPrompt(prompt string) error {
|
||||||
|
maxLen := config.AppConfig.Validation.MaxSystemPromptLength
|
||||||
|
if len(prompt) > maxLen {
|
||||||
|
return ValidationError{
|
||||||
|
Field: "system_prompt",
|
||||||
|
Message: fmt.Sprintf("системный промпт слишком длинный: %d символов (максимум %d)", len(prompt), maxLen),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateUserMessage проверяет длину пользовательского сообщения
|
||||||
|
func ValidateUserMessage(message string) error {
|
||||||
|
maxLen := config.AppConfig.Validation.MaxUserMessageLength
|
||||||
|
if len(message) > maxLen {
|
||||||
|
return ValidationError{
|
||||||
|
Field: "user_message",
|
||||||
|
Message: fmt.Sprintf("пользовательское сообщение слишком длинное: %d символов (максимум %d)", len(message), maxLen),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidatePromptAndMessage проверяет и системный промпт, и пользовательское сообщение
|
||||||
|
func ValidatePromptAndMessage(systemPrompt, userMessage string) error {
|
||||||
|
if err := ValidateSystemPrompt(systemPrompt); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := ValidateUserMessage(userMessage); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TruncateSystemPrompt обрезает системный промпт до максимальной длины
|
||||||
|
func TruncateSystemPrompt(prompt string) string {
|
||||||
|
maxLen := config.AppConfig.Validation.MaxSystemPromptLength
|
||||||
|
if len(prompt) <= maxLen {
|
||||||
|
return prompt
|
||||||
|
}
|
||||||
|
return prompt[:maxLen]
|
||||||
|
}
|
||||||
|
|
||||||
|
// TruncateUserMessage обрезает пользовательское сообщение до максимальной длины
|
||||||
|
func TruncateUserMessage(message string) string {
|
||||||
|
maxLen := config.AppConfig.Validation.MaxUserMessageLength
|
||||||
|
if len(message) <= maxLen {
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
return message[:maxLen]
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSystemPromptLength возвращает длину системного промпта
|
||||||
|
func GetSystemPromptLength(prompt string) int {
|
||||||
|
return len(prompt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserMessageLength возвращает длину пользовательского сообщения
|
||||||
|
func GetUserMessageLength(message string) int {
|
||||||
|
return len(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormatLengthInfo форматирует информацию о длине для отображения
|
||||||
|
func FormatLengthInfo(systemPrompt, userMessage string) string {
|
||||||
|
systemLen := GetSystemPromptLength(systemPrompt)
|
||||||
|
userLen := GetUserMessageLength(userMessage)
|
||||||
|
maxSystemLen := config.AppConfig.Validation.MaxSystemPromptLength
|
||||||
|
maxUserLen := config.AppConfig.Validation.MaxUserMessageLength
|
||||||
|
|
||||||
|
var warnings []string
|
||||||
|
|
||||||
|
if systemLen > maxSystemLen {
|
||||||
|
warnings = append(warnings, fmt.Sprintf("⚠️ Системный промпт превышает лимит: %d/%d символов", systemLen, maxSystemLen))
|
||||||
|
}
|
||||||
|
|
||||||
|
if userLen > maxUserLen {
|
||||||
|
warnings = append(warnings, fmt.Sprintf("⚠️ Пользовательское сообщение превышает лимит: %d/%d символов", userLen, maxUserLen))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(warnings) == 0 {
|
||||||
|
return fmt.Sprintf("✅ Длины в пределах нормы: системный промпт %d/%d, сообщение %d/%d",
|
||||||
|
systemLen, maxSystemLen, userLen, maxUserLen)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(warnings, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidatePromptName проверяет длину названия промпта
|
||||||
|
func ValidatePromptName(name string) error {
|
||||||
|
maxLen := config.AppConfig.Validation.MaxPromptNameLength
|
||||||
|
if len(name) > maxLen {
|
||||||
|
return ValidationError{
|
||||||
|
Field: "prompt_name",
|
||||||
|
Message: fmt.Sprintf("название промпта слишком длинное: %d символов (максимум %d)", len(name), maxLen),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidatePromptDescription проверяет длину описания промпта
|
||||||
|
func ValidatePromptDescription(description string) error {
|
||||||
|
maxLen := config.AppConfig.Validation.MaxPromptDescLength
|
||||||
|
if len(description) > maxLen {
|
||||||
|
return ValidationError{
|
||||||
|
Field: "prompt_description",
|
||||||
|
Message: fmt.Sprintf("описание промпта слишком длинное: %d символов (максимум %d)", len(description), maxLen),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateCommand проверяет длину команды
|
||||||
|
func ValidateCommand(command string) error {
|
||||||
|
maxLen := config.AppConfig.Validation.MaxCommandLength
|
||||||
|
if len(command) > maxLen {
|
||||||
|
return ValidationError{
|
||||||
|
Field: "command",
|
||||||
|
Message: fmt.Sprintf("команда слишком длинная: %d символов (максимум %d)", len(command), maxLen),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateExplanation проверяет длину объяснения
|
||||||
|
func ValidateExplanation(explanation string) error {
|
||||||
|
maxLen := config.AppConfig.Validation.MaxExplanationLength
|
||||||
|
if len(explanation) > maxLen {
|
||||||
|
return ValidationError{
|
||||||
|
Field: "explanation",
|
||||||
|
Message: fmt.Sprintf("объяснение слишком длинное: %d символов (максимум %d)", len(explanation), maxLen),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user