diff --git a/USAGE_GUIDE.md b/USAGE_GUIDE.md index 910bf7a..5be1950 100644 --- a/USAGE_GUIDE.md +++ b/USAGE_GUIDE.md @@ -2,7 +2,7 @@ ## Что это -Linux Command GPT (`lcg`) преобразует описание на естественном языке в готовую Linux‑команду. Инструмент поддерживает сменные провайдеры LLM (Ollama или Proxy), управление системными промптами, историю за сессию, сохранение результатов и интерактивные действия над сгенерированной командой. +Linux Command GPT (`lcg`) преобразует описание на естественном языке в готовую Linux‑команду. Инструмент поддерживает сменные провайдеры LLM (Ollama или Proxy), управление системными промптами, историю запросов, сохранение результатов, HTTP сервер для просмотра результатов и интерактивные действия над сгенерированной командой. ## Требования @@ -66,17 +66,21 @@ lcg --file /path/to/context.txt "хочу вывести список дирек | Переменная | Значение по умолчанию | Назначение | | --- | --- | --- | | `LCG_HOST` | `http://192.168.87.108:11434/` | Базовый URL API провайдера (для Ollama поставьте, например, `http://localhost:11434/`). | +| `LCG_PROXY_URL` | `/api/v1/protected/sberchat/chat` | Относительный путь эндпоинта для Proxy провайдера. | | `LCG_COMPLETIONS_PATH` | `api/chat` | Относительный путь эндпоинта для Ollama. | -| `LCG_MODEL` | `codegeex4` | Имя модели у выбранного провайдера. | +| `LCG_MODEL` | `hf.co/yandex/YandexGPT-5-Lite-8B-instruct-GGUF:Q4_K_M` | Имя модели у выбранного провайдера. | | `LCG_PROMPT` | См. значение в коде | Содержимое системного промпта по умолчанию. | | `LCG_API_KEY_FILE` | `.openai_api_key` | Файл с API‑ключом (для Ollama/Proxy не требуется). | -| `LCG_RESULT_FOLDER` | `$(pwd)/gpt_results` | Папка для сохранения результатов. | +| `LCG_RESULT_FOLDER` | `~/.config/lcg/gpt_results` | Папка для сохранения результатов. | | `LCG_PROVIDER` | `ollama` | Тип провайдера: `ollama` или `proxy`. | | `LCG_JWT_TOKEN` | пусто | JWT токен для `proxy` провайдера (альтернатива — файл `~/.proxy_jwt_token`). | | `LCG_PROMPT_ID` | `1` | ID системного промпта по умолчанию. | -| `LCG_TIMEOUT` | `120` | Таймаут запроса в секундах. | +| `LCG_TIMEOUT` | `300` | Таймаут запроса в секундах. | | `LCG_RESULT_HISTORY` | `$(LCG_RESULT_FOLDER)/lcg_history.json` | Путь к JSON‑истории запросов. | +| `LCG_PROMPT_FOLDER` | `~/.config/lcg/gpt_sys_prompts` | Папка для хранения системных промптов. | | `LCG_NO_HISTORY` | пусто | Если `1`/`true` — полностью отключает запись/обновление истории. | +| `LCG_SERVER_PORT` | `8080` | Порт для HTTP сервера просмотра результатов. | +| `LCG_SERVER_HOST` | `localhost` | Хост для HTTP сервера просмотра результатов. | Примеры настройки: @@ -104,7 +108,9 @@ lcg [глобальные опции] <описание команды> - `--file, -f string` — прочитать часть запроса из файла и добавить к описанию. - `--sys, -s string` — системный промпт (содержимое или ID как строка). Если не задан, используется `--prompt-id` или `LCG_PROMPT`. - `--prompt-id, --pid int` — ID системного промпта (1–5 для стандартных, либо ваш кастомный ID). -- `--timeout, -t int` — таймаут запроса в секундах (по умолчанию 120). +- `--timeout, -t int` — таймаут запроса в секундах (по умолчанию 300). +- `--no-history, --nh` — отключить запись/обновление истории для текущего запуска. +- `--debug, -d` — показать отладочную информацию (параметры запроса и промпты). - `--version, -v` — вывести версию. - `--help, -h` — помощь. @@ -122,10 +128,14 @@ lcg [глобальные опции] <описание команды> - `lcg history delete ` (`-d`): удалить запись истории по `index` (с перенумерацией). - Флаг `--no-history` (`-nh`) отключает запись истории для текущего запуска и имеет приоритет над `LCG_NO_HISTORY`. - `lcg prompts ...` (`-p`): управление системными промптами: - - `lcg prompts list` (`-l`) — список всех промптов. + - `lcg prompts list` (`-l`) — список всех промптов с содержимым в читаемом формате. + - `lcg prompts list --full` (`-f`) — полный вывод содержимого без обрезки длинных строк. - `lcg prompts add` (`-a`) — добавить пользовательский промпт (по шагам в интерактиве). - `lcg prompts delete ` (`-d`) — удалить пользовательский промпт по ID (>5). - `lcg test-prompt <описание>` (`-tp`): показать детали выбранного системного промпта и протестировать его на заданном описании. +- `lcg serve-result` (`serve`): запустить HTTP сервер для просмотра сохраненных результатов: + - `--port, -p` — порт сервера (по умолчанию из `LCG_SERVER_PORT`) + - `--host, -H` — хост сервера (по умолчанию из `LCG_SERVER_HOST`) ### Подробные объяснения (v/vv/vvv) @@ -165,7 +175,7 @@ lcg [глобальные опции] <описание команды> ### Таймауты -- Стартовые значения: локально с Ollama — **60–120 сек**, удалённый proxy — **120–300 сек**. +- Стартовые значения: локально с Ollama — **120–300 сек**, удалённый proxy — **300–600 сек**. - Увеличьте таймаут для больших моделей/длинных запросов. Флаг `--timeout` перекрывает `LCG_TIMEOUT` на время запуска. - Если часто видите таймауты — проверьте здоровье API (`lcg health`) и сетевую доступность `LCG_HOST`. @@ -178,7 +188,17 @@ lcg [глобальные опции] <описание команды> ## Системные промпты -Встроенные (ID 1–5): +### Управление промптами + +Системные промпты хранятся в папке, указанной в переменной `LCG_PROMPT_FOLDER` (по умолчанию: `~/.config/lcg/gpt_sys_prompts`). + +**Логика загрузки:** + +- Если файл `sys_prompts` **не существует** — создается файл с системными промптами (ID 1–5) и промптами подробности (ID 6–8) +- Если файл `sys_prompts` **существует** — загружаются все промпты из файла +- **Промпты подробности** (v/vv/vvv) сохраняются в том же файле с ID 6, 7, 8 + +### Встроенные промпты (ID 1–5) | ID | Name | Описание | | --- | --- | --- | @@ -188,16 +208,61 @@ lcg [глобальные опции] <описание команды> | 4 | linux-command-verbose | Команда с подробными объяснениями флагов и альтернатив. | | 5 | linux-command-simple | Простые команды, избегать сложных опций. | -Пользовательские промпты сохраняются в `~/.lcg_prompts.json` и доступны между запусками. +### Промпты подробности (ID 6–8) + +| ID | Name | Описание | +| --- | --- | --- | +| 6 | verbose-v | Подробный режим (v) - детальное объяснение команды | +| 7 | verbose-vv | Очень подробный режим (vv) - исчерпывающее объяснение с альтернативами | +| 8 | verbose-vvv | Максимально подробный режим (vvv) - полное руководство с примерами | + +### Веб-интерфейс управления + +Через HTTP сервер (`lcg serve-result`) доступно полное управление промптами: + +- **Просмотр всех промптов** (встроенных и пользовательских) +- **Редактирование любых промптов** (включая встроенные) +- **Добавление новых промптов** +- **Удаление промптов** +- **Автоматическое сохранение** в файл `sys_prompts` ## Сохранение результатов -При выборе действия `s` ответ сохраняется в `LCG_RESULT_FOLDER` (по умолчанию: `./gpt_results`) в файл вида: +При выборе действия `s` ответ сохраняется в `LCG_RESULT_FOLDER` (по умолчанию: `~/.config/lcg/gpt_results`) в файл вида: ```text gpt_request__YYYY-MM-DD_HH-MM-SS.md ``` +## HTTP сервер для просмотра результатов + +Команда `lcg serve-result` запускает веб-сервер для удобного просмотра всех сохраненных результатов: + +```bash +# Запуск с настройками по умолчанию +lcg serve-result + +# Запуск на другом порту +lcg serve-result --port 9090 + +# Запуск на другом хосте +lcg serve-result --host 0.0.0.0 --port 8080 + +# Использование переменных окружения +export LCG_SERVER_PORT=3000 +export LCG_SERVER_HOST=0.0.0.0 +lcg serve-result +``` + +### Возможности веб-интерфейса + +- **Главная страница** (`/`) — отображает все сохраненные файлы с превью +- **Статистика** — количество файлов, файлы за последние 7 дней +- **Просмотр файлов** (`/file/{filename}`) — отображение содержимого конкретного файла +- **Современный дизайн** — адаптивный интерфейс с карточками файлов +- **Сортировка** — файлы отсортированы по дате изменения (новые сверху) +- **Превью содержимого** — первые 200 символов каждого файла + Структура файла (команда): - `# <заголовок>` — H1, это ваш запрос, при длине >120 символов обрезается до 116 + `...`. @@ -264,6 +329,19 @@ lcg health lcg models ``` +1. HTTP сервер для просмотра результатов: + +```bash +# Запуск сервера +lcg serve-result + +# Запуск на другом порту +lcg serve-result --port 9090 + +# Запуск на всех интерфейсах +lcg serve-result --host 0.0.0.0 --port 8080 +``` + ## История `lcg history` выводит историю текущего процесса (не сохраняется между запусками, максимум 100 записей): @@ -279,6 +357,8 @@ lcg history list - Копирование не работает: установите `xclip` или `xsel`. - Нет допуска к папке результатов: настройте `LCG_RESULT_FOLDER` или права доступа. - Для `ollama`/`proxy` API‑ключ не нужен; команды `update-key`/`delete-key` просто уведомят об этом. +- HTTP сервер не запускается: проверьте, что порт свободен, используйте `--port` для смены порта. +- Веб-интерфейс не отображает файлы: убедитесь, что в `LCG_RESULT_FOLDER` есть `.md` файлы. ## JSON‑история запросов diff --git a/cmd/explain.go b/cmd/explain.go index 273e0f5..bf82b9d 100644 --- a/cmd/explain.go +++ b/cmd/explain.go @@ -25,17 +25,86 @@ type ExplainDeps struct { // ShowDetailedExplanation делает дополнительный запрос с подробным описанием и альтернативами func ShowDetailedExplanation(command string, gpt3 gpt.Gpt3, system, originalCmd string, timeout int, level int, deps ExplainDeps) { - var detailedSystem string - switch level { - case 1: // v — кратко - detailedSystem = "Ты опытный Linux-инженер. Объясни КРАТКО, по делу: что делает команда и самые важные ключи. Без сравнений и альтернатив. Минимум текста. Пиши на русском." - case 2: // vv — средне - detailedSystem = "Ты опытный Linux-инженер. Дай сбалансированное объяснение: назначение команды, разбор основных ключей, 1-2 примера. Кратко упомяни 1-2 альтернативы без глубокого сравнения. Пиши на русском." - default: // vvv — максимально подробно - detailedSystem = "Ты опытный Linux-инженер. Дай подробное объяснение команды с полным разбором ключей, подкоманд, сценариев применения, примеров. Затем предложи альтернативные способы решения задачи другой командой/инструментами (со сравнениями и когда что лучше применять). Пиши на русском." + // Получаем домашнюю директорию пользователя + homeDir, err := os.UserHomeDir() + if err != nil { + // Fallback к встроенным промптам + detailedSystem := getBuiltinVerbosePrompt(level) + ask := getBuiltinAsk(originalCmd, command) + processExplanation(detailedSystem, ask, gpt3, timeout, deps, originalCmd, command, system, level) + return } - ask := fmt.Sprintf("Объясни подробно команду и предложи альтернативы. Исходная команда: %s. Исходное задание пользователя: %s", command, originalCmd) + // Создаем менеджер промптов + pm := gpt.NewPromptManager(homeDir) + + // Получаем промпт подробности по уровню + verbosePrompt := getVerbosePromptByLevel(pm.Prompts, level) + + // Формируем ask в зависимости от языка + ask := getAskByLanguage(pm.GetCurrentLanguage(), originalCmd, command) + + processExplanation(verbosePrompt, ask, gpt3, timeout, deps, originalCmd, command, system, level) +} + +// getVerbosePromptByLevel возвращает промпт подробности по уровню +func getVerbosePromptByLevel(prompts []gpt.SystemPrompt, level int) string { + // Ищем промпт подробности по ID + for _, prompt := range prompts { + if prompt.ID >= 6 && prompt.ID <= 8 { + switch level { + case 1: // v + if prompt.ID == 6 { + return prompt.Content + } + case 2: // vv + if prompt.ID == 7 { + return prompt.Content + } + default: // vvv + if prompt.ID == 8 { + return prompt.Content + } + } + } + } + + // Fallback к встроенным промптам + return getBuiltinVerbosePrompt(level) +} + +// getBuiltinVerbosePrompt возвращает встроенный промпт подробности +func getBuiltinVerbosePrompt(level int) string { + switch level { + case 1: // v — кратко + return "Ты опытный Linux-инженер. Объясни КРАТКО, по делу: что делает команда и самые важные ключи. Без сравнений и альтернатив. Минимум текста. Пиши на русском." + case 2: // vv — средне + return "Ты опытный Linux-инженер. Дай сбалансированное объяснение: назначение команды, разбор основных ключей, 1-2 примера. Кратко упомяни 1-2 альтернативы без глубокого сравнения. Пиши на русском." + default: // vvv — максимально подробно + return "Ты опытный Linux-инженер. Дай подробное объяснение команды с полным разбором ключей, подкоманд, сценариев применения, примеров. Затем предложи альтернативные способы решения задачи другой командой/инструментами (со сравнениями и когда что лучше применять). Пиши на русском." + } +} + +// getAskByLanguage формирует ask в зависимости от языка +func getAskByLanguage(lang, originalCmd, command string) string { + if lang == "ru" { + return fmt.Sprintf("Объясни подробно команду и предложи альтернативы. Исходная команда: %s. Исходное задание пользователя: %s", command, originalCmd) + } + // Английский + return fmt.Sprintf("Explain the command in detail and suggest alternatives. Original command: %s. Original user request: %s", command, originalCmd) +} + +// getBuiltinAsk возвращает встроенный ask +func getBuiltinAsk(originalCmd, command string) string { + return fmt.Sprintf("Объясни подробно команду и предложи альтернативы. Исходная команда: %s. Исходное задание пользователя: %s", command, originalCmd) +} + +// processExplanation обрабатывает объяснение +func processExplanation(detailedSystem, ask string, gpt3 gpt.Gpt3, timeout int, deps ExplainDeps, originalCmd string, command string, system string, level int) { + // Выводим debug информацию если включен флаг + if config.AppConfig.MainFlags.Debug { + printVerboseDebugInfo(detailedSystem, ask, gpt3, timeout, level) + } detailed := gpt.NewGpt3(gpt3.ProviderType, config.AppConfig.Host, gpt3.ApiKey, gpt3.Model, detailedSystem, 0.2, timeout) deps.PrintColored("\n🧠 Получаю подробное объяснение...\n", deps.ColorPurple) @@ -105,3 +174,16 @@ func truncateTitle(s string) string { } return string(r[:head]) + " ..." } + +// printVerboseDebugInfo выводит отладочную информацию для режимов v/vv/vvv +func printVerboseDebugInfo(detailedSystem, ask string, gpt3 gpt.Gpt3, timeout int, level int) { + fmt.Printf("\n🔍 DEBUG VERBOSE (v%d):\n", level) + fmt.Printf("📝 Системный промпт подробности:\n%s\n", detailedSystem) + fmt.Printf("💬 Запрос подробности:\n%s\n", ask) + fmt.Printf("⏱️ Таймаут: %d сек\n", timeout) + fmt.Printf("🌐 Провайдер: %s\n", gpt3.ProviderType) + fmt.Printf("🏠 Хост: %s\n", config.AppConfig.Host) + fmt.Printf("🧠 Модель: %s\n", gpt3.Model) + fmt.Printf("🎯 Уровень подробности: %d\n", level) + fmt.Printf("────────────────────────────────────────\n") +} diff --git a/cmd/history.go b/cmd/history.go index 2b3b9ec..1c96835 100644 --- a/cmd/history.go +++ b/cmd/history.go @@ -137,6 +137,34 @@ func SaveToHistory(historyPath, resultFolder, cmdText, response, system string, return nil } +// SaveToHistoryFromHistory сохраняет запись из истории без запроса о перезаписи +func SaveToHistoryFromHistory(historyPath, resultFolder, cmdText, response, system, explanation string) error { + items, _ := read(historyPath) + duplicateIndex := -1 + for i, h := range items { + if strings.EqualFold(strings.TrimSpace(h.Command), strings.TrimSpace(cmdText)) { + duplicateIndex = i + break + } + } + entry := HistoryEntry{ + Index: len(items) + 1, + Command: cmdText, + Response: response, + Explanation: explanation, + System: system, + Timestamp: time.Now(), + } + if duplicateIndex == -1 { + items = append(items, entry) + return write(historyPath, items) + } + // Если дубликат найден, перезаписываем без запроса + entry.Index = items[duplicateIndex].Index + items[duplicateIndex] = entry + return write(historyPath, items) +} + func CheckAndSuggestFromHistory(historyPath, cmdText string) (bool, *HistoryEntry) { items, err := read(historyPath) if err != nil || len(items) == 0 { diff --git a/cmd/serve.go b/cmd/serve.go new file mode 100644 index 0000000..766508f --- /dev/null +++ b/cmd/serve.go @@ -0,0 +1,1938 @@ +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 50c6155..e45334f 100644 --- a/config/config.go +++ b/config/config.go @@ -15,6 +15,7 @@ type Config struct { Prompt string ApiKeyFile string ResultFolder string + PromptFolder string ProviderType string JwtToken string PromptID string @@ -22,6 +23,7 @@ type Config struct { ResultHistory string NoHistoryEnv string MainFlags MainFlags + Server ServerConfig } type MainFlags struct { @@ -30,6 +32,12 @@ type MainFlags struct { Sys string PromptID int Timeout int + Debug bool +} + +type ServerConfig struct { + Port string + Host string } func getEnv(key, defaultValue string) string { @@ -49,6 +57,9 @@ func Load() Config { os.MkdirAll(path.Join(homedir, ".config", "lcg", "gpt_results"), 0755) resultFolder := getEnv("LCG_RESULT_FOLDER", path.Join(homedir, ".config", "lcg", "gpt_results")) + os.MkdirAll(path.Join(homedir, ".config", "lcg", "gpt_sys_prompts"), 0755) + 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/"), @@ -58,12 +69,17 @@ func Load() Config { 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", ""), + Server: ServerConfig{ + Port: getEnv("LCG_SERVER_PORT", "8080"), + Host: getEnv("LCG_SERVER_HOST", "localhost"), + }, } } diff --git a/go.mod b/go.mod index c8ce165..938c77f 100644 --- a/go.mod +++ b/go.mod @@ -4,9 +4,11 @@ go 1.18 require github.com/atotto/clipboard v0.1.4 +require gopkg.in/yaml.v3 v3.0.1 + require ( - github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect - github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.5 //indirect + github.com/russross/blackfriday/v2 v2.1.0 github.com/urfave/cli/v2 v2.27.5 github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect ) diff --git a/go.sum b/go.sum index 21547c9..f795117 100644 --- a/go.sum +++ b/go.sum @@ -8,3 +8,6 @@ github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/gpt/builtin_prompts.go b/gpt/builtin_prompts.go new file mode 100644 index 0000000..989c2f6 --- /dev/null +++ b/gpt/builtin_prompts.go @@ -0,0 +1,124 @@ +package gpt + +import ( + _ "embed" + + "gopkg.in/yaml.v3" +) + +//go:embed builtin_prompts.yaml +var builtinPromptsYAML string + +var builtinPrompts string + +// BuiltinPromptsData структура для YAML файла +type BuiltinPromptsData struct { + Prompts []BuiltinPrompt `yaml:"prompts"` +} + +// BuiltinPrompt структура для встроенных промптов с поддержкой языков +type BuiltinPrompt struct { + ID int `yaml:"id"` + Name string `yaml:"name"` + Description map[string]string `yaml:"description"` + Content map[string]string `yaml:"content"` +} + +// ToSystemPrompt конвертирует BuiltinPrompt в SystemPrompt для указанного языка +func (bp *BuiltinPrompt) ToSystemPrompt(lang string) SystemPrompt { + // Если язык не найден, используем английский по умолчанию + if _, exists := bp.Description[lang]; !exists { + lang = "en" + } + + return SystemPrompt{ + ID: bp.ID, + Name: bp.Name, + Description: bp.Description[lang], + Content: bp.Content[lang], + } +} + +// GetBuiltinPrompts возвращает встроенные промпты из YAML (по умолчанию английские) +func GetBuiltinPrompts() []SystemPrompt { + return GetBuiltinPromptsByLanguage("en") +} + +// GetBuiltinPromptsByLanguage возвращает встроенные промпты для указанного языка +func GetBuiltinPromptsByLanguage(lang string) []SystemPrompt { + var data BuiltinPromptsData + if err := yaml.Unmarshal([]byte(builtinPrompts), &data); err != nil { + // В случае ошибки возвращаем пустой массив + return []SystemPrompt{} + } + + var result []SystemPrompt + for _, prompt := range data.Prompts { + result = append(result, prompt.ToSystemPrompt(lang)) + } + return result +} + +// IsBuiltinPrompt проверяет, является ли промпт встроенным +func IsBuiltinPrompt(prompt SystemPrompt) bool { + // Проверяем английскую версию + englishPrompts := GetBuiltinPromptsByLanguage("en") + for _, builtin := range englishPrompts { + if builtin.ID == prompt.ID { + if builtin.Content == prompt.Content && + builtin.Name == prompt.Name && + builtin.Description == prompt.Description { + return true + } + } + } + + // Проверяем русскую версию + russianPrompts := GetBuiltinPromptsByLanguage("ru") + for _, builtin := range russianPrompts { + if builtin.ID == prompt.ID { + if builtin.Content == prompt.Content && + builtin.Name == prompt.Name && + builtin.Description == prompt.Description { + return true + } + } + } + + return false +} + +// GetBuiltinPromptByID возвращает встроенный промпт по ID (английская версия) +func GetBuiltinPromptByID(id int) *SystemPrompt { + builtinPrompts := GetBuiltinPrompts() + + for _, prompt := range builtinPrompts { + if prompt.ID == id { + return &prompt + } + } + + return nil +} + +// GetBuiltinPromptByIDAndLanguage возвращает встроенный промпт по ID и языку +func GetBuiltinPromptByIDAndLanguage(id int, lang string) *SystemPrompt { + builtinPrompts := GetBuiltinPromptsByLanguage(lang) + + for _, prompt := range builtinPrompts { + if prompt.ID == id { + return &prompt + } + } + + return nil +} + +func InitBuiltinPrompts(embeddedBuiltinPromptsYAML string) { + // Используем встроенный YAML, если переданный параметр пустой + if embeddedBuiltinPromptsYAML == "" { + builtinPrompts = builtinPromptsYAML + } else { + builtinPrompts = embeddedBuiltinPromptsYAML + } +} diff --git a/gpt/builtin_prompts.yaml b/gpt/builtin_prompts.yaml new file mode 100644 index 0000000..d3bca6d --- /dev/null +++ b/gpt/builtin_prompts.yaml @@ -0,0 +1,262 @@ +prompts: + - id: 1 + name: "linux-command" + description: + en: "Main prompt for generating Linux commands" + ru: "Основной промпт для генерации Linux команд" + content: + en: | + You are a Linux command line expert. + Analyze the user's task, given in natural language, and suggest + a Linux command that will help accomplish this task, and provide a detailed explanation of what it does, + its parameters and possible use cases. + Focus on practical examples and best practices. + In the response, you should only provide the commands or sequence of commands ready to copy and execute + in the command line without any explanationformatting or code blocks, without ```bash``` or ```sh```, ` or ``` symbols. + + ru: | + Вы эксперт по Linux командам и командной строке. + Проанализируйте задачу пользователя на естественном языке и предложите Linux команду или набор команд, которые помогут выполнить эту задачу, и предоставьте подробное объяснение того, что она делает, её параметры и возможные случаи использования. + Сосредоточьтесь на практических примерах и лучших практиках. + В ответе должна присутствовать только команда или последовательность команд, + готовая к копированию и выполнению в командной строке + без объяснений, выделений и форматирования наподобие ```bash``` или ```sh```, без символов ` или ```. + + - id: 2 + name: "linux-command-with-explanation" + description: + en: "Prompt with detailed command explanation" + ru: "Промпт с подробным объяснением команд" + content: + en: | + You are a Linux system administrator with extensive experience. + Generate Linux commands based on user task descriptions and provide comprehensive explanations. + + Provide a detailed analysis including: + 1. **Generated Command**: The Linux command that accomplishes the task + 2. **Command Breakdown**: Explain each part of the command + 3. **Parameters**: Explain each flag and option used + 4. **Examples**: Show practical usage scenarios + 5. **Security**: Highlight any security considerations + 6. **Alternatives**: Suggest similar commands if applicable + 7. **Best Practices**: Recommend optimal usage + + Use clear formatting with headers and bullet points for readability. + ru: | + Вы системный администратор Linux с обширным опытом. + Генерируйте Linux команды на основе описаний задач пользователей и предоставляйте исчерпывающие объяснения. + + Предоставьте подробный анализ, включая: + 1. **Сгенерированная команда**: Linux команда, которая выполняет задачу + 2. **Разбор команды**: Объясните каждую часть команды + 3. **Параметры**: Объясните каждый используемый флаг и опцию + 4. **Примеры**: Покажите практические сценарии использования + 5. **Безопасность**: Выделите любые соображения безопасности + 6. **Альтернативы**: Предложите похожие команды, если применимо + 7. **Лучшие практики**: Рекомендуйте оптимальное использование + + Используйте четкое форматирование с заголовками и маркерами для читаемости. + + - id: 3 + name: "linux-command-safe" + description: + en: "Safe command analysis with warnings" + ru: "Безопасный анализ команд с предупреждениями" + content: + en: | + You are a Linux security expert. Generate safe Linux commands based on user task descriptions with a focus on safety and security implications. + + Provide a security-focused analysis: + 1. **Generated Safe Command**: The secure Linux command for the task + 2. **Safety Assessment**: Why this command is safe to run + 3. **Potential Risks**: What could go wrong and how to mitigate + 4. **Data Impact**: What files or data might be affected + 5. **Permissions**: What permissions are required + 6. **Recovery**: How to undo changes if needed + 7. **Best Practices**: Safe alternatives or precautions + 8. **Warnings**: Critical safety considerations + + Always prioritize user safety and data protection. + ru: | + Вы эксперт по безопасности Linux. Генерируйте безопасные Linux команды на основе описаний задач пользователей с акцентом на безопасность и последствия для безопасности. + + Предоставьте анализ, ориентированный на безопасность: + 1. **Сгенерированная безопасная команда**: Безопасная Linux команда для задачи + 2. **Оценка безопасности**: Почему эта команда безопасна для выполнения + 3. **Потенциальные риски**: Что может пойти не так и как это смягчить + 4. **Воздействие на данные**: Какие файлы или данные могут быть затронуты + 5. **Разрешения**: Какие разрешения требуются + 6. **Восстановление**: Как отменить изменения при необходимости + 7. **Лучшие практики**: Безопасные альтернативы или меры предосторожности + 8. **Предупреждения**: Критические соображения безопасности + + Всегда приоритизируйте безопасность пользователя и защиту данных. + + - id: 4 + name: "linux-command-verbose" + description: + en: "Detailed analysis with technical details" + ru: "Подробный анализ с техническими деталями" + content: + en: | + You are a Linux kernel and system expert. Generate Linux commands based on user task descriptions and provide an in-depth technical analysis. + + Deliver a comprehensive technical breakdown: + 1. **Generated Command**: The Linux command that accomplishes the task + 2. **System Level**: How the command interacts with the kernel + 3. **Process Flow**: Step-by-step execution details + 4. **Resource Usage**: CPU, memory, I/O implications + 5. **File System**: Impact on files and directories + 6. **Network**: Network operations if applicable + 7. **Performance**: Optimization considerations + 8. **Debugging**: Troubleshooting approaches + 9. **Advanced Usage**: Expert-level techniques + + Include technical details, system calls, and low-level operations. + ru: | + Вы эксперт по ядру Linux и системам. Генерируйте Linux команды на основе описаний задач пользователей и предоставляйте глубокий технический анализ. + + Предоставьте исчерпывающий технический разбор: + 1. **Сгенерированная команда**: Linux команда, которая выполняет задачу + 2. **Системный уровень**: Как команда взаимодействует с ядром + 3. **Поток выполнения**: Детали пошагового выполнения + 4. **Использование ресурсов**: Последствия для CPU, памяти, I/O + 5. **Файловая система**: Воздействие на файлы и каталоги + 6. **Сеть**: Сетевые операции, если применимо + 7. **Производительность**: Соображения по оптимизации + 8. **Отладка**: Подходы к устранению неполадок + 9. **Продвинутое использование**: Техники экспертного уровня + + Включите технические детали, системные вызовы и низкоуровневые операции. + + - id: 5 + name: "linux-command-simple" + description: + en: "Simple and clear explanation" + ru: "Простое и понятное объяснение" + content: + en: | + You are a friendly Linux mentor. Explain the given command in simple, easy-to-understand terms. + + Command: {{.command}} + + Provide a beginner-friendly explanation: + 1. **What it does**: Simple, clear description + 2. **Why use it**: Common reasons to use this command + 3. **Basic example**: Simple usage example + 4. **What to expect**: Expected output or behavior + 5. **Tips**: Helpful hints for beginners + + Use plain language, avoid jargon, and focus on practical understanding. + ru: | + Вы дружелюбный наставник по Linux. Объясните данную команду простыми, понятными терминами. + + Команда: {{.command}} + + Предоставьте объяснение, подходящее для начинающих: + 1. **Что она делает**: Простое, четкое описание + 2. **Зачем использовать**: Общие причины использования этой команды + 3. **Базовый пример**: Простой пример использования + 4. **Что ожидать**: Ожидаемый вывод или поведение + 5. **Советы**: Полезные подсказки для начинающих + + Используйте простой язык, избегайте жаргона и сосредоточьтесь на практическом понимании. + + - id: 6 + name: "verbose-v" + description: + en: "Prompt for v mode (basic explanation)" + ru: "Промпт для режима v (базовое объяснение)" + content: + en: | + You are a Linux command expert. You can provide a clear and concise explanation of the given Linux command. + Your explanation should include: + 1. What this command does for the task + 2. Main parameters and their purpose + 3. Common use cases + 4. Any important warnings or considerations + ru: | + Вы эксперт по Linux командам. Вы можете предоставьте четкое и краткое объяснение заданной Linux команды. + Ваши краткие объяснения должны включать: + 1. Что делает эта команда + 2. Основные параметры и их назначение + 3. Общие случаи использования + 4. Любые важные предупреждения или соображения + + - id: 7 + name: "verbose-vv" + description: + en: "Prompt for vv mode (detailed explanation)" + ru: "Промпт для режима vv (подробное объяснение)" + content: + en: | + You are a Linux system expert. Provide a detailed technical explanation of the given command. + + Provide a comprehensive analysis: + 1. **Command Purpose**: What it accomplishes + 2. **Syntax Breakdown**: Detailed parameter analysis + 3. **Technical Details**: How it works internally + 4. **Use Cases**: Practical scenarios and examples + 5. **Performance Impact**: Resource usage and optimization + 6. **Security Considerations**: Potential risks and mitigations + 7. **Advanced Usage**: Expert techniques and tips + 8. **Troubleshooting**: Common issues and solutions + + Include technical depth while maintaining clarity. + ru: | + Вы эксперт по Linux системам. Предоставьте подробное техническое объяснение заданной команды. + + Предоставьте исчерпывающий анализ: + 1. **Цель команды**: Что она достигает + 2. **Разбор синтаксиса**: Подробный анализ параметров + 3. **Технические детали**: Как она работает внутренне + 4. **Случаи использования**: Практические сценарии и примеры + 5. **Влияние на производительность**: Использование ресурсов и оптимизация + 6. **Соображения безопасности**: Потенциальные риски и меры по их снижению + 7. **Продвинутое использование**: Экспертные техники и советы + 8. **Устранение неполадок**: Общие проблемы и решения + + Включите техническую глубину, сохраняя ясность. + + - id: 8 + name: "verbose-vvv" + description: + en: "Prompt for vvv mode (maximum detailed explanation)" + ru: "Промпт для режима vvv (максимально подробное объяснение)" + content: + en: | + You are a Linux kernel and system architecture expert. Provide an exhaustive technical analysis of the given command. + + Deliver a comprehensive technical deep-dive: + 1. **System Architecture**: How it fits into the Linux ecosystem + 2. **Kernel Interaction**: System calls and kernel operations + 3. **Process Management**: Process creation, scheduling, and lifecycle + 4. **Memory Management**: Memory allocation and management + 5. **File System Operations**: I/O operations and file system impact + 6. **Network Stack**: Network operations and protocols + 7. **Security Model**: Permissions, capabilities, and security implications + 8. **Performance Analysis**: CPU, memory, I/O, and network impact + 9. **Debugging and Profiling**: Advanced troubleshooting techniques + 10. **Source Code Analysis**: Key implementation details + 11. **Alternative Implementations**: Different approaches and trade-offs + 12. **Historical Context**: Evolution and development history + + Provide maximum technical depth with system-level insights, code examples, and architectural understanding. + ru: | + Вы эксперт по ядру Linux и системной архитектуре. Предоставьте исчерпывающий технический анализ заданной команды. + + Предоставьте исчерпывающий технический глубокий анализ: + 1. **Системная архитектура**: Как она вписывается в экосистему Linux + 2. **Взаимодействие с ядром**: Системные вызовы и операции ядра + 3. **Управление процессами**: Создание, планирование и жизненный цикл процессов + 4. **Управление памятью**: Выделение и управление памятью + 5. **Операции файловой системы**: I/O операции и воздействие на файловую систему + 6. **Сетевой стек**: Сетевые операции и протоколы + 7. **Модель безопасности**: Разрешения, возможности и последствия безопасности + 8. **Анализ производительности**: Воздействие на CPU, память, I/O и сеть + 9. **Отладка и профилирование**: Продвинутые техники устранения неполадок + 10. **Анализ исходного кода**: Ключевые детали реализации + 11. **Альтернативные реализации**: Разные подходы и компромиссы + 12. **Исторический контекст**: Эволюция и история разработки + + Предоставьте максимальную техническую глубину с системными инсайтами, примерами кода и архитектурным пониманием. diff --git a/gpt/prompts.go b/gpt/prompts.go index fad3efa..6a2c0c0 100644 --- a/gpt/prompts.go +++ b/gpt/prompts.go @@ -6,6 +6,8 @@ import ( "os" "path/filepath" "strings" + + "github.com/direct-dev-ru/linux-command-gpt/config" ) // SystemPrompt представляет системный промпт @@ -21,28 +23,50 @@ type PromptManager struct { Prompts []SystemPrompt ConfigFile string HomeDir string + Language string // Текущий язык для файла sys_prompts (en/ru) } // NewPromptManager создает новый менеджер промптов func NewPromptManager(homeDir string) *PromptManager { - configFile := filepath.Join(homeDir, ".lcg_prompts.json") + // Используем конфигурацию из модуля config + promptFolder := config.AppConfig.PromptFolder + + // Путь к файлу sys_prompts + sysPromptsFile := filepath.Join(promptFolder, "sys_prompts") pm := &PromptManager{ - ConfigFile: configFile, + ConfigFile: sysPromptsFile, HomeDir: homeDir, } - // Загружаем предустановленные промпты - pm.loadDefaultPrompts() + // Проверяем, существует ли файл sys_prompts + if _, err := os.Stat(sysPromptsFile); os.IsNotExist(err) { + // Если файла нет, создаем его с системными промптами и промптами подробности + pm.createInitialPromptsFile() + } - // Загружаем пользовательские промпты - pm.loadCustomPrompts() + // Загружаем все промпты из файла + pm.loadAllPrompts() return pm } +// createInitialPromptsFile создает начальный файл с системными промптами и промптами подробности +func (pm *PromptManager) createInitialPromptsFile() { + // Загружаем все встроенные промпты из YAML (английские по умолчанию) + pm.Prompts = GetBuiltinPrompts() + + // Фикс: при первичном сохранении явно выставляем язык файла + if pm.Language == "" { + pm.Language = "en" + } + + // Сохраняем все промпты в файл + pm.saveAllPrompts() +} + // loadDefaultPrompts загружает предустановленные промпты -func (pm *PromptManager) loadDefaultPrompts() { +func (pm *PromptManager) LoadDefaultPrompts() { defaultPrompts := []SystemPrompt{ { ID: 1, @@ -79,8 +103,8 @@ func (pm *PromptManager) loadDefaultPrompts() { pm.Prompts = defaultPrompts } -// loadCustomPrompts загружает пользовательские промпты из файла -func (pm *PromptManager) loadCustomPrompts() { +// loadAllPrompts загружает все промпты из файла sys_prompts +func (pm *PromptManager) loadAllPrompts() { if _, err := os.Stat(pm.ConfigFile); os.IsNotExist(err) { return } @@ -90,18 +114,60 @@ func (pm *PromptManager) loadCustomPrompts() { return } - var customPrompts []SystemPrompt - if err := json.Unmarshal(data, &customPrompts); err != nil { + // Новый формат: объект с полями language и prompts + var pf promptsFile + if err := json.Unmarshal(data, &pf); err == nil && len(pf.Prompts) > 0 { + pm.Language = pf.Language + pm.Prompts = pf.Prompts return } - // Добавляем пользовательские промпты с новыми ID - for i, prompt := range customPrompts { - prompt.ID = len(pm.Prompts) + i + 1 - pm.Prompts = append(pm.Prompts, prompt) + // Старый формат: просто массив промптов + var prompts []SystemPrompt + if err := json.Unmarshal(data, &prompts); err == nil { + pm.Prompts = prompts + pm.Language = "en" + // Миграция в новый формат при следующем сохранении } } +// saveAllPrompts сохраняет все промпты в файл sys_prompts +// внутренний формат хранения файла sys_prompts +type promptsFile struct { + Language string `json:"language,omitempty"` + Prompts []SystemPrompt `json:"prompts"` +} + +func (pm *PromptManager) saveAllPrompts() error { + pf := promptsFile{ + Language: pm.Language, + Prompts: pm.Prompts, + } + data, err := json.MarshalIndent(pf, "", " ") + if err != nil { + return err + } + return os.WriteFile(pm.ConfigFile, data, 0644) +} + +// SaveAllPrompts экспортированная версия saveAllPrompts +func (pm *PromptManager) SaveAllPrompts() error { + return pm.saveAllPrompts() +} + +// GetCurrentLanguage возвращает текущий язык из файла промптов +func (pm *PromptManager) GetCurrentLanguage() string { + if pm.Language == "" { + return "en" + } + return pm.Language +} + +// SetLanguage устанавливает язык для всех промптов +func (pm *PromptManager) SetLanguage(lang string) { + pm.Language = lang +} + // saveCustomPrompts сохраняет пользовательские промпты func (pm *PromptManager) saveCustomPrompts() error { // Находим пользовательские промпты (ID > 5) @@ -140,24 +206,136 @@ func (pm *PromptManager) GetPromptByName(name string) (*SystemPrompt, error) { return nil, fmt.Errorf("промпт с именем '%s' не найден", name) } +// AddPrompt добавляет новый промпт +func (pm *PromptManager) AddPrompt(name, description, content string) error { + // Находим максимальный ID + maxID := 0 + for _, prompt := range pm.Prompts { + if prompt.ID > maxID { + maxID = prompt.ID + } + } + + newPrompt := SystemPrompt{ + ID: maxID + 1, + Name: name, + Description: description, + Content: content, + } + + pm.Prompts = append(pm.Prompts, newPrompt) + return pm.saveAllPrompts() +} + +// UpdatePrompt обновляет существующий промпт +func (pm *PromptManager) UpdatePrompt(id int, name, description, content string) error { + for i, prompt := range pm.Prompts { + if prompt.ID == id { + pm.Prompts[i].Name = name + pm.Prompts[i].Description = description + pm.Prompts[i].Content = content + return pm.saveAllPrompts() + } + } + return fmt.Errorf("промпт с ID %d не найден", id) +} + +// DeletePrompt удаляет промпт по ID +func (pm *PromptManager) DeletePrompt(id int) error { + for i, prompt := range pm.Prompts { + if prompt.ID == id { + pm.Prompts = append(pm.Prompts[:i], pm.Prompts[i+1:]...) + return pm.saveAllPrompts() + } + } + return fmt.Errorf("промпт с ID %d не найден", id) +} + // ListPrompts выводит список всех доступных промптов func (pm *PromptManager) ListPrompts() { - fmt.Println("Available system prompts:") - fmt.Println("ID | Name | Description") - fmt.Println("---+---------------------------+--------------------------------") + pm.ListPromptsWithFull(false) +} - for _, prompt := range pm.Prompts { - description := prompt.Description - if len(description) > 80 { - description = description[:77] + "..." +// ListPromptsWithFull выводит список промптов с опцией полного вывода +func (pm *PromptManager) ListPromptsWithFull(full bool) { + fmt.Println("📝 Доступные системные промпты:") + fmt.Println() + + for i, prompt := range pm.Prompts { + // Разделитель между промптами + if i > 0 { + fmt.Println("─" + strings.Repeat("─", 60)) } - fmt.Printf("%-2d | %-25s | %s\n", - prompt.ID, - truncateString(prompt.Name, 25), - description) + + // Проверяем, является ли промпт встроенным и неизмененным + isDefault := pm.isDefaultPrompt(prompt) + + // Заголовок промпта + if isDefault { + fmt.Printf("🔹 ID: %d | Название: %s | Встроенный\n", prompt.ID, prompt.Name) + } else { + fmt.Printf("🔹 ID: %d | Название: %s\n", prompt.ID, prompt.Name) + } + + // Описание + if prompt.Description != "" { + fmt.Printf("📋 Описание: %s\n", prompt.Description) + } + + // Содержимое промпта + fmt.Println("📄 Содержимое:") + fmt.Println("┌" + strings.Repeat("─", 58) + "┐") + + // Разбиваем содержимое на строки и выводим с отступами + lines := strings.Split(prompt.Content, "\n") + for _, line := range lines { + if full { + // Полный вывод без обрезки - разбиваем длинные строки + if len(line) > 56 { + // Разбиваем длинную строку на части + for i := 0; i < len(line); i += 56 { + end := i + 56 + if end > len(line) { + end = len(line) + } + fmt.Printf("│ %-56s │\n", line[i:end]) + } + } else { + fmt.Printf("│ %-56s │\n", line) + } + } else { + // Обычный вывод с обрезкой + fmt.Printf("│ %-56s │\n", truncateString(line, 56)) + } + } + + fmt.Println("└" + strings.Repeat("─", 58) + "┘") + fmt.Println() } } +// isDefaultPrompt проверяет, является ли промпт встроенным и неизмененным +func (pm *PromptManager) isDefaultPrompt(prompt SystemPrompt) bool { + // Используем новую функцию из builtin_prompts.go + return IsBuiltinPrompt(prompt) +} + +// IsDefaultPromptByID проверяет, является ли промпт встроенным только по ID (игнорирует содержимое) +func (pm *PromptManager) IsDefaultPromptByID(prompt SystemPrompt) bool { + // Проверяем, что ID находится в диапазоне встроенных промптов (1-8) + return prompt.ID >= 1 && prompt.ID <= 8 +} + +// GetRussianDefaultPrompts возвращает русские версии встроенных промптов +func GetRussianDefaultPrompts() []SystemPrompt { + return GetBuiltinPromptsByLanguage("ru") +} + +// getDefaultPrompts возвращает оригинальные встроенные промпты +func (pm *PromptManager) GetDefaultPrompts() []SystemPrompt { + return GetBuiltinPrompts() +} + // AddCustomPrompt добавляет новый пользовательский промпт func (pm *PromptManager) AddCustomPrompt(name, description, content string) error { // Проверяем, что имя уникально diff --git a/main.go b/main.go index c83a11e..f69b27d 100644 --- a/main.go +++ b/main.go @@ -27,6 +27,9 @@ var Version string // disableHistory управляет записью/обновлением истории на уровне процесса (флаг имеет приоритет над env) var disableHistory bool +// fromHistory указывает, что текущий ответ взят из истории +var fromHistory bool + const ( colorRed = "\033[31m" colorGreen = "\033[32m" @@ -41,6 +44,13 @@ const ( func main() { _ = colorBlue + gpt.InitBuiltinPrompts("") + + // Авто-инициализация sys_prompts при старте CLI (создаст файл при отсутствии) + if currentUser, err := user.Current(); err == nil { + _ = gpt.NewPromptManager(currentUser.HomeDir) + } + app := &cli.App{ Name: "lcg", Usage: "Linux Command GPT - Генерация Linux команд из описаний", @@ -97,6 +107,12 @@ Linux Command GPT - инструмент для генерации Linux ком DefaultText: "120", Value: 120, }, + &cli.BoolFlag{ + Name: "debug", + Aliases: []string{"d"}, + Usage: "Show debug information (request parameters and prompts)", + Value: false, + }, }, Action: func(c *cli.Context) error { file := c.String("file") @@ -117,6 +133,7 @@ Linux Command GPT - инструмент для генерации Linux ком Sys: system, PromptID: promptID, Timeout: timeout, + Debug: c.Bool("debug"), } disableHistory = config.AppConfig.MainFlags.NoHistory || config.AppConfig.IsNoHistoryEnabled() args := c.Args().Slice() @@ -384,10 +401,18 @@ func getCommands() []*cli.Command { Name: "list", Aliases: []string{"l"}, Usage: "List all available prompts", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "full", + Aliases: []string{"f"}, + Usage: "Show full content without truncation", + }, + }, Action: func(c *cli.Context) error { currentUser, _ := user.Current() pm := gpt.NewPromptManager(currentUser.HomeDir) - pm.ListPrompts() + full := c.Bool("full") + pm.ListPromptsWithFull(full) return nil }, }, @@ -491,10 +516,43 @@ func getCommands() []*cli.Command { return nil }, }, + { + Name: "serve-result", + Aliases: []string{"serve"}, + Usage: "Start HTTP server to browse saved results", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "port", + Aliases: []string{"p"}, + Usage: "Server port", + Value: config.AppConfig.Server.Port, + }, + &cli.StringFlag{ + Name: "host", + Aliases: []string{"H"}, + Usage: "Server host", + Value: config.AppConfig.Server.Host, + }, + }, + Action: func(c *cli.Context) error { + port := c.String("port") + host := c.String("host") + + 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) + }, + }, } } func executeMain(file, system, commandInput string, timeout int) { + // Выводим debug информацию если включен флаг + if config.AppConfig.MainFlags.Debug { + printDebugInfo(file, system, commandInput, timeout) + } if file != "" { if err := reader.FileToPrompt(&commandInput, file); err != nil { printColored(fmt.Sprintf("❌ Ошибка чтения файла: %v\n", err), colorRed) @@ -518,6 +576,7 @@ func executeMain(file, system, commandInput string, timeout int) { // Проверка истории: если такой запрос уже встречался — предложить открыть из истории if !disableHistory { if found, hist := cmdPackage.CheckAndSuggestFromHistory(config.AppConfig.ResultHistory, commandInput); found && hist != nil { + fromHistory = true // Устанавливаем флаг, что ответ из истории gpt3 := initGPT(system, timeout) printColored("\nВНИМАНИЕ: ОТВЕТ СФОРМИРОВАН ИИ. ТРЕБУЕТСЯ ПРОВЕРКА И КРИТИЧЕСКИЙ АНАЛИЗ. ВОЗМОЖНЫ ОШИБКИ И ГАЛЛЮЦИНАЦИИ.\n", colorRed) printColored("\n📋 Команда (из истории):\n", colorYellow) @@ -527,7 +586,7 @@ func executeMain(file, system, commandInput string, timeout int) { fmt.Println(hist.Explanation) } // Показали из истории — не выполняем запрос к API, сразу меню действий - handlePostResponse(hist.Response, gpt3, system, commandInput, timeout) + handlePostResponse(hist.Response, gpt3, system, commandInput, timeout, hist.Explanation) return } } @@ -553,7 +612,8 @@ func executeMain(file, system, commandInput string, timeout int) { // Сохраняем в историю (после завершения работы – т.е. позже, в зависимости от выбора действия) // Здесь не сохраняем, чтобы учесть правило: сохранять после действия, отличного от v/vv/vvv - handlePostResponse(response, gpt3, system, commandInput, timeout) + fromHistory = false // Сбрасываем флаг для новых запросов + handlePostResponse(response, gpt3, system, commandInput, timeout, "") } // checkAndSuggestFromHistory проверяет файл истории и при совпадении запроса предлагает показать сохраненный результат @@ -607,7 +667,7 @@ func getCommand(gpt3 gpt.Gpt3, cmd string) (string, float64) { return response, elapsed } -func handlePostResponse(response string, gpt3 gpt.Gpt3, system, cmd string, timeout int) { +func handlePostResponse(response string, gpt3 gpt.Gpt3, system, cmd string, timeout int, explanation string) { fmt.Printf("Действия: (c)копировать, (s)сохранить, (r)перегенерировать, (e)выполнить, (v|vv|vvv)подробно, (n)ничего: ") var choice string fmt.Scanln(&choice) @@ -617,12 +677,24 @@ func handlePostResponse(response string, gpt3 gpt.Gpt3, system, cmd string, time clipboard.WriteAll(response) fmt.Println("✅ Команда скопирована в буфер обмена") if !disableHistory { - cmdPackage.SaveToHistory(config.AppConfig.ResultHistory, config.AppConfig.ResultFolder, cmd, response, gpt3.Prompt) + 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) + } } case "s": - saveResponse(response, gpt3.Model, gpt3.Prompt, cmd) + if fromHistory && strings.TrimSpace(explanation) != "" { + saveResponse(response, gpt3.Model, gpt3.Prompt, cmd, explanation) + } else { + saveResponse(response, gpt3.Model, gpt3.Prompt, cmd) + } if !disableHistory { - cmdPackage.SaveToHistory(config.AppConfig.ResultHistory, config.AppConfig.ResultFolder, cmd, response, gpt3.Prompt) + 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) + } } case "r": fmt.Println("🔄 Перегенерирую...") @@ -630,7 +702,11 @@ func handlePostResponse(response string, gpt3 gpt.Gpt3, system, cmd string, time case "e": executeCommand(response) if !disableHistory { - cmdPackage.SaveToHistory(config.AppConfig.ResultHistory, config.AppConfig.ResultFolder, cmd, response, gpt3.Prompt) + 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) + } } case "v", "vv", "vvv": level := len(choice) // 1, 2, 3 @@ -647,7 +723,11 @@ func handlePostResponse(response string, gpt3 gpt.Gpt3, system, cmd string, time default: fmt.Println(" До свидания!") if !disableHistory { - cmdPackage.SaveToHistory(config.AppConfig.ResultHistory, config.AppConfig.ResultFolder, cmd, response, gpt3.Prompt) + 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) + } } } } @@ -702,4 +782,19 @@ func showTips() { fmt.Println(" • Команда 'history list' покажет историю запросов") fmt.Println(" • Команда 'config' покажет текущие настройки") fmt.Println(" • Команда 'health' проверит доступность API") + fmt.Println(" • Команда 'serve-result' запустит HTTP сервер для просмотра результатов") +} + +// printDebugInfo выводит отладочную информацию о параметрах запроса +func printDebugInfo(file, system, commandInput string, timeout int) { + printColored("\n🔍 DEBUG ИНФОРМАЦИЯ:\n", colorCyan) + fmt.Printf("📁 Файл: %s\n", file) + fmt.Printf("🤖 Системный промпт: %s\n", system) + fmt.Printf("💬 Запрос: %s\n", commandInput) + fmt.Printf("⏱️ Таймаут: %d сек\n", timeout) + fmt.Printf("🌐 Провайдер: %s\n", config.AppConfig.ProviderType) + fmt.Printf("🏠 Хост: %s\n", config.AppConfig.Host) + fmt.Printf("🧠 Модель: %s\n", config.AppConfig.Model) + fmt.Printf("📝 История: %t\n", !config.AppConfig.MainFlags.NoHistory) + printColored("────────────────────────────────────────\n", colorCyan) } diff --git a/response.go b/response.go index 9a5623e..a57c6b0 100644 --- a/response.go +++ b/response.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path" + "strings" "time" "github.com/direct-dev-ru/linux-command-gpt/config" @@ -25,12 +26,22 @@ func writeFile(filePath, content string) { } } -func saveResponse(response string, gpt3Model string, prompt string, cmd string) { +func saveResponse(response string, gpt3Model string, prompt string, cmd string, explanation ...string) { timestamp := nowTimestamp() filename := fmt.Sprintf("gpt_request_%s_%s.md", gpt3Model, timestamp) filePath := pathJoin(config.AppConfig.ResultFolder, filename) title := truncateTitle(cmd) - content := fmt.Sprintf("# %s\n\n## Prompt\n\n%s\n\n## Response\n\n%s\n", title, cmd+". "+prompt, response) + + var content string + if len(explanation) > 0 && strings.TrimSpace(explanation[0]) != "" { + // Если есть объяснение, сохраняем полную структуру + content = fmt.Sprintf("# %s\n\n## Prompt\n\n%s\n\n## Response\n\n%s\n\n## Explanation\n\n%s\n", + title, cmd+". "+prompt, response, explanation[0]) + } else { + // Если объяснения нет, сохраняем базовую структуру + content = fmt.Sprintf("# %s\n\n## Prompt\n\n%s\n\n## Response\n\n%s\n", + title, cmd+". "+prompt, response) + } writeFile(filePath, content) }