diff --git a/API_GUIDE.md b/API_GUIDE.md new file mode 100644 index 0000000..dc23253 --- /dev/null +++ b/API_GUIDE.md @@ -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']}") +``` diff --git a/README.md b/README.md index 56e514c..b9c24d5 100644 --- a/README.md +++ b/README.md @@ -48,8 +48,10 @@ Clipboard support requires `xclip` or `xsel`. - `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 @@ -72,7 +74,9 @@ Clipboard support requires `xclip` or `xsel`. - `history list` — list history from JSON - `history view ` — view by index - `history delete ` — delete by index (re-numbering) -- `serve-result` — start HTTP server to browse saved results (`--port`, `--host`) +- `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 @@ -95,4 +99,64 @@ Files are saved to `LCG_RESULT_FOLDER` (default `~/.config/lcg/gpt_results`). - 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`. diff --git a/USAGE_GUIDE.md b/USAGE_GUIDE.md index f2ec67f..d385f73 100644 --- a/USAGE_GUIDE.md +++ b/USAGE_GUIDE.md @@ -79,6 +79,7 @@ lcg --file /path/to/context.txt "хочу вывести список дирек | `LCG_RESULT_HISTORY` | `$(LCG_RESULT_FOLDER)/lcg_history.json` | Путь к JSON‑истории запросов. | | `LCG_PROMPT_FOLDER` | `~/.config/lcg/gpt_sys_prompts` | Папка для хранения системных промптов. | | `LCG_NO_HISTORY` | пусто | Если `1`/`true` — полностью отключает запись/обновление истории. | +| `LCG_ALLOW_EXECUTION` | пусто | Если `1`/`true` — включает возможность выполнения команд через опцию `(e)` в меню действий. | | `LCG_SERVER_PORT` | `8080` | Порт для HTTP сервера просмотра результатов. | | `LCG_SERVER_HOST` | `localhost` | Хост для HTTP сервера просмотра результатов. | @@ -133,7 +134,7 @@ lcg [глобальные опции] <описание команды> - `lcg prompts add` (`-a`) — добавить пользовательский промпт (по шагам в интерактиве). - `lcg prompts delete ` (`-d`) — удалить пользовательский промпт по ID (>5). - `lcg test-prompt <описание>` (`-tp`): показать детали выбранного системного промпта и протестировать его на заданном описании. -- `lcg serve-result` (`serve`): запустить HTTP сервер для просмотра сохраненных результатов: +- `lcg serve`: запустить HTTP сервер для просмотра сохраненных результатов: - `--port, -p` — порт сервера (по умолчанию из `LCG_SERVER_PORT`) - `--host, -H` — хост сервера (по умолчанию из `LCG_SERVER_HOST`) @@ -218,7 +219,7 @@ lcg [глобальные опции] <описание команды> ### Веб-интерфейс управления -Через HTTP сервер (`lcg serve-result`) доступно полное управление промптами: +Через HTTP сервер (`lcg serve`) доступно полное управление промптами: - **Просмотр всех промптов** (встроенных и пользовательских) - **Редактирование любых промптов** (включая встроенные) @@ -236,22 +237,22 @@ gpt_request__YYYY-MM-DD_HH-MM-SS.md ## HTTP сервер для просмотра результатов -Команда `lcg serve-result` запускает веб-сервер для удобного просмотра всех сохраненных результатов: +Команда `lcg serve` запускает веб-сервер для удобного просмотра всех сохраненных результатов: ```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 # Использование переменных окружения export LCG_SERVER_PORT=3000 export LCG_SERVER_HOST=0.0.0.0 -lcg serve-result +lcg serve ``` ### Возможности веб-интерфейса @@ -333,13 +334,13 @@ lcg models ```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 ``` ## История diff --git a/_main.go b/_main.go deleted file mode 100644 index 51d3b8e..0000000 --- a/_main.go +++ /dev/null @@ -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 -// } -// } diff --git a/cmd/serve.go b/cmd/serve.go deleted file mode 100644 index 766508f..0000000 --- a/cmd/serve.go +++ /dev/null @@ -1,1938 +0,0 @@ -package cmd - -import ( - "encoding/json" - "fmt" - "html/template" - "net/http" - "os" - "path/filepath" - "strconv" - "strings" - "time" - - "github.com/direct-dev-ru/linux-command-gpt/config" - "github.com/direct-dev-ru/linux-command-gpt/gpt" - "github.com/russross/blackfriday/v2" -) - -// StartResultServer запускает HTTP сервер для просмотра сохраненных результатов -func StartResultServer(host, port string) error { - http.HandleFunc("/", handleResultsPage) - http.HandleFunc("/file/", handleFileView) - http.HandleFunc("/delete/", handleDeleteFile) - http.HandleFunc("/history", handleHistoryPage) - http.HandleFunc("/history/delete/", handleDeleteHistoryEntry) - http.HandleFunc("/history/clear", handleClearHistory) - http.HandleFunc("/prompts", handlePromptsPage) - http.HandleFunc("/prompts/add", handleAddPrompt) - http.HandleFunc("/prompts/edit/", handleEditPrompt) - http.HandleFunc("/prompts/delete/", handleDeletePrompt) - http.HandleFunc("/prompts/restore/", handleRestorePrompt) - http.HandleFunc("/prompts/restore-verbose/", handleRestoreVerbosePrompt) - http.HandleFunc("/prompts/save-lang", handleSaveLang) - - addr := fmt.Sprintf("%s:%s", host, port) - fmt.Printf("Сервер запущен на http://%s\n", addr) - fmt.Println("Нажмите Ctrl+C для остановки") - - return http.ListenAndServe(addr, nil) -} - -// 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 := ` - - - - - - LCG Results - Linux Command GPT - - - -
-
-

🚀 LCG Results

-

Просмотр сохраненных результатов Linux Command GPT

-
-
-
- - 📝 История - ⚙️ Промпты -
- -
-
-
{{.TotalFiles}}
-
Всего файлов
-
-
-
{{.RecentFiles}}
-
За последние 7 дней
-
-
- - {{if .Files}} -
- {{range .Files}} -
-
- -
-
-
{{.Name}}
-
- 📅 {{.ModTime}} | 📏 {{.Size}} -
-
{{.Preview}}
-
-
- {{end}} -
- {{else}} -
-

📁 Папка пуста

-

Здесь будут отображаться сохраненные результаты после использования команды lcg

-
- {{end}} -
-
- - - -` - - 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 - }{ - Files: files, - TotalFiles: len(files), - RecentFiles: recentCount, - } - - w.Header().Set("Content-Type", "text/html; charset=utf-8") - t.Execute(w, data) -} - -// FileInfo содержит информацию о файле -type FileInfo struct { - Name string - Size string - ModTime string - Preview string -} - -// 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 символов) и конвертируем Markdown - preview := "" - if content, err := os.ReadFile(filepath.Join(config.AppConfig.ResultFolder, entry.Name())); err == nil { - // Конвертируем Markdown в HTML для превью - htmlContent := blackfriday.Run(content) - preview = strings.TrimSpace(string(htmlContent)) - // Удаляем HTML теги для превью - preview = strings.ReplaceAll(preview, "

", "") - preview = strings.ReplaceAll(preview, "

", "") - preview = strings.ReplaceAll(preview, "

", "") - preview = strings.ReplaceAll(preview, "

", "") - preview = strings.ReplaceAll(preview, "

", "") - preview = strings.ReplaceAll(preview, "

", "") - preview = strings.ReplaceAll(preview, "

", "") - preview = strings.ReplaceAll(preview, "

", "") - preview = strings.ReplaceAll(preview, "", "") - preview = strings.ReplaceAll(preview, "", "") - preview = strings.ReplaceAll(preview, "
", "")
-			preview = strings.ReplaceAll(preview, "
", "") - preview = strings.ReplaceAll(preview, "", "") - preview = strings.ReplaceAll(preview, "", "") - preview = strings.ReplaceAll(preview, "", "") - preview = strings.ReplaceAll(preview, "", "") - preview = strings.ReplaceAll(preview, "
    ", "") - preview = strings.ReplaceAll(preview, "
", "") - preview = strings.ReplaceAll(preview, "
  • ", "• ") - preview = strings.ReplaceAll(preview, "
  • ", "") - preview = strings.ReplaceAll(preview, "
      ", "") - preview = strings.ReplaceAll(preview, "
    ", "") - preview = strings.ReplaceAll(preview, "
    ", "") - preview = strings.ReplaceAll(preview, "
    ", "") - preview = strings.ReplaceAll(preview, "
    ", "") - preview = strings.ReplaceAll(preview, "
    ", "") - preview = strings.ReplaceAll(preview, "
    ", "") - - // Очищаем от лишних пробелов и переносов - 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: preview, - }) - } - - // Сортируем по времени изменения (новые сверху) - 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) - - // Создаем HTML страницу с красивым отображением - htmlPage := fmt.Sprintf(` - - - - - - %s - LCG Results - - - -
    - -
    - %s -
    -
    - -`, filename, filename, string(htmlContent)) - - // Устанавливаем заголовки для отображения HTML - w.Header().Set("Content-Type", "text/html; charset=utf-8") - w.Write([]byte(htmlPage)) -} - -// 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("Файл успешно удален")) -} - -// 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 := ` - - - - - - История запросов - LCG Results - - - -
    -
    -

    📝 История запросов

    -

    Управление историей запросов Linux Command GPT

    -
    -
    - - - {{if .Entries}} - {{range .Entries}} -
    -
    -
    - #{{.Index}} - {{.Timestamp}} -
    - -
    -
    {{.Command}}
    -
    {{.Response}}
    -
    - {{end}} - {{else}} -
    -

    📝 История пуста

    -

    Здесь будут отображаться запросы после использования команды lcg

    -
    - {{end}} -
    -
    - - - -` - - t, err := template.New("history").Parse(tmpl) - if err != nil { - http.Error(w, "Ошибка шаблона", http.StatusInternalServerError) - return - } - - data := struct { - Entries []HistoryEntryInfo - }{ - Entries: historyEntries, - } - - w.Header().Set("Content-Type", "text/html; charset=utf-8") - t.Execute(w, data) -} - -// HistoryEntryInfo содержит информацию о записи истории для отображения -type HistoryEntryInfo struct { - Index int - Command string - Response string - Timestamp string -} - -// readHistoryEntries читает записи истории -func readHistoryEntries() ([]HistoryEntryInfo, error) { - entries, err := read(config.AppConfig.ResultHistory) - if err != nil { - return nil, err - } - - 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 - } - - 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("История успешно очищена")) -} - -// 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 := ` - - - - - - Системные промпты - LCG Results - - - -
    -
    -

    ⚙️ Системные промпты

    -

    Управление системными промптами Linux Command GPT

    -
    -
    - - - -
    - - -
    - - -
    - {{if .Prompts}} - {{range .Prompts}} -
    -
    - - - -
    -
    -
    - #{{.ID}} - {{.Name}} - {{if .IsDefault}}Встроенный{{end}} -
    -
    -
    {{.Description}}
    -
    {{.Content}}
    -
    - {{end}} - {{else}} -
    -

    ⚙️ Промпты не найдены

    -

    Добавьте пользовательские промпты для настройки поведения системы

    -
    - {{end}} -
    - - -
    - {{if .VerbosePrompts}} - {{range .VerbosePrompts}} -
    -
    - - -
    -
    -
    - #{{.Mode}} - {{.Name}} - {{if .IsDefault}}Встроенный{{end}} -
    -
    -
    {{.Description}}
    -
    {{.Content}}
    -
    - {{end}} - {{else}} -
    -

    📝 Промпты подробности

    -

    Промпты для режимов v, vv, vvv

    -
    - {{end}} -
    -
    -
    - - - - - - -` - - 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 - }{ - Prompts: promptsWithDefault, - VerbosePrompts: verbosePrompts, - Lang: lang, - } - - 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 := 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 := 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("Промпт успешно удален")) -} - -// VerbosePrompt структура для промптов подробности -type VerbosePrompt struct { - Mode string - Name string - Description string - Content string - IsDefault bool -} - -// 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 -} - -// 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("Промпты сохранены")) -} - -// 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, - }) -} diff --git a/config/config.go b/config/config.go index e45334f..cfc3fe4 100644 --- a/config/config.go +++ b/config/config.go @@ -7,23 +7,24 @@ import ( ) type Config struct { - Cwd string - Host string - ProxyUrl string - Completions string - Model string - Prompt string - ApiKeyFile string - ResultFolder string - PromptFolder string - ProviderType string - JwtToken string - PromptID string - Timeout string - ResultHistory string - NoHistoryEnv string - MainFlags MainFlags - Server ServerConfig + Cwd string + Host string + ProxyUrl string + Completions string + Model string + Prompt string + ApiKeyFile string + ResultFolder string + PromptFolder string + ProviderType string + JwtToken string + PromptID string + Timeout string + ResultHistory string + NoHistoryEnv string + AllowExecution bool + MainFlags MainFlags + Server ServerConfig } type MainFlags struct { @@ -61,21 +62,22 @@ func Load() Config { promptFolder := getEnv("LCG_PROMPT_FOLDER", path.Join(homedir, ".config", "lcg", "gpt_sys_prompts")) return Config{ - Cwd: cwd, - Host: getEnv("LCG_HOST", "http://192.168.87.108:11434/"), - ProxyUrl: getEnv("LCG_PROXY_URL", "/api/v1/protected/sberchat/chat"), - Completions: getEnv("LCG_COMPLETIONS_PATH", "api/chat"), - Model: getEnv("LCG_MODEL", "hf.co/yandex/YandexGPT-5-Lite-8B-instruct-GGUF:Q4_K_M"), - 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."), - ApiKeyFile: getEnv("LCG_API_KEY_FILE", ".openai_api_key"), - ResultFolder: resultFolder, - PromptFolder: promptFolder, - ProviderType: getEnv("LCG_PROVIDER", "ollama"), - JwtToken: getEnv("LCG_JWT_TOKEN", ""), - PromptID: getEnv("LCG_PROMPT_ID", "1"), - Timeout: getEnv("LCG_TIMEOUT", "300"), - ResultHistory: getEnv("LCG_RESULT_HISTORY", path.Join(resultFolder, "lcg_history.json")), - NoHistoryEnv: getEnv("LCG_NO_HISTORY", ""), + Cwd: cwd, + Host: getEnv("LCG_HOST", "http://192.168.87.108:11434/"), + ProxyUrl: getEnv("LCG_PROXY_URL", "/api/v1/protected/sberchat/chat"), + Completions: getEnv("LCG_COMPLETIONS_PATH", "api/chat"), + Model: getEnv("LCG_MODEL", "hf.co/yandex/YandexGPT-5-Lite-8B-instruct-GGUF:Q4_K_M"), + 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."), + ApiKeyFile: getEnv("LCG_API_KEY_FILE", ".openai_api_key"), + ResultFolder: resultFolder, + PromptFolder: promptFolder, + ProviderType: getEnv("LCG_PROVIDER", "ollama"), + JwtToken: getEnv("LCG_JWT_TOKEN", ""), + PromptID: getEnv("LCG_PROMPT_ID", "1"), + Timeout: getEnv("LCG_TIMEOUT", "300"), + ResultHistory: getEnv("LCG_RESULT_HISTORY", path.Join(resultFolder, "lcg_history.json")), + NoHistoryEnv: getEnv("LCG_NO_HISTORY", ""), + AllowExecution: isAllowExecutionEnabled(), Server: ServerConfig{ Port: getEnv("LCG_SERVER_PORT", "8080"), Host: getEnv("LCG_SERVER_HOST", "localhost"), @@ -92,6 +94,15 @@ func (c Config) IsNoHistoryEnabled() bool { 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" +} + var AppConfig Config func init() { diff --git a/gpt/prompts.go b/gpt/prompts.go index 6a2c0c0..5fa19a3 100644 --- a/gpt/prompts.go +++ b/gpt/prompts.go @@ -53,13 +53,11 @@ func NewPromptManager(homeDir string) *PromptManager { // createInitialPromptsFile создает начальный файл с системными промптами и промптами подробности func (pm *PromptManager) createInitialPromptsFile() { - // Загружаем все встроенные промпты из YAML (английские по умолчанию) - pm.Prompts = GetBuiltinPrompts() + // Устанавливаем язык по умолчанию как русский + pm.Language = "ru" - // Фикс: при первичном сохранении явно выставляем язык файла - if pm.Language == "" { - pm.Language = "en" - } + // Загружаем все встроенные промпты из YAML на русском языке + pm.Prompts = GetBuiltinPromptsByLanguage("ru") // Сохраняем все промпты в файл pm.saveAllPrompts() @@ -379,3 +377,27 @@ func truncateString(s string, maxLen int) string { } 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 "" +} diff --git a/main.go b/main.go index f69b27d..9d38065 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,7 @@ import ( "os" "os/exec" "os/user" + "path/filepath" "strconv" "strings" "time" @@ -16,6 +17,7 @@ import ( "github.com/direct-dev-ru/linux-command-gpt/config" "github.com/direct-dev-ru/linux-command-gpt/gpt" "github.com/direct-dev-ru/linux-command-gpt/reader" + "github.com/direct-dev-ru/linux-command-gpt/serve" "github.com/urfave/cli/v2" ) @@ -517,9 +519,8 @@ func getCommands() []*cli.Command { }, }, { - Name: "serve-result", - Aliases: []string{"serve"}, - Usage: "Start HTTP server to browse saved results", + Name: "serve", + Usage: "Start HTTP server to browse saved results", Flags: []cli.Flag{ &cli.StringFlag{ Name: "port", @@ -533,16 +534,42 @@ func getCommands() []*cli.Command { Usage: "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 { port := c.String("port") host := c.String("host") + openBrowser := c.Bool("browser") + + // Пробрасываем глобальный флаг debug для web-сервера + // Позволяет запускать: lcg -d serve -p ... + if c.Bool("debug") { + config.AppConfig.MainFlags.Debug = true + } printColored(fmt.Sprintf("🌐 Запускаю HTTP сервер на %s:%s\n", host, port), colorCyan) printColored(fmt.Sprintf("📁 Папка результатов: %s\n", config.AppConfig.ResultFolder), colorYellow) - printColored(fmt.Sprintf("🔗 Откройте в браузере: http://%s:%s\n", host, port), colorGreen) - return cmdPackage.StartResultServer(host, port) + url := fmt.Sprintf("http://%s:%s", host, port) + + 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) }, }, } @@ -668,7 +695,14 @@ func getCommand(gpt3 gpt.Gpt3, cmd string) (string, float64) { } 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 fmt.Scanln(&choice) @@ -700,13 +734,17 @@ func handlePostResponse(response string, gpt3 gpt.Gpt3, system, cmd string, time fmt.Println("🔄 Перегенерирую...") executeMain("", system, cmd, timeout) case "e": - executeCommand(response) - if !disableHistory { - if fromHistory { - cmdPackage.SaveToHistoryFromHistory(config.AppConfig.ResultHistory, config.AppConfig.ResultFolder, cmd, response, gpt3.Prompt, explanation) - } else { - cmdPackage.SaveToHistory(config.AppConfig.ResultHistory, config.AppConfig.ResultFolder, cmd, response, gpt3.Prompt) + if config.AppConfig.AllowExecution { + executeCommand(response) + if !disableHistory { + if fromHistory { + cmdPackage.SaveToHistoryFromHistory(config.AppConfig.ResultHistory, config.AppConfig.ResultFolder, cmd, response, gpt3.Prompt, explanation) + } else { + cmdPackage.SaveToHistory(config.AppConfig.ResultHistory, config.AppConfig.ResultFolder, cmd, response, gpt3.Prompt) + } } + } else { + fmt.Println("⚠️ Выполнение команд отключено. Установите LCG_ALLOW_EXECUTION=1 для включения этой функции.") } case "v", "vv", "vvv": level := len(choice) // 1, 2, 3 @@ -782,7 +820,9 @@ func showTips() { fmt.Println(" • Команда 'history list' покажет историю запросов") fmt.Println(" • Команда 'config' покажет текущие настройки") fmt.Println(" • Команда 'health' проверит доступность API") - fmt.Println(" • Команда 'serve-result' запустит HTTP сервер для просмотра результатов") + fmt.Println(" • Команда 'serve' запустит HTTP сервер для просмотра результатов") + fmt.Println(" • Используйте --browser для автоматического открытия браузера") + fmt.Println(" • Установите LCG_BROWSER_PATH для указания конкретного браузера") } // printDebugInfo выводит отладочную информацию о параметрах запроса @@ -798,3 +838,49 @@ func printDebugInfo(file, system, commandInput string, timeout int) { fmt.Printf("📝 История: %t\n", !config.AppConfig.MainFlags.NoHistory) 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("не найден ни один из поддерживаемых браузеров") +} diff --git a/serve/README.md b/serve/README.md new file mode 100644 index 0000000..1253f11 --- /dev/null +++ b/serve/README.md @@ -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 +- **История**: Поиск дубликатов с учетом регистра +- **Промпты**: Управление встроенными и пользовательскими промптами diff --git a/serve/api.go b/serve/api.go new file mode 100644 index 0000000..d121974 --- /dev/null +++ b/serve/api.go @@ -0,0 +1,205 @@ +package serve + +import ( + "encoding/json" + "fmt" + "net/http" + "os" + "path" + "strings" + "time" + + "github.com/direct-dev-ru/linux-command-gpt/config" +) + +// 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 := 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 + } + + // Проверяем, есть ли уже такой запрос в истории + 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]) + " ..." +} diff --git a/serve/debug.go b/serve/debug.go new file mode 100644 index 0000000..d101ed1 --- /dev/null +++ b/serve/debug.go @@ -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") +} diff --git a/serve/execute.go b/serve/execute.go new file mode 100644 index 0000000..17af41f --- /dev/null +++ b/serve/execute.go @@ -0,0 +1,178 @@ +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" +) + +// 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 + } + + // Определяем системный промпт + systemPrompt := "" + if req.SystemText != "" { + 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 + } + systemPrompt = prompt.Content + } else { + // Используем промпт по умолчанию + 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) +} diff --git a/serve/execute_page.go b/serve/execute_page.go new file mode 100644 index 0000000..537d75b --- /dev/null +++ b/serve/execute_page.go @@ -0,0 +1,293 @@ +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/russross/blackfriday/v2" +) + +// ExecutePageData содержит данные для страницы выполнения +type ExecutePageData struct { + Title string + Header string + CurrentPrompt string + SystemOptions []SystemPromptOption + ResultSection template.HTML + VerboseButtons template.HTML + ActionButtons template.HTML +} + +// 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) + case http.MethodPost: + // Обрабатываем выполнение + handleExecuteRequest(w, r) + default: + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + } +} + +// showExecuteForm показывает форму выполнения +func showExecuteForm(w http.ResponseWriter) { + // Получаем системные промпты + 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(""), + } + + 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 + } + + 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 + } + + // Создаем 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, + }) + } + } + + data := ExecutePageData{ + Title: "Результат выполнения", + Header: "Результат выполнения", + CurrentPrompt: prompt, + SystemOptions: systemOptions, + ResultSection: template.HTML(formatResultSection(result)), + VerboseButtons: template.HTML(formatVerboseButtons(result)), + ActionButtons: template.HTML(formatActionButtons(result)), + } + + 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(` +
    +
    +

    ❌ Ошибка

    +

    %s

    +
    +
    `, result.Error) + } + + explanationSection := "" + if result.Explanation != "" { + explanationSection = fmt.Sprintf(` +
    +

    📖 Подробное объяснение (%s):

    +
    %s
    +
    + `, 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(`
    %s
    `, string(cmdHTML)) + } else { + // Оставляем как простой однострочный вывод команды + commandBlock = fmt.Sprintf(`
    %s
    `, result.Command) + } + + return fmt.Sprintf(` +
    +
    +

    ✅ Команда:

    + %s +
    + Модель: %s + Время: %.2f сек +
    +
    + %s +
    + `, + 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 ` +
    + + + +
    ` +} + +// formatActionButtons форматирует кнопки действий +func formatActionButtons(result ExecuteResultData) string { + if !result.Success { + return "" // Скрываем кнопки если есть ошибка + } + + return ` +
    + + +
    ` +} diff --git a/serve/history.go b/serve/history.go new file mode 100644 index 0000000..f62e11c --- /dev/null +++ b/serve/history.go @@ -0,0 +1,168 @@ +package serve + +import ( + "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/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 + }{ + Entries: historyEntries, + } + + 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 + } + + 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 + } + + 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 + 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(` +
    +

    📖 Подробное объяснение:

    +
    %s
    +
    `, string(explanationHTML)) + } + + // Создаем HTML страницу + htmlPage := fmt.Sprintf(templates.HistoryViewTemplate, + index, // title + index, // header + targetEntry.Timestamp.Format("02.01.2006 15:04:05"), // timestamp + index, // meta index + targetEntry.Command, // command + targetEntry.Response, // response + explanationSection, // explanation (if exists) + index, // delete button index + ) + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Write([]byte(htmlPage)) +} diff --git a/serve/history_utils.go b/serve/history_utils.go new file mode 100644 index 0000000..71a99b3 --- /dev/null +++ b/serve/history_utils.go @@ -0,0 +1,67 @@ +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 || len(data) == 0 { + return nil, err + } + 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) +} diff --git a/serve/prompts.go b/serve/prompts.go new file mode 100644 index 0000000..8b576a6 --- /dev/null +++ b/serve/prompts.go @@ -0,0 +1,421 @@ +package serve + +import ( + "encoding/json" + "fmt" + "html/template" + "net/http" + "os" + "strconv" + "strings" + + "github.com/direct-dev-ru/linux-command-gpt/gpt" + "github.com/direct-dev-ru/linux-command-gpt/serve/templates" +) + +// 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 + }{ + Prompts: promptsWithDefault, + VerbosePrompts: verbosePrompts, + Lang: lang, + } + + 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 := 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 := 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("Промпты сохранены")) +} diff --git a/serve/prompts_helpers.go b/serve/prompts_helpers.go new file mode 100644 index 0000000..9319cb3 --- /dev/null +++ b/serve/prompts_helpers.go @@ -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 +} diff --git a/serve/results.go b/serve/results.go new file mode 100644 index 0000000..b238e21 --- /dev/null +++ b/serve/results.go @@ -0,0 +1,239 @@ +package serve + +import ( + "fmt" + "html/template" + "net/http" + "os" + "path/filepath" + "strings" + "time" + + "github.com/direct-dev-ru/linux-command-gpt/config" + "github.com/direct-dev-ru/linux-command-gpt/serve/templates" + "github.com/russross/blackfriday/v2" +) + +// FileInfo содержит информацию о файле +type FileInfo struct { + Name string + Size string + ModTime string + Preview string + 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 + }{ + Files: files, + TotalFiles: len(files), + RecentFiles: recentCount, + } + + 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 символов) и конвертируем Markdown + preview := "" + fullContent := "" + if content, err := os.ReadFile(filepath.Join(config.AppConfig.ResultFolder, entry.Name())); err == nil { + // Сохраняем полное содержимое для поиска + fullContent = string(content) + // Конвертируем Markdown в HTML для превью + htmlContent := blackfriday.Run(content) + preview = strings.TrimSpace(string(htmlContent)) + // Удаляем HTML теги для превью + preview = strings.ReplaceAll(preview, "

    ", "") + preview = strings.ReplaceAll(preview, "

    ", "") + preview = strings.ReplaceAll(preview, "

    ", "") + preview = strings.ReplaceAll(preview, "

    ", "") + preview = strings.ReplaceAll(preview, "

    ", "") + preview = strings.ReplaceAll(preview, "

    ", "") + preview = strings.ReplaceAll(preview, "

    ", "") + preview = strings.ReplaceAll(preview, "

    ", "") + preview = strings.ReplaceAll(preview, "", "") + preview = strings.ReplaceAll(preview, "", "") + preview = strings.ReplaceAll(preview, "
    ", "")
    +			preview = strings.ReplaceAll(preview, "
    ", "") + preview = strings.ReplaceAll(preview, "", "") + preview = strings.ReplaceAll(preview, "", "") + preview = strings.ReplaceAll(preview, "", "") + preview = strings.ReplaceAll(preview, "", "") + preview = strings.ReplaceAll(preview, "
      ", "") + preview = strings.ReplaceAll(preview, "
    ", "") + preview = strings.ReplaceAll(preview, "
  • ", "• ") + preview = strings.ReplaceAll(preview, "
  • ", "") + preview = strings.ReplaceAll(preview, "
      ", "") + preview = strings.ReplaceAll(preview, "
    ", "") + preview = strings.ReplaceAll(preview, "
    ", "") + preview = strings.ReplaceAll(preview, "
    ", "") + preview = strings.ReplaceAll(preview, "
    ", "") + preview = strings.ReplaceAll(preview, "
    ", "") + preview = strings.ReplaceAll(preview, "
    ", "") + + // Очищаем от лишних пробелов и переносов + 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: 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) + + // Создаем HTML страницу с красивым отображением + htmlPage := fmt.Sprintf(templates.FileViewTemplate, filename, filename, string(htmlContent)) + + // Устанавливаем заголовки для отображения HTML + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Write([]byte(htmlPage)) +} + +// 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("Файл успешно удален")) +} diff --git a/serve/serve.go b/serve/serve.go new file mode 100644 index 0000000..aa2dbe0 --- /dev/null +++ b/serve/serve.go @@ -0,0 +1,59 @@ +package serve + +import ( + "fmt" + "net/http" + + "github.com/direct-dev-ru/linux-command-gpt/config" +) + +// StartResultServer запускает HTTP сервер для просмотра сохраненных результатов +func StartResultServer(host, port string) error { + // Регистрируем все маршруты + registerRoutes() + + addr := fmt.Sprintf("%s:%s", host, port) + fmt.Printf("Сервер запущен на http://%s\n", addr) + fmt.Println("Нажмите Ctrl+C для остановки") + + // Тестовое логирование для проверки debug флага + if config.AppConfig.MainFlags.Debug { + fmt.Printf("🔍 DEBUG РЕЖИМ ВКЛЮЧЕН - веб-операции будут логироваться\n") + } else { + fmt.Printf("🔍 DEBUG РЕЖИМ ОТКЛЮЧЕН - веб-операции не будут логироваться\n") + } + + return http.ListenAndServe(addr, nil) +} + +// registerRoutes регистрирует все маршруты сервера +func registerRoutes() { + // Главная страница и файлы + http.HandleFunc("/", handleResultsPage) + http.HandleFunc("/file/", handleFileView) + http.HandleFunc("/delete/", handleDeleteFile) + + // История запросов + http.HandleFunc("/history", handleHistoryPage) + http.HandleFunc("/history/view/", handleHistoryView) + http.HandleFunc("/history/delete/", handleDeleteHistoryEntry) + http.HandleFunc("/history/clear", handleClearHistory) + + // Управление промптами + http.HandleFunc("/prompts", handlePromptsPage) + http.HandleFunc("/prompts/add", handleAddPrompt) + http.HandleFunc("/prompts/edit/", handleEditPrompt) + http.HandleFunc("/prompts/delete/", handleDeletePrompt) + http.HandleFunc("/prompts/restore/", handleRestorePrompt) + http.HandleFunc("/prompts/restore-verbose/", handleRestoreVerbosePrompt) + http.HandleFunc("/prompts/save-lang", handleSaveLang) + + // Веб-страница для выполнения запросов + http.HandleFunc("/run", handleExecutePage) + + // API для выполнения запросов + http.HandleFunc("/api/execute", handleExecute) + // API для сохранения результатов и истории + http.HandleFunc("/api/save-result", handleSaveResult) + http.HandleFunc("/api/add-to-history", handleAddToHistory) +} diff --git a/serve/templates/execute.css.go b/serve/templates/execute.css.go new file mode 100644 index 0000000..1eb627f --- /dev/null +++ b/serve/templates/execute.css.go @@ -0,0 +1,600 @@ +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; + } + .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; + } + } +`)) \ No newline at end of file diff --git a/serve/templates/execute.go b/serve/templates/execute.go new file mode 100644 index 0000000..b610061 --- /dev/null +++ b/serve/templates/execute.go @@ -0,0 +1,88 @@ +package templates + +import "html/template" + +// ExecutePageTemplate - шаблон страницы выполнения запросов +var ExecutePageTemplate = template.Must(template.New("execute").Parse(` + + + + + {{.Title}} - Linux Command GPT + + + +
    +
    +

    {{.Header}}

    +

    Выполнение запросов к Linux Command GPT через веб-интерфейс

    +
    +
    + + +
    +
    +
    + + +
    + +
    + + +
    + + + + +
    + + +
    +
    +
    + +
    +
    +

    Обрабатываю запрос...

    +
    + + {{.ResultSection}} + + {{.VerboseButtons}} + +
    +
    +

    Получаю подробное объяснение...

    +
    + + {{.ActionButtons}} +
    +
    + + + + + {{template "execute_scripts" .}} + +`)) + +// Объединяем шаблоны +func init() { + template.Must(ExecutePageTemplate.AddParseTree("execute_css", ExecutePageCSSTemplate.Tree)) + template.Must(ExecutePageTemplate.AddParseTree("execute_scripts", ExecutePageScriptsTemplate.Tree)) +} diff --git a/serve/templates/execute.js.go b/serve/templates/execute.js.go new file mode 100644 index 0000000..7131893 --- /dev/null +++ b/serve/templates/execute.js.go @@ -0,0 +1,284 @@ +package templates + +import "html/template" + +var ExecutePageScriptsTemplate = template.Must(template.New("execute_scripts").Parse(` + +`)) \ No newline at end of file diff --git a/serve/templates/file.go b/serve/templates/file.go new file mode 100644 index 0000000..9879c38 --- /dev/null +++ b/serve/templates/file.go @@ -0,0 +1,136 @@ +package templates + +// FileViewTemplate шаблон для просмотра файла результата +const FileViewTemplate = ` + + + + + + %s - LCG Results + + + +
    + +
    + %s +
    +
    + +` diff --git a/serve/templates/history.go b/serve/templates/history.go new file mode 100644 index 0000000..3d9e903 --- /dev/null +++ b/serve/templates/history.go @@ -0,0 +1,309 @@ +package templates + +// HistoryPageTemplate шаблон страницы истории запросов +const HistoryPageTemplate = ` + + + + + + История запросов - LCG Results + + + +
    +
    +

    📝 История запросов

    +

    Управление историей запросов Linux Command GPT

    +
    +
    + + + +
    + +
    +
    + + {{if .Entries}} + {{range .Entries}} +
    +
    +
    + #{{.Index}} + {{.Timestamp}} +
    + +
    +
    {{.Command}}
    +
    {{.Response}}
    +
    + {{end}} + {{else}} +
    +

    📝 История пуста

    +

    Здесь будут отображаться запросы после использования команды lcg

    +
    + {{end}} +
    +
    + + + +` diff --git a/serve/templates/history_view.go b/serve/templates/history_view.go new file mode 100644 index 0000000..b5f601b --- /dev/null +++ b/serve/templates/history_view.go @@ -0,0 +1,279 @@ +package templates + +// HistoryViewTemplate шаблон для просмотра записи истории +const HistoryViewTemplate = ` + + + + + + Запись #%d - LCG History + + + +
    +
    +

    📝 Запись #%d

    + ← Назад к истории +
    +
    +
    +
    + 📅 Время: %s +
    +
    + 🔢 Индекс: #%d +
    +
    + +
    +

    💬 Запрос пользователя:

    +
    %s
    +
    + +
    +

    🤖 Ответ Модели:

    +
    %s
    +
    + + %s + +
    + 📝 К истории + +
    +
    +
    + + + +` diff --git a/serve/templates/prompts.go b/serve/templates/prompts.go new file mode 100644 index 0000000..a35b74f --- /dev/null +++ b/serve/templates/prompts.go @@ -0,0 +1,515 @@ +package templates + +// PromptsPageTemplate шаблон страницы управления промптами +const PromptsPageTemplate = ` + + + + + + Системные промпты - LCG Results + + + +
    +
    +

    ⚙️ Системные промпты

    +

    Управление системными промптами Linux Command GPT

    +
    +
    + + + +
    + + +
    + + +
    + {{if .Prompts}} + {{range .Prompts}} +
    +
    + + + +
    +
    +
    + #{{.ID}} + {{.Name}} + {{if .IsDefault}}Встроенный{{end}} +
    +
    +
    {{.Description}}
    +
    {{.Content}}
    +
    + {{end}} + {{else}} +
    +

    ⚙️ Промпты не найдены

    +

    Добавьте пользовательские промпты для настройки поведения системы

    +
    + {{end}} +
    + + +
    + {{if .VerbosePrompts}} + {{range .VerbosePrompts}} +
    +
    + + +
    +
    +
    + #{{.Mode}} + {{.Name}} + {{if .IsDefault}}Встроенный{{end}} +
    +
    +
    {{.Description}}
    +
    {{.Content}}
    +
    + {{end}} + {{else}} +
    +

    📝 Промпты подробности

    +

    Промпты для режимов v, vv, vvv

    +
    + {{end}} +
    +
    +
    + + + + + + +` diff --git a/serve/templates/results.go b/serve/templates/results.go new file mode 100644 index 0000000..4a21784 --- /dev/null +++ b/serve/templates/results.go @@ -0,0 +1,329 @@ +package templates + +// ResultsPageTemplate шаблон главной страницы со списком файлов +const ResultsPageTemplate = ` + + + + + + LCG Results - Linux Command GPT + + + +
    +
    +

    🚀 LCG Results

    +

    Просмотр сохраненных результатов Linux Command GPT

    +
    +
    + + + +
    + +
    +
    + +
    +
    +
    {{.TotalFiles}}
    +
    Всего файлов
    +
    +
    +
    {{.RecentFiles}}
    +
    За последние 7 дней
    +
    +
    + + {{if .Files}} +
    + {{range .Files}} +
    +
    + +
    +
    +
    {{.Name}}
    +
    + 📅 {{.ModTime}} | 📏 {{.Size}} +
    +
    {{.Preview}}
    +
    +
    + {{end}} +
    + {{else}} +
    +

    📁 Папка пуста

    +

    Здесь будут отображаться сохраненные результаты после использования команды lcg

    +
    + {{end}} +
    +
    + + + +` diff --git a/shell-code/curl.sh b/shell-code/curl.sh new file mode 100644 index 0000000..8dcedff --- /dev/null +++ b/shell-code/curl.sh @@ -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" \ No newline at end of file