mirror of
https://github.com/Direct-Dev-Ru/go-lcg.git
synced 2025-11-16 01:29:55 +00:00
before result functions add
This commit is contained in:
183
API_CONTRACT.md
Normal file
183
API_CONTRACT.md
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
# Контракт API для провайдеров (proxy и ollama)
|
||||||
|
|
||||||
|
Этот документ описывает минимально необходимый API, который должен предоставлять сервер-провайдер (режимы: "proxy" и "ollama"), чтобы CLI-приложение работало корректно.
|
||||||
|
|
||||||
|
## Общие требования
|
||||||
|
|
||||||
|
- **Базовый URL** берётся из `config.AppConfig.Host`. Трейлинг-слэш на стороне клиента обрезается.
|
||||||
|
- **Таймаут** HTTP-запросов задаётся в секундах через конфигурацию (см. `config.AppConfig.Timeout`).
|
||||||
|
- **Кодирование**: все тела запросов и ответов — `application/json; charset=utf-8`.
|
||||||
|
- **Стриминг**: на данный момент клиент всегда запрашивает `stream=false`; стриминг не используется.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Режим proxy
|
||||||
|
|
||||||
|
### Аутентификация
|
||||||
|
|
||||||
|
- Все защищённые эндпоинты требуют заголовок: `Authorization: Bearer <JWT>`.
|
||||||
|
- Токен берётся из `config.AppConfig.JwtToken`, либо из файла `~/.proxy_jwt_token`.
|
||||||
|
|
||||||
|
### 1) POST `/api/v1/protected/sberchat/chat`
|
||||||
|
|
||||||
|
- **Назначение**: получить единственный текстовый ответ LLM.
|
||||||
|
- **Заголовки**:
|
||||||
|
- `Content-Type: application/json`
|
||||||
|
- `Authorization: Bearer <JWT>` (обязательно)
|
||||||
|
- **Тело запроса** (минимально необходимые поля):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"messages": [
|
||||||
|
{ "role": "system", "content": "<system_prompt>" },
|
||||||
|
{ "role": "user", "content": "<ask>" }
|
||||||
|
],
|
||||||
|
"model": "<model_name>",
|
||||||
|
"temperature": 0.5,
|
||||||
|
"top_p": 0.5,
|
||||||
|
"stream": false,
|
||||||
|
"random_words": ["linux", "command", "gpt"],
|
||||||
|
"fallback_string": "I'm sorry, I can't help with that. Please try again."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Ответ 200 OK**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"response": "<string>",
|
||||||
|
"usage": {
|
||||||
|
"prompt_tokens": 0,
|
||||||
|
"completion_tokens": 0,
|
||||||
|
"total_tokens": 0
|
||||||
|
},
|
||||||
|
"error": "",
|
||||||
|
"model": "<model_name>",
|
||||||
|
"timeout_seconds": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Ошибки**: любой статус != 200 воспринимается как ошибка. Желательно вернуть JSON вида:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "error": "<message>" }
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Пример cURL**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sS -X POST \
|
||||||
|
-H "Authorization: Bearer $JWT" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"$HOST/api/v1/protected/sberchat/chat" \
|
||||||
|
-d '{
|
||||||
|
"messages": [
|
||||||
|
{"role":"system","content":"system prompt"},
|
||||||
|
{"role":"user","content":"user ask"}
|
||||||
|
],
|
||||||
|
"model":"GigaChat-2-Max",
|
||||||
|
"temperature":0.5,
|
||||||
|
"top_p":0.5,
|
||||||
|
"stream":false,
|
||||||
|
"random_words":["linux","command","gpt"],
|
||||||
|
"fallback_string":"I'm sorry, I can't help with that. Please try again."
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2) GET `/api/v1/protected/sberchat/health`
|
||||||
|
|
||||||
|
- **Назначение**: health-check API и получение части метаданных по умолчанию.
|
||||||
|
- **Заголовки**:
|
||||||
|
- `Authorization: Bearer <JWT>` (если сервер требует авторизацию на health)
|
||||||
|
- **Ответ 200 OK**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "ok",
|
||||||
|
"message": "<string>",
|
||||||
|
"default_model": "<string>",
|
||||||
|
"default_timeout_seconds": 120
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Ошибки**: любой статус != 200 считается падением health.
|
||||||
|
|
||||||
|
### Модели
|
||||||
|
|
||||||
|
- В текущей реализации клиент не запрашивает список моделей у proxy и использует фиксированный набор.
|
||||||
|
- Опционально можно реализовать эндпоинт для списка моделей (например, `GET /api/v1/protected/sberchat/models`) и расширить клиента позже.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Режим ollama
|
||||||
|
|
||||||
|
### 1) POST `/api/chat`
|
||||||
|
|
||||||
|
- **Назначение**: синхронная генерация одного ответа (без стрима).
|
||||||
|
- **Заголовки**:
|
||||||
|
- `Content-Type: application/json`
|
||||||
|
- **Тело запроса**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"model": "<model_name>",
|
||||||
|
"stream": false,
|
||||||
|
"messages": [
|
||||||
|
{ "role": "system", "content": "<system_prompt>" },
|
||||||
|
{ "role": "user", "content": "<ask>" }
|
||||||
|
],
|
||||||
|
"options": {"temperature": 0.2}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Ответ 200 OK** (минимальный, который поддерживает клиент):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"model": "<model_name>",
|
||||||
|
"message": { "role": "assistant", "content": "<string>" },
|
||||||
|
"done": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Прочие поля ответа (`total_duration`, `eval_count` и т.д.) допускаются, но клиент использует только `message.content`.
|
||||||
|
|
||||||
|
- **Ошибки**: любой статус != 200 считается ошибкой. Желательно возвращать читаемое тело.
|
||||||
|
|
||||||
|
### 2) GET `/api/tags`
|
||||||
|
|
||||||
|
- **Назначение**: используется как health-check и для получения списка моделей.
|
||||||
|
- **Ответ 200 OK**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"models": [
|
||||||
|
{ "name": "llama3:8b", "modified_at": "2024-01-01T00:00:00Z", "size": 123456789 },
|
||||||
|
{ "name": "qwen2.5:7b", "modified_at": "2024-01-02T00:00:00Z", "size": 987654321 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Любой другой статус трактуется как ошибка health.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Семантика сообщений
|
||||||
|
|
||||||
|
- `messages` — массив объектов `{ "role": "system"|"user"|"assistant", "content": "<string>" }`.
|
||||||
|
- Клиент всегда отправляет как минимум 2 сообщения: системное и пользовательское.
|
||||||
|
- Ответ должен содержать один финальный текст в виде `response` (proxy) или `message.content` (ollama).
|
||||||
|
|
||||||
|
## Поведение при таймаутах
|
||||||
|
|
||||||
|
- Сервер должен завершать запрос в пределах `config.AppConfig.Timeout` секунд (значение передаётся клиентом в настройки HTTP-клиента; отдельным полем в запросе оно не отправляется, исключение — `proxy` может возвращать `timeout_seconds` в ответе как справочную информацию).
|
||||||
|
|
||||||
|
## Коды ответов и ошибки
|
||||||
|
|
||||||
|
- 200 — успешный ответ с телом согласно контракту.
|
||||||
|
- !=200 — ошибка; тело желательно в JSON с полем `error`.
|
||||||
|
|
||||||
|
## Изменения контракта
|
||||||
|
|
||||||
|
- Добавление новых полей в ответах, не используемых клиентом, допустимо при сохранении существующих.
|
||||||
|
- Переименование или удаление полей `response` (proxy) и `message.content` (ollama) нарушит совместимость.
|
||||||
@@ -149,6 +149,33 @@ lcg [глобальные опции] <описание команды>
|
|||||||
- Основные эндпоинты: `/api/v1/protected/sberchat/chat` и `/api/v1/protected/sberchat/health`.
|
- Основные эндпоинты: `/api/v1/protected/sberchat/chat` и `/api/v1/protected/sberchat/health`.
|
||||||
- Команды `update-jwt`/`delete-jwt` помогают управлять токеном локально.
|
- Команды `update-jwt`/`delete-jwt` помогают управлять токеном локально.
|
||||||
|
|
||||||
|
## Рекомендации по выбору провайдера, модели и таймаутов
|
||||||
|
|
||||||
|
### Выбор провайдера
|
||||||
|
|
||||||
|
- **Ollama**: выбирайте для локальной работы (офлайн/частные данные), когда есть доступ к GPU/CPU и готовность поддерживать локальные модели. Минимальные задержки сети, полная приватность.
|
||||||
|
- **Proxy**: выбирайте для централизованного хостинга моделей, более мощных/обновляемых моделей, простоты развёртывания у команды. Обязательно используйте HTTPS и корректный `JWT`.
|
||||||
|
|
||||||
|
### Выбор модели
|
||||||
|
|
||||||
|
- Для генерации Linux‑команд подходят компактные «code»/«general» модели (по умолчанию `codegeex4`).
|
||||||
|
- Для подробных объяснений (`v`/`vv`/`vvv`) точность выше у более крупных моделей (например, семейства LLaMA/Qwen/GigaChat), но они медленнее.
|
||||||
|
- Русскоязычные запросы часто лучше обрабатываются в `GigaChat-*` (режим proxy), английские — в популярных open‑source (Ollama).
|
||||||
|
- Балансируйте: скорость (малые модели) vs качество (крупные модели). Тестируйте `lcg models` и подбирайте `LCG_MODEL`.
|
||||||
|
|
||||||
|
### Таймауты
|
||||||
|
|
||||||
|
- Стартовые значения: локально с Ollama — **60–120 сек**, удалённый proxy — **120–300 сек**.
|
||||||
|
- Увеличьте таймаут для больших моделей/длинных запросов. Флаг `--timeout` перекрывает `LCG_TIMEOUT` на время запуска.
|
||||||
|
- Если часто видите таймауты — проверьте здоровье API (`lcg health`) и сетевую доступность `LCG_HOST`.
|
||||||
|
|
||||||
|
### Практические советы
|
||||||
|
|
||||||
|
- Если данные чувствительные — используйте Ollama локально и `--no-history` при необходимости.
|
||||||
|
- Для «черновой» команды начните с `Ollama + небольшая модель`; для «объяснений и альтернатив» используйте более крупную модель/Proxy.
|
||||||
|
- Не вставляйте секреты в запросы. Перед выполнением (`e`) проверяйте команду вручную.
|
||||||
|
- Для структуры API см. `API_CONTRACT.md` (эндпоинты и форматы запросов/ответов).
|
||||||
|
|
||||||
## Системные промпты
|
## Системные промпты
|
||||||
|
|
||||||
Встроенные (ID 1–5):
|
Встроенные (ID 1–5):
|
||||||
|
|||||||
107
cmd/explain.go
Normal file
107
cmd/explain.go
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/atotto/clipboard"
|
||||||
|
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||||
|
"github.com/direct-dev-ru/linux-command-gpt/gpt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ExplainDeps инъекция зависимостей для вывода и окружения
|
||||||
|
type ExplainDeps struct {
|
||||||
|
DisableHistory bool
|
||||||
|
PrintColored func(string, string)
|
||||||
|
ColorPurple string
|
||||||
|
ColorGreen string
|
||||||
|
ColorRed string
|
||||||
|
ColorYellow string
|
||||||
|
GetCommand func(gpt.Gpt3, string) (string, float64)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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-инженер. Дай подробное объяснение команды с полным разбором ключей, подкоманд, сценариев применения, примеров. Затем предложи альтернативные способы решения задачи другой командой/инструментами (со сравнениями и когда что лучше применять). Пиши на русском."
|
||||||
|
}
|
||||||
|
|
||||||
|
ask := fmt.Sprintf("Объясни подробно команду и предложи альтернативы. Исходная команда: %s. Исходное задание пользователя: %s", command, originalCmd)
|
||||||
|
detailed := gpt.NewGpt3(gpt3.ProviderType, config.AppConfig.Host, gpt3.ApiKey, gpt3.Model, detailedSystem, 0.2, timeout)
|
||||||
|
|
||||||
|
deps.PrintColored("\n🧠 Получаю подробное объяснение...\n", deps.ColorPurple)
|
||||||
|
explanation, elapsed := deps.GetCommand(*detailed, ask)
|
||||||
|
if explanation == "" {
|
||||||
|
deps.PrintColored("❌ Не удалось получить подробное объяснение.\n", deps.ColorRed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
deps.PrintColored(fmt.Sprintf("✅ Готово за %.2f сек\n", elapsed), deps.ColorGreen)
|
||||||
|
deps.PrintColored("\nВНИМАНИЕ: ОТВЕТ СФОРМИРОВАН ИИ. ТРЕБУЕТСЯ ПРОВЕРКА И КРИТИЧЕСКИЙ АНАЛИЗ. ВОЗМОЖНЫ ОШИБКИ И ГАЛЛЮЦИНАЦИИ.\n", deps.ColorRed)
|
||||||
|
deps.PrintColored("\n📖 Подробное объяснение и альтернативы:\n\n", deps.ColorYellow)
|
||||||
|
fmt.Println(explanation)
|
||||||
|
|
||||||
|
fmt.Printf("\nДействия: (c)копировать, (s)сохранить, (r)перегенерировать, (n)ничего: ")
|
||||||
|
var choice string
|
||||||
|
fmt.Scanln(&choice)
|
||||||
|
switch strings.ToLower(choice) {
|
||||||
|
case "c":
|
||||||
|
clipboard.WriteAll(explanation)
|
||||||
|
fmt.Println("✅ Объяснение скопировано в буфер обмена")
|
||||||
|
case "s":
|
||||||
|
saveExplanation(explanation, gpt3.Model, originalCmd, command, config.AppConfig.ResultFolder)
|
||||||
|
case "r":
|
||||||
|
fmt.Println("🔄 Перегенерирую подробное объяснение...")
|
||||||
|
ShowDetailedExplanation(command, gpt3, system, originalCmd, timeout, level, deps)
|
||||||
|
default:
|
||||||
|
fmt.Println(" Возврат в основное меню.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !deps.DisableHistory && (strings.ToLower(choice) == "c" || strings.ToLower(choice) == "s" || strings.ToLower(choice) == "n") {
|
||||||
|
SaveToHistory(config.AppConfig.ResultHistory, config.AppConfig.ResultFolder, originalCmd, command, system, explanation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveExplanation сохраняет подробное объяснение и альтернативные способы
|
||||||
|
func saveExplanation(explanation string, model string, originalCmd string, commandResponse string, resultFolder string) {
|
||||||
|
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
||||||
|
filename := fmt.Sprintf("gpt_explanation_%s_%s.md", model, timestamp)
|
||||||
|
filePath := path.Join(resultFolder, filename)
|
||||||
|
title := truncateTitle(originalCmd)
|
||||||
|
content := fmt.Sprintf(
|
||||||
|
"# %s\n\n## Prompt\n\n%s\n\n## Command\n\n%s\n\n## Explanation and Alternatives (model: %s)\n\n%s\n",
|
||||||
|
title,
|
||||||
|
originalCmd,
|
||||||
|
commandResponse,
|
||||||
|
model,
|
||||||
|
explanation,
|
||||||
|
)
|
||||||
|
if err := os.WriteFile(filePath, []byte(content), 0644); err != nil {
|
||||||
|
fmt.Println("Failed to save explanation:", err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Saved to %s\n", filePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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]) + " ..."
|
||||||
|
}
|
||||||
157
cmd/history.go
Normal file
157
cmd/history.go
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ShowHistory(historyPath string, printColored func(string, string), colorYellow string) {
|
||||||
|
items, err := read(historyPath)
|
||||||
|
if err != nil || len(items) == 0 {
|
||||||
|
printColored("📝 История пуста\n", colorYellow)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
printColored("📝 История (из файла):\n", colorYellow)
|
||||||
|
for _, h := range items {
|
||||||
|
ts := h.Timestamp.Format("2006-01-02 15:04:05")
|
||||||
|
fmt.Printf("%d. [%s] %s → %s\n", h.Index, ts, h.Command, h.Response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ViewHistoryEntry(historyPath string, id int, printColored func(string, string), colorYellow, colorBold, colorGreen string) {
|
||||||
|
items, err := read(historyPath)
|
||||||
|
if err != nil || len(items) == 0 {
|
||||||
|
fmt.Println("История пуста или недоступна")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var h *HistoryEntry
|
||||||
|
for i := range items {
|
||||||
|
if items[i].Index == id {
|
||||||
|
h = &items[i]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if h == nil {
|
||||||
|
fmt.Println("Запись не найдена")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
printColored("\n📋 Команда:\n", colorYellow)
|
||||||
|
printColored(fmt.Sprintf(" %s\n\n", h.Response), colorBold+colorGreen)
|
||||||
|
if strings.TrimSpace(h.Explanation) != "" {
|
||||||
|
printColored("\n📖 Подробное объяснение:\n\n", colorYellow)
|
||||||
|
fmt.Println(h.Explanation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SaveToHistory(historyPath, resultFolder, cmdText, response, system string, explanationOptional ...string) error {
|
||||||
|
var explanation string
|
||||||
|
if len(explanationOptional) > 0 {
|
||||||
|
explanation = explanationOptional[0]
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
fmt.Printf("\nЗапрос уже есть в истории от %s. Перезаписать? (y/N): ", items[duplicateIndex].Timestamp.Format("2006-01-02 15:04:05"))
|
||||||
|
var ans string
|
||||||
|
fmt.Scanln(&ans)
|
||||||
|
if strings.ToLower(ans) == "y" || strings.ToLower(ans) == "yes" {
|
||||||
|
entry.Index = items[duplicateIndex].Index
|
||||||
|
items[duplicateIndex] = entry
|
||||||
|
return write(historyPath, items)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckAndSuggestFromHistory(historyPath, cmdText string) (bool, *HistoryEntry) {
|
||||||
|
items, err := read(historyPath)
|
||||||
|
if err != nil || len(items) == 0 {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
for _, h := range items {
|
||||||
|
if strings.EqualFold(strings.TrimSpace(h.Command), strings.TrimSpace(cmdText)) {
|
||||||
|
fmt.Printf("\nВ истории найден похожий запрос от %s. Показать сохраненный результат? (y/N): ", h.Timestamp.Format("2006-01-02 15:04:05"))
|
||||||
|
var ans string
|
||||||
|
fmt.Scanln(&ans)
|
||||||
|
if strings.ToLower(ans) == "y" || strings.ToLower(ans) == "yes" {
|
||||||
|
return true, &h
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
83
config/config.go
Normal file
83
config/config.go
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Cwd string
|
||||||
|
Host string
|
||||||
|
ProxyUrl string
|
||||||
|
Completions string
|
||||||
|
Model string
|
||||||
|
Prompt string
|
||||||
|
ApiKeyFile string
|
||||||
|
ResultFolder string
|
||||||
|
ProviderType string
|
||||||
|
JwtToken string
|
||||||
|
PromptID string
|
||||||
|
Timeout string
|
||||||
|
ResultHistory string
|
||||||
|
NoHistoryEnv string
|
||||||
|
MainFlags MainFlags
|
||||||
|
}
|
||||||
|
|
||||||
|
type MainFlags struct {
|
||||||
|
File string
|
||||||
|
NoHistory bool
|
||||||
|
Sys string
|
||||||
|
PromptID int
|
||||||
|
Timeout int
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEnv(key, defaultValue string) string {
|
||||||
|
if value, exists := os.LookupEnv(key); exists {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func Load() Config {
|
||||||
|
cwd, _ := os.Getwd()
|
||||||
|
|
||||||
|
homedir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
homedir = cwd
|
||||||
|
}
|
||||||
|
os.MkdirAll(path.Join(homedir, ".config", "lcg", "gpt_results"), 0755)
|
||||||
|
resultFolder := getEnv("LCG_RESULT_FOLDER", path.Join(homedir, ".config", "lcg", "gpt_results"))
|
||||||
|
|
||||||
|
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,
|
||||||
|
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", ""),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Config) IsNoHistoryEnabled() bool {
|
||||||
|
v := strings.TrimSpace(c.NoHistoryEnv)
|
||||||
|
if v == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
vLower := strings.ToLower(v)
|
||||||
|
return vLower == "1" || vLower == "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
var AppConfig Config
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
AppConfig = Load()
|
||||||
|
}
|
||||||
476
main.go
476
main.go
@@ -2,18 +2,18 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"os/user"
|
"os/user"
|
||||||
"path"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/atotto/clipboard"
|
"github.com/atotto/clipboard"
|
||||||
|
cmdPackage "github.com/direct-dev-ru/linux-command-gpt/cmd"
|
||||||
|
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||||
"github.com/direct-dev-ru/linux-command-gpt/gpt"
|
"github.com/direct-dev-ru/linux-command-gpt/gpt"
|
||||||
"github.com/direct-dev-ru/linux-command-gpt/reader"
|
"github.com/direct-dev-ru/linux-command-gpt/reader"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
@@ -22,21 +22,7 @@ import (
|
|||||||
//go:embed VERSION.txt
|
//go:embed VERSION.txt
|
||||||
var Version string
|
var Version string
|
||||||
|
|
||||||
var (
|
// используем глобальный экземпляр конфига из пакета config
|
||||||
cwd, _ = os.Getwd()
|
|
||||||
HOST = getEnv("LCG_HOST", "http://192.168.87.108:11434/")
|
|
||||||
COMPLETIONS = getEnv("LCG_COMPLETIONS_PATH", "api/chat")
|
|
||||||
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"))
|
|
||||||
PROVIDER_TYPE = getEnv("LCG_PROVIDER", "ollama") // "ollama", "proxy"
|
|
||||||
JWT_TOKEN = getEnv("LCG_JWT_TOKEN", "")
|
|
||||||
PROMPT_ID = getEnv("LCG_PROMPT_ID", "1") // ID промпта по умолчанию
|
|
||||||
TIMEOUT = getEnv("LCG_TIMEOUT", "120") // Таймаут в секундах по умолчанию
|
|
||||||
RESULT_HISTORY = getEnv("LCG_RESULT_HISTORY", path.Join(RESULT_FOLDER, "lcg_history.json"))
|
|
||||||
NO_HISTORY_ENV = getEnv("LCG_NO_HISTORY", "")
|
|
||||||
)
|
|
||||||
|
|
||||||
// disableHistory управляет записью/обновлением истории на уровне процесса (флаг имеет приоритет над env)
|
// disableHistory управляет записью/обновлением истории на уровне процесса (флаг имеет приоритет над env)
|
||||||
var disableHistory bool
|
var disableHistory bool
|
||||||
@@ -53,6 +39,8 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
_ = colorBlue
|
||||||
|
|
||||||
app := &cli.App{
|
app := &cli.App{
|
||||||
Name: "lcg",
|
Name: "lcg",
|
||||||
Usage: "Linux Command GPT - Генерация Linux команд из описаний",
|
Usage: "Linux Command GPT - Генерация Linux команд из описаний",
|
||||||
@@ -68,7 +56,7 @@ lcg [опции] <описание команды>
|
|||||||
Description: `
|
Description: `
|
||||||
Linux Command GPT - инструмент для генерации Linux команд из описаний на естественном языке.
|
Linux Command GPT - инструмент для генерации Linux команд из описаний на естественном языке.
|
||||||
Поддерживает чтение частей промпта из файлов и позволяет сохранять, копировать или перегенерировать результаты.
|
Поддерживает чтение частей промпта из файлов и позволяет сохранять, копировать или перегенерировать результаты.
|
||||||
|
может задавать системный промпт или выбирать из предустановленных промптов.
|
||||||
Переменные окружения:
|
Переменные окружения:
|
||||||
LCG_HOST Endpoint для LLM API (по умолчанию: http://192.168.87.108:11434/)
|
LCG_HOST Endpoint для LLM API (по умолчанию: http://192.168.87.108:11434/)
|
||||||
LCG_MODEL Название модели (по умолчанию: codegeex4)
|
LCG_MODEL Название модели (по умолчанию: codegeex4)
|
||||||
@@ -113,9 +101,24 @@ Linux Command GPT - инструмент для генерации Linux ком
|
|||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
file := c.String("file")
|
file := c.String("file")
|
||||||
system := c.String("sys")
|
system := c.String("sys")
|
||||||
disableHistory = c.Bool("no-history") || isNoHistoryEnv()
|
// обновляем конфиг на основе флагов
|
||||||
|
if system != "" {
|
||||||
|
config.AppConfig.Prompt = system
|
||||||
|
}
|
||||||
|
if c.IsSet("timeout") {
|
||||||
|
config.AppConfig.Timeout = fmt.Sprintf("%d", c.Int("timeout"))
|
||||||
|
}
|
||||||
promptID := c.Int("prompt-id")
|
promptID := c.Int("prompt-id")
|
||||||
timeout := c.Int("timeout")
|
timeout := c.Int("timeout")
|
||||||
|
// сохраняем конкретные значения флагов
|
||||||
|
config.AppConfig.MainFlags = config.MainFlags{
|
||||||
|
File: file,
|
||||||
|
NoHistory: c.Bool("no-history"),
|
||||||
|
Sys: system,
|
||||||
|
PromptID: promptID,
|
||||||
|
Timeout: timeout,
|
||||||
|
}
|
||||||
|
disableHistory = config.AppConfig.MainFlags.NoHistory || config.AppConfig.IsNoHistoryEnabled()
|
||||||
args := c.Args().Slice()
|
args := c.Args().Slice()
|
||||||
|
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
@@ -162,15 +165,15 @@ func getCommands() []*cli.Command {
|
|||||||
Aliases: []string{"u"},
|
Aliases: []string{"u"},
|
||||||
Usage: "Update the API key",
|
Usage: "Update the API key",
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
if PROVIDER_TYPE == "ollama" || PROVIDER_TYPE == "proxy" {
|
if config.AppConfig.ProviderType == "ollama" || config.AppConfig.ProviderType == "proxy" {
|
||||||
fmt.Println("API key is not needed for ollama and proxy providers")
|
fmt.Println("API key is not needed for ollama and proxy providers")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
timeout := 120 // default timeout
|
timeout := 120 // default timeout
|
||||||
if t, err := strconv.Atoi(TIMEOUT); err == nil {
|
if t, err := strconv.Atoi(config.AppConfig.Timeout); err == nil {
|
||||||
timeout = t
|
timeout = t
|
||||||
}
|
}
|
||||||
gpt3 := initGPT(PROMPT, timeout)
|
gpt3 := initGPT(config.AppConfig.Prompt, timeout)
|
||||||
gpt3.UpdateKey()
|
gpt3.UpdateKey()
|
||||||
fmt.Println("API key updated.")
|
fmt.Println("API key updated.")
|
||||||
return nil
|
return nil
|
||||||
@@ -181,15 +184,15 @@ func getCommands() []*cli.Command {
|
|||||||
Aliases: []string{"d"},
|
Aliases: []string{"d"},
|
||||||
Usage: "Delete the API key",
|
Usage: "Delete the API key",
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
if PROVIDER_TYPE == "ollama" || PROVIDER_TYPE == "proxy" {
|
if config.AppConfig.ProviderType == "ollama" || config.AppConfig.ProviderType == "proxy" {
|
||||||
fmt.Println("API key is not needed for ollama and proxy providers")
|
fmt.Println("API key is not needed for ollama and proxy providers")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
timeout := 120 // default timeout
|
timeout := 120 // default timeout
|
||||||
if t, err := strconv.Atoi(TIMEOUT); err == nil {
|
if t, err := strconv.Atoi(config.AppConfig.Timeout); err == nil {
|
||||||
timeout = t
|
timeout = t
|
||||||
}
|
}
|
||||||
gpt3 := initGPT(PROMPT, timeout)
|
gpt3 := initGPT(config.AppConfig.Prompt, timeout)
|
||||||
gpt3.DeleteKey()
|
gpt3.DeleteKey()
|
||||||
fmt.Println("API key deleted.")
|
fmt.Println("API key deleted.")
|
||||||
return nil
|
return nil
|
||||||
@@ -200,7 +203,7 @@ func getCommands() []*cli.Command {
|
|||||||
Aliases: []string{"j"},
|
Aliases: []string{"j"},
|
||||||
Usage: "Update the JWT token for proxy API",
|
Usage: "Update the JWT token for proxy API",
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
if PROVIDER_TYPE != "proxy" {
|
if config.AppConfig.ProviderType != "proxy" {
|
||||||
fmt.Println("JWT token is only needed for proxy provider")
|
fmt.Println("JWT token is only needed for proxy provider")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -225,7 +228,7 @@ func getCommands() []*cli.Command {
|
|||||||
Aliases: []string{"dj"},
|
Aliases: []string{"dj"},
|
||||||
Usage: "Delete the JWT token for proxy API",
|
Usage: "Delete the JWT token for proxy API",
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
if PROVIDER_TYPE != "proxy" {
|
if config.AppConfig.ProviderType != "proxy" {
|
||||||
fmt.Println("JWT token is only needed for proxy provider")
|
fmt.Println("JWT token is only needed for proxy provider")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -247,17 +250,17 @@ func getCommands() []*cli.Command {
|
|||||||
Usage: "Show available models",
|
Usage: "Show available models",
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
timeout := 120 // default timeout
|
timeout := 120 // default timeout
|
||||||
if t, err := strconv.Atoi(TIMEOUT); err == nil {
|
if t, err := strconv.Atoi(config.AppConfig.Timeout); err == nil {
|
||||||
timeout = t
|
timeout = t
|
||||||
}
|
}
|
||||||
gpt3 := initGPT(PROMPT, timeout)
|
gpt3 := initGPT(config.AppConfig.Prompt, timeout)
|
||||||
models, err := gpt3.GetAvailableModels()
|
models, err := gpt3.GetAvailableModels()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Ошибка получения моделей: %v\n", err)
|
fmt.Printf("Ошибка получения моделей: %v\n", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Доступные модели для провайдера %s:\n", PROVIDER_TYPE)
|
fmt.Printf("Доступные модели для провайдера %s:\n", config.AppConfig.ProviderType)
|
||||||
for i, model := range models {
|
for i, model := range models {
|
||||||
fmt.Printf(" %d. %s\n", i+1, model)
|
fmt.Printf(" %d. %s\n", i+1, model)
|
||||||
}
|
}
|
||||||
@@ -270,10 +273,10 @@ func getCommands() []*cli.Command {
|
|||||||
Usage: "Check API health",
|
Usage: "Check API health",
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
timeout := 120 // default timeout
|
timeout := 120 // default timeout
|
||||||
if t, err := strconv.Atoi(TIMEOUT); err == nil {
|
if t, err := strconv.Atoi(config.AppConfig.Timeout); err == nil {
|
||||||
timeout = t
|
timeout = t
|
||||||
}
|
}
|
||||||
gpt3 := initGPT(PROMPT, timeout)
|
gpt3 := initGPT(config.AppConfig.Prompt, timeout)
|
||||||
if err := gpt3.Health(); err != nil {
|
if err := gpt3.Health(); err != nil {
|
||||||
fmt.Printf("Health check failed: %v\n", err)
|
fmt.Printf("Health check failed: %v\n", err)
|
||||||
return err
|
return err
|
||||||
@@ -287,14 +290,14 @@ func getCommands() []*cli.Command {
|
|||||||
Aliases: []string{"co"}, // Изменено с "c" на "co"
|
Aliases: []string{"co"}, // Изменено с "c" на "co"
|
||||||
Usage: "Show current configuration",
|
Usage: "Show current configuration",
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
fmt.Printf("Provider: %s\n", PROVIDER_TYPE)
|
fmt.Printf("Provider: %s\n", config.AppConfig.ProviderType)
|
||||||
fmt.Printf("Host: %s\n", HOST)
|
fmt.Printf("Host: %s\n", config.AppConfig.Host)
|
||||||
fmt.Printf("Model: %s\n", MODEL)
|
fmt.Printf("Model: %s\n", config.AppConfig.Model)
|
||||||
fmt.Printf("Prompt: %s\n", PROMPT)
|
fmt.Printf("Prompt: %s\n", config.AppConfig.Prompt)
|
||||||
fmt.Printf("Timeout: %s seconds\n", TIMEOUT)
|
fmt.Printf("Timeout: %s seconds\n", config.AppConfig.Timeout)
|
||||||
if PROVIDER_TYPE == "proxy" {
|
if config.AppConfig.ProviderType == "proxy" {
|
||||||
fmt.Printf("JWT Token: %s\n", func() string {
|
fmt.Printf("JWT Token: %s\n", func() string {
|
||||||
if JWT_TOKEN != "" {
|
if config.AppConfig.JwtToken != "" {
|
||||||
return "***set***"
|
return "***set***"
|
||||||
}
|
}
|
||||||
currentUser, _ := user.Current()
|
currentUser, _ := user.Current()
|
||||||
@@ -318,7 +321,11 @@ func getCommands() []*cli.Command {
|
|||||||
Aliases: []string{"l"},
|
Aliases: []string{"l"},
|
||||||
Usage: "List history entries",
|
Usage: "List history entries",
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
showHistory()
|
if disableHistory {
|
||||||
|
printColored("📝 История отключена (--no-history / LCG_NO_HISTORY)\n", colorYellow)
|
||||||
|
} else {
|
||||||
|
cmdPackage.ShowHistory(config.AppConfig.ResultHistory, printColored, colorYellow)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -336,7 +343,11 @@ func getCommands() []*cli.Command {
|
|||||||
fmt.Println("Неверный ID")
|
fmt.Println("Неверный ID")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
viewHistoryEntry(id)
|
if disableHistory {
|
||||||
|
fmt.Println("История отключена")
|
||||||
|
} else {
|
||||||
|
cmdPackage.ViewHistoryEntry(config.AppConfig.ResultHistory, id, printColored, colorYellow, colorBold, colorGreen)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -354,7 +365,11 @@ func getCommands() []*cli.Command {
|
|||||||
fmt.Println("Неверный ID")
|
fmt.Println("Неверный ID")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
deleteHistoryEntry(id)
|
if disableHistory {
|
||||||
|
fmt.Println("История отключена")
|
||||||
|
} else if err := cmdPackage.DeleteHistoryEntry(config.AppConfig.ResultHistory, id); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -467,7 +482,7 @@ func getCommands() []*cli.Command {
|
|||||||
command := strings.Join(c.Args().Slice()[1:], " ")
|
command := strings.Join(c.Args().Slice()[1:], " ")
|
||||||
fmt.Printf("\nTesting with command: %s\n", command)
|
fmt.Printf("\nTesting with command: %s\n", command)
|
||||||
timeout := 120 // default timeout
|
timeout := 120 // default timeout
|
||||||
if t, err := strconv.Atoi(TIMEOUT); err == nil {
|
if t, err := strconv.Atoi(config.AppConfig.Timeout); err == nil {
|
||||||
timeout = t
|
timeout = t
|
||||||
}
|
}
|
||||||
executeMain("", prompt.Content, command, timeout)
|
executeMain("", prompt.Content, command, timeout)
|
||||||
@@ -489,12 +504,12 @@ func executeMain(file, system, commandInput string, timeout int) {
|
|||||||
|
|
||||||
// Если system пустой, используем дефолтный промпт
|
// Если system пустой, используем дефолтный промпт
|
||||||
if system == "" {
|
if system == "" {
|
||||||
system = PROMPT
|
system = config.AppConfig.Prompt
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обеспечим папку результатов заранее (может понадобиться при действиях)
|
// Обеспечим папку результатов заранее (может понадобиться при действиях)
|
||||||
if _, err := os.Stat(RESULT_FOLDER); os.IsNotExist(err) {
|
if _, err := os.Stat(config.AppConfig.ResultFolder); os.IsNotExist(err) {
|
||||||
if err := os.MkdirAll(RESULT_FOLDER, 0755); err != nil {
|
if err := os.MkdirAll(config.AppConfig.ResultFolder, 0755); err != nil {
|
||||||
printColored(fmt.Sprintf("❌ Ошибка создания папки результатов: %v\n", err), colorRed)
|
printColored(fmt.Sprintf("❌ Ошибка создания папки результатов: %v\n", err), colorRed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -502,7 +517,7 @@ func executeMain(file, system, commandInput string, timeout int) {
|
|||||||
|
|
||||||
// Проверка истории: если такой запрос уже встречался — предложить открыть из истории
|
// Проверка истории: если такой запрос уже встречался — предложить открыть из истории
|
||||||
if !disableHistory {
|
if !disableHistory {
|
||||||
if found, hist := checkAndSuggestFromHistory(commandInput); found && hist != nil {
|
if found, hist := cmdPackage.CheckAndSuggestFromHistory(config.AppConfig.ResultHistory, commandInput); found && hist != nil {
|
||||||
gpt3 := initGPT(system, timeout)
|
gpt3 := initGPT(system, timeout)
|
||||||
printColored("\nВНИМАНИЕ: ОТВЕТ СФОРМИРОВАН ИИ. ТРЕБУЕТСЯ ПРОВЕРКА И КРИТИЧЕСКИЙ АНАЛИЗ. ВОЗМОЖНЫ ОШИБКИ И ГАЛЛЮЦИНАЦИИ.\n", colorRed)
|
printColored("\nВНИМАНИЕ: ОТВЕТ СФОРМИРОВАН ИИ. ТРЕБУЕТСЯ ПРОВЕРКА И КРИТИЧЕСКИЙ АНАЛИЗ. ВОЗМОЖНЫ ОШИБКИ И ГАЛЛЮЦИНАЦИИ.\n", colorRed)
|
||||||
printColored("\n📋 Команда (из истории):\n", colorYellow)
|
printColored("\n📋 Команда (из истории):\n", colorYellow)
|
||||||
@@ -542,39 +557,15 @@ func executeMain(file, system, commandInput string, timeout int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// checkAndSuggestFromHistory проверяет файл истории и при совпадении запроса предлагает показать сохраненный результат
|
// checkAndSuggestFromHistory проверяет файл истории и при совпадении запроса предлагает показать сохраненный результат
|
||||||
func checkAndSuggestFromHistory(cmd string) (bool, *CommandHistory) {
|
// moved to history.go
|
||||||
if disableHistory {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
data, err := os.ReadFile(RESULT_HISTORY)
|
|
||||||
if err != nil || len(data) == 0 {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
var fileHistory []CommandHistory
|
|
||||||
if err := json.Unmarshal(data, &fileHistory); err != nil {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
for _, h := range fileHistory {
|
|
||||||
if strings.TrimSpace(strings.ToLower(h.Command)) == strings.TrimSpace(strings.ToLower(cmd)) {
|
|
||||||
fmt.Printf("\nВ истории найден похожий запрос от %s. Показать сохраненный результат? (y/N): ", h.Timestamp.Format("2006-01-02 15:04:05"))
|
|
||||||
var ans string
|
|
||||||
fmt.Scanln(&ans)
|
|
||||||
if strings.ToLower(ans) == "y" || strings.ToLower(ans) == "yes" {
|
|
||||||
return true, &h
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func initGPT(system string, timeout int) gpt.Gpt3 {
|
func initGPT(system string, timeout int) gpt.Gpt3 {
|
||||||
currentUser, _ := user.Current()
|
currentUser, _ := user.Current()
|
||||||
|
|
||||||
// Загружаем JWT токен в зависимости от провайдера
|
// Загружаем JWT токен в зависимости от провайдера
|
||||||
var jwtToken string
|
var jwtToken string
|
||||||
if PROVIDER_TYPE == "proxy" {
|
if config.AppConfig.ProviderType == "proxy" {
|
||||||
jwtToken = JWT_TOKEN
|
jwtToken = config.AppConfig.JwtToken
|
||||||
if jwtToken == "" {
|
if jwtToken == "" {
|
||||||
// Пытаемся загрузить из файла
|
// Пытаемся загрузить из файла
|
||||||
jwtFile := currentUser.HomeDir + "/.proxy_jwt_token"
|
jwtFile := currentUser.HomeDir + "/.proxy_jwt_token"
|
||||||
@@ -584,7 +575,7 @@ func initGPT(system string, timeout int) gpt.Gpt3 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return *gpt.NewGpt3(PROVIDER_TYPE, HOST, jwtToken, MODEL, system, 0.01, timeout)
|
return *gpt.NewGpt3(config.AppConfig.ProviderType, config.AppConfig.Host, jwtToken, config.AppConfig.Model, system, 0.01, timeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCommand(gpt3 gpt.Gpt3, cmd string) (string, float64) {
|
func getCommand(gpt3 gpt.Gpt3, cmd string) (string, float64) {
|
||||||
@@ -626,12 +617,12 @@ func handlePostResponse(response string, gpt3 gpt.Gpt3, system, cmd string, time
|
|||||||
clipboard.WriteAll(response)
|
clipboard.WriteAll(response)
|
||||||
fmt.Println("✅ Команда скопирована в буфер обмена")
|
fmt.Println("✅ Команда скопирована в буфер обмена")
|
||||||
if !disableHistory {
|
if !disableHistory {
|
||||||
saveToHistory(cmd, response, gpt3.Prompt)
|
cmdPackage.SaveToHistory(config.AppConfig.ResultHistory, config.AppConfig.ResultFolder, cmd, response, gpt3.Prompt)
|
||||||
}
|
}
|
||||||
case "s":
|
case "s":
|
||||||
saveResponse(response, gpt3, cmd)
|
saveResponse(response, gpt3.Model, gpt3.Prompt, cmd)
|
||||||
if !disableHistory {
|
if !disableHistory {
|
||||||
saveToHistory(cmd, response, gpt3.Prompt)
|
cmdPackage.SaveToHistory(config.AppConfig.ResultHistory, config.AppConfig.ResultFolder, cmd, response, gpt3.Prompt)
|
||||||
}
|
}
|
||||||
case "r":
|
case "r":
|
||||||
fmt.Println("🔄 Перегенерирую...")
|
fmt.Println("🔄 Перегенерирую...")
|
||||||
@@ -639,125 +630,37 @@ func handlePostResponse(response string, gpt3 gpt.Gpt3, system, cmd string, time
|
|||||||
case "e":
|
case "e":
|
||||||
executeCommand(response)
|
executeCommand(response)
|
||||||
if !disableHistory {
|
if !disableHistory {
|
||||||
saveToHistory(cmd, response, gpt3.Prompt)
|
cmdPackage.SaveToHistory(config.AppConfig.ResultHistory, config.AppConfig.ResultFolder, cmd, response, gpt3.Prompt)
|
||||||
}
|
}
|
||||||
case "v", "vv", "vvv":
|
case "v", "vv", "vvv":
|
||||||
level := len(choice) // 1, 2, 3
|
level := len(choice) // 1, 2, 3
|
||||||
showDetailedExplanation(response, gpt3, system, cmd, timeout, level)
|
deps := cmdPackage.ExplainDeps{
|
||||||
|
DisableHistory: disableHistory,
|
||||||
|
PrintColored: printColored,
|
||||||
|
ColorPurple: colorPurple,
|
||||||
|
ColorGreen: colorGreen,
|
||||||
|
ColorRed: colorRed,
|
||||||
|
ColorYellow: colorYellow,
|
||||||
|
GetCommand: getCommand,
|
||||||
|
}
|
||||||
|
cmdPackage.ShowDetailedExplanation(response, gpt3, system, cmd, timeout, level, deps)
|
||||||
default:
|
default:
|
||||||
fmt.Println(" До свидания!")
|
fmt.Println(" До свидания!")
|
||||||
if !disableHistory {
|
if !disableHistory {
|
||||||
saveToHistory(cmd, response, gpt3.Prompt)
|
cmdPackage.SaveToHistory(config.AppConfig.ResultHistory, config.AppConfig.ResultFolder, cmd, response, gpt3.Prompt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveResponse(response string, gpt3 gpt.Gpt3, cmd string) {
|
// moved to response.go
|
||||||
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
|
||||||
filename := fmt.Sprintf("gpt_request_%s_%s.md", gpt3.Model, timestamp)
|
|
||||||
filePath := path.Join(RESULT_FOLDER, filename)
|
|
||||||
// Заголовок — сокращенный текст запроса пользователя
|
|
||||||
title := truncateTitle(cmd)
|
|
||||||
content := fmt.Sprintf("# %s\n\n## Prompt\n\n%s\n\n## Response\n\n%s\n", title, cmd+". "+gpt3.Prompt, response)
|
|
||||||
|
|
||||||
if err := os.WriteFile(filePath, []byte(content), 0644); err != nil {
|
|
||||||
fmt.Println("Failed to save response:", err)
|
|
||||||
} else {
|
|
||||||
fmt.Printf("Response saved to %s\n", filePath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// saveExplanation сохраняет подробное объяснение и альтернативные способы
|
// saveExplanation сохраняет подробное объяснение и альтернативные способы
|
||||||
func saveExplanation(explanation string, model string, originalCmd string, commandResponse string) {
|
// moved to explain.go
|
||||||
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
|
||||||
filename := fmt.Sprintf("gpt_explanation_%s_%s.md", model, timestamp)
|
|
||||||
filePath := path.Join(RESULT_FOLDER, filename)
|
|
||||||
title := truncateTitle(originalCmd)
|
|
||||||
content := fmt.Sprintf(
|
|
||||||
"# %s\n\n## Prompt\n\n%s\n\n## Command\n\n%s\n\n## Explanation and Alternatives (model: %s)\n\n%s\n",
|
|
||||||
title,
|
|
||||||
originalCmd,
|
|
||||||
commandResponse,
|
|
||||||
model,
|
|
||||||
explanation,
|
|
||||||
)
|
|
||||||
|
|
||||||
if err := os.WriteFile(filePath, []byte(content), 0644); err != nil {
|
|
||||||
fmt.Println("Failed to save explanation:", err)
|
|
||||||
} else {
|
|
||||||
fmt.Printf("Explanation saved to %s\n", filePath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// truncateTitle сокращает строку до 120 символов (по рунам), добавляя " ..." при усечении
|
// truncateTitle сокращает строку до 120 символов (по рунам), добавляя " ..." при усечении
|
||||||
func truncateTitle(s string) string {
|
// moved to response.go
|
||||||
const maxLen = 120
|
|
||||||
if runeCount := len([]rune(s)); runeCount <= maxLen {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
// взять первые 116 рунических символов и добавить " ..."
|
|
||||||
const head = 116
|
|
||||||
r := []rune(s)
|
|
||||||
if len(r) <= head {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
return string(r[:head]) + " ..."
|
|
||||||
}
|
|
||||||
|
|
||||||
// showDetailedExplanation делает дополнительный запрос с подробным описанием и альтернативами
|
// moved to explain.go
|
||||||
func showDetailedExplanation(command string, gpt3 gpt.Gpt3, system, originalCmd string, timeout int, level int) {
|
|
||||||
// Формируем системный промпт для подробного ответа (на русском)
|
|
||||||
var detailedSystem string
|
|
||||||
switch level {
|
|
||||||
case 1: // v — кратко
|
|
||||||
detailedSystem = "Ты опытный Linux-инженер. Объясни КРАТКО, по делу: что делает команда и самые важные ключи. Без сравнений и альтернатив. Минимум текста. Пиши на русском."
|
|
||||||
case 2: // vv — средне
|
|
||||||
detailedSystem = "Ты опытный Linux-инженер. Дай сбалансированное объяснение: назначение команды, разбор основных ключей, 1-2 примера. Кратко упомяни 1-2 альтернативы без глубокого сравнения. Пиши на русском."
|
|
||||||
default: // vvv — максимально подробно
|
|
||||||
detailedSystem = "Ты опытный Linux-инженер. Дай подробное объяснение команды с полным разбором ключей, подкоманд, сценариев применения, примеров. Затем предложи альтернативные способы решения задачи другой командой/инструментами (со сравнениями и когда что лучше применять). Пиши на русском."
|
|
||||||
}
|
|
||||||
|
|
||||||
// Текст запроса к модели
|
|
||||||
ask := fmt.Sprintf("Объясни подробно команду и предложи альтернативы. Исходная команда: %s. Исходное задание пользователя: %s", command, originalCmd)
|
|
||||||
|
|
||||||
// Создаем временный экземпляр с иным системным промптом
|
|
||||||
detailed := gpt.NewGpt3(gpt3.ProviderType, HOST, gpt3.ApiKey, gpt3.Model, detailedSystem, 0.2, timeout)
|
|
||||||
|
|
||||||
printColored("\n🧠 Получаю подробное объяснение...\n", colorPurple)
|
|
||||||
explanation, elapsed := getCommand(*detailed, ask)
|
|
||||||
if explanation == "" {
|
|
||||||
printColored("❌ Не удалось получить подробное объяснение.\n", colorRed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
printColored(fmt.Sprintf("✅ Готово за %.2f сек\n", elapsed), colorGreen)
|
|
||||||
// Обязательное предупреждение перед выводом подробного объяснения
|
|
||||||
printColored("\nВНИМАНИЕ: ОТВЕТ СФОРМИРОВАН ИИ. ТРЕБУЕТСЯ ПРОВЕРКА И КРИТИЧЕСКИЙ АНАЛИЗ. ВОЗМОЖНЫ ОШИБКИ И ГАЛЛЮЦИНАЦИИ.\n", colorRed)
|
|
||||||
printColored("\n📖 Подробное объяснение и альтернативы:\n\n", colorYellow)
|
|
||||||
fmt.Println(explanation)
|
|
||||||
|
|
||||||
// Вторичное меню действий
|
|
||||||
fmt.Printf("\nДействия: (c)копировать, (s)сохранить, (r)перегенерировать, (n)ничего: ")
|
|
||||||
var choice string
|
|
||||||
fmt.Scanln(&choice)
|
|
||||||
switch strings.ToLower(choice) {
|
|
||||||
case "c":
|
|
||||||
clipboard.WriteAll(explanation)
|
|
||||||
fmt.Println("✅ Объяснение скопировано в буфер обмена")
|
|
||||||
case "s":
|
|
||||||
saveExplanation(explanation, gpt3.Model, originalCmd, command)
|
|
||||||
case "r":
|
|
||||||
fmt.Println("🔄 Перегенерирую подробное объяснение...")
|
|
||||||
showDetailedExplanation(command, gpt3, system, originalCmd, timeout, level)
|
|
||||||
default:
|
|
||||||
fmt.Println(" Возврат в основное меню.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// После работы с объяснением — сохраняем запись в файл истории, но только если было действие не r
|
|
||||||
if !disableHistory && (strings.ToLower(choice) == "c" || strings.ToLower(choice) == "s" || strings.ToLower(choice) == "n") {
|
|
||||||
saveToHistory(originalCmd, command, system, explanation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func executeCommand(command string) {
|
func executeCommand(command string) {
|
||||||
fmt.Printf("🚀 Выполняю: %s\n", command)
|
fmt.Printf("🚀 Выполняю: %s\n", command)
|
||||||
@@ -780,214 +683,9 @@ func executeCommand(command string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getEnv(key, defaultValue string) string {
|
// env helpers moved to config package
|
||||||
if value, exists := os.LookupEnv(key); exists {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
|
|
||||||
func isNoHistoryEnv() bool {
|
// moved to history.go
|
||||||
v := strings.TrimSpace(NO_HISTORY_ENV)
|
|
||||||
vLower := strings.ToLower(v)
|
|
||||||
return vLower == "1" || vLower == "true"
|
|
||||||
}
|
|
||||||
|
|
||||||
type CommandHistory 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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var commandHistory []CommandHistory
|
|
||||||
|
|
||||||
func saveToHistory(cmd, response, system string, explanationOptional ...string) {
|
|
||||||
if disableHistory {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var explanation string
|
|
||||||
if len(explanationOptional) > 0 {
|
|
||||||
explanation = explanationOptional[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
entry := CommandHistory{
|
|
||||||
Index: len(commandHistory) + 1,
|
|
||||||
Command: cmd,
|
|
||||||
Response: response,
|
|
||||||
Explanation: explanation,
|
|
||||||
System: system,
|
|
||||||
Timestamp: time.Now(),
|
|
||||||
}
|
|
||||||
|
|
||||||
commandHistory = append(commandHistory, entry)
|
|
||||||
|
|
||||||
// Ограничиваем историю 100 командами в оперативной памяти
|
|
||||||
if len(commandHistory) > 100 {
|
|
||||||
commandHistory = commandHistory[1:]
|
|
||||||
// Перепривязать индексы после усечения
|
|
||||||
for i := range commandHistory {
|
|
||||||
commandHistory[i].Index = i + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обеспечим существование папки
|
|
||||||
if _, err := os.Stat(RESULT_FOLDER); os.IsNotExist(err) {
|
|
||||||
_ = os.MkdirAll(RESULT_FOLDER, 0755)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Загрузим существующий файл истории
|
|
||||||
var fileHistory []CommandHistory
|
|
||||||
if data, err := os.ReadFile(RESULT_HISTORY); err == nil && len(data) > 0 {
|
|
||||||
_ = json.Unmarshal(data, &fileHistory)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Поиск дубликата по полю Command
|
|
||||||
duplicateIndex := -1
|
|
||||||
for i, h := range fileHistory {
|
|
||||||
if strings.TrimSpace(strings.ToLower(h.Command)) == strings.TrimSpace(strings.ToLower(cmd)) {
|
|
||||||
duplicateIndex = i
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if duplicateIndex == -1 {
|
|
||||||
// Добавляем молча, если такого запроса не было
|
|
||||||
fileHistory = append(fileHistory, entry)
|
|
||||||
} else {
|
|
||||||
// Спросим о перезаписи
|
|
||||||
fmt.Printf("\nЗапрос уже есть в истории от %s. Перезаписать? (y/N): ", fileHistory[duplicateIndex].Timestamp.Format("2006-01-02 15:04:05"))
|
|
||||||
var ans string
|
|
||||||
fmt.Scanln(&ans)
|
|
||||||
if strings.ToLower(ans) == "y" || strings.ToLower(ans) == "yes" {
|
|
||||||
entry.Index = fileHistory[duplicateIndex].Index
|
|
||||||
fileHistory[duplicateIndex] = entry
|
|
||||||
} else {
|
|
||||||
// Оставляем как есть, ничего не делаем
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Пересчитать индексы в файле
|
|
||||||
for i := range fileHistory {
|
|
||||||
fileHistory[i].Index = i + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if out, err := json.MarshalIndent(fileHistory, "", " "); err == nil {
|
|
||||||
_ = os.WriteFile(RESULT_HISTORY, out, 0644)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func showHistory() {
|
|
||||||
// Пытаемся прочитать историю из файла
|
|
||||||
if disableHistory {
|
|
||||||
printColored("📝 История отключена (--no-history / LCG_NO_HISTORY)\n", colorYellow)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
data, err := os.ReadFile(RESULT_HISTORY)
|
|
||||||
if err == nil && len(data) > 0 {
|
|
||||||
var fileHistory []CommandHistory
|
|
||||||
if err := json.Unmarshal(data, &fileHistory); err == nil && len(fileHistory) > 0 {
|
|
||||||
printColored("📝 История (из файла):\n", colorYellow)
|
|
||||||
for _, hist := range fileHistory {
|
|
||||||
ts := hist.Timestamp.Format("2006-01-02 15:04:05")
|
|
||||||
fmt.Printf("%d. [%s] %s → %s\n", hist.Index, ts, hist.Command, hist.Response)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Фоллбек к памяти процесса
|
|
||||||
if len(commandHistory) == 0 {
|
|
||||||
printColored("📝 История пуста\n", colorYellow)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
printColored("📝 История команд:\n", colorYellow)
|
|
||||||
for i, hist := range commandHistory {
|
|
||||||
fmt.Printf("%d. %s → %s (%s)\n",
|
|
||||||
i+1,
|
|
||||||
hist.Command,
|
|
||||||
hist.Response,
|
|
||||||
hist.Timestamp.Format("15:04:05"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func readFileHistory() ([]CommandHistory, error) {
|
|
||||||
if disableHistory {
|
|
||||||
return nil, fmt.Errorf("history disabled")
|
|
||||||
}
|
|
||||||
data, err := os.ReadFile(RESULT_HISTORY)
|
|
||||||
if err != nil || len(data) == 0 {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var fileHistory []CommandHistory
|
|
||||||
if err := json.Unmarshal(data, &fileHistory); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return fileHistory, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func viewHistoryEntry(id int) {
|
|
||||||
fileHistory, err := readFileHistory()
|
|
||||||
if err != nil || len(fileHistory) == 0 {
|
|
||||||
fmt.Println("История пуста или недоступна")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var h *CommandHistory
|
|
||||||
for i := range fileHistory {
|
|
||||||
if fileHistory[i].Index == id {
|
|
||||||
h = &fileHistory[i]
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if h == nil {
|
|
||||||
fmt.Println("Запись не найдена")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
printColored("\n📋 Команда:\n", colorYellow)
|
|
||||||
printColored(fmt.Sprintf(" %s\n\n", h.Response), colorBold+colorGreen)
|
|
||||||
if strings.TrimSpace(h.Explanation) != "" {
|
|
||||||
printColored("\n📖 Подробное объяснение:\n\n", colorYellow)
|
|
||||||
fmt.Println(h.Explanation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func deleteHistoryEntry(id int) {
|
|
||||||
fileHistory, err := readFileHistory()
|
|
||||||
if err != nil || len(fileHistory) == 0 {
|
|
||||||
fmt.Println("История пуста или недоступна")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Найти индекс элемента с совпадающим полем Index
|
|
||||||
pos := -1
|
|
||||||
for i := range fileHistory {
|
|
||||||
if fileHistory[i].Index == id {
|
|
||||||
pos = i
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if pos == -1 {
|
|
||||||
fmt.Println("Запись не найдена")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Удаляем элемент
|
|
||||||
fileHistory = append(fileHistory[:pos], fileHistory[pos+1:]...)
|
|
||||||
// Перенумеровываем индексы
|
|
||||||
for i := range fileHistory {
|
|
||||||
fileHistory[i].Index = i + 1
|
|
||||||
}
|
|
||||||
if out, err := json.MarshalIndent(fileHistory, "", " "); err == nil {
|
|
||||||
if err := os.WriteFile(RESULT_HISTORY, out, 0644); err != nil {
|
|
||||||
fmt.Println("Ошибка записи истории:", err)
|
|
||||||
} else {
|
|
||||||
fmt.Println("Запись удалена")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fmt.Println("Ошибка сериализации истории:", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func printColored(text, color string) {
|
func printColored(text, color string) {
|
||||||
fmt.Printf("%s%s%s", color, text, colorReset)
|
fmt.Printf("%s%s%s", color, text, colorReset)
|
||||||
|
|||||||
48
response.go
Normal file
48
response.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func nowTimestamp() string {
|
||||||
|
return time.Now().Format("2006-01-02_15-04-05")
|
||||||
|
}
|
||||||
|
|
||||||
|
func pathJoin(base, name string) string {
|
||||||
|
return path.Join(base, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeFile(filePath, content string) {
|
||||||
|
if err := os.WriteFile(filePath, []byte(content), 0644); err != nil {
|
||||||
|
fmt.Println("Failed to save response:", err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Saved to %s\n", filePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveResponse(response string, gpt3Model string, prompt string, cmd 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)
|
||||||
|
writeFile(filePath, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
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]) + " ..."
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user