mirror of
https://github.com/Direct-Dev-Ru/go-lcg.git
synced 2025-11-15 17:20:00 +00:00
before refactoring
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -15,3 +15,5 @@ bin-linux-arm64/*
|
|||||||
binaries-for-upload/*
|
binaries-for-upload/*
|
||||||
gpt_results
|
gpt_results
|
||||||
shell-code/jwt.admin.token
|
shell-code/jwt.admin.token
|
||||||
|
run.sh
|
||||||
|
lcg_history.json
|
||||||
|
|||||||
1
LICENSE
1
LICENSE
@@ -1,6 +1,7 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2023 asrul10
|
Copyright (c) 2023 asrul10
|
||||||
|
Copyright (c) 2025 direct-dev.ru
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
161
README.md
161
README.md
@@ -1,102 +1,85 @@
|
|||||||
# Linux Command GPT (lcg)
|
# Linux Command GPT (lcg)
|
||||||
|
|
||||||
Get Linux commands in natural language with the power of ChatGPT.
|
This repo is forked from <https://github.com/asrul10/linux-command-gpt.git>
|
||||||
|
|
||||||
|
Generate Linux commands from natural language. Supports Ollama and Proxy backends, system prompts, different explanation levels (v/vv/vvv), and JSON history.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Build from source
|
Build from source:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
> git clone --depth 1 https://github.com/asrul10/linux-command-gpt.git ~/.linux-command-gpt
|
git clone --depth 1 https://github.com/Direct-Dev-Ru/go-lcg.git ~/.linux-command-gpt
|
||||||
> cd ~/.linux-command-gpt
|
cd ~/.linux-command-gpt
|
||||||
> go build -o lcg
|
go build -o lcg
|
||||||
# Add to your environment $PATH
|
|
||||||
> ln -s ~/.linux-command-gpt/lcg ~/.local/bin
|
# Add to your PATH
|
||||||
|
ln -s ~/.linux-command-gpt/lcg ~/.local/bin
|
||||||
```
|
```
|
||||||
|
|
||||||
Or you can [download lcg executable file](https://github.com/asrul10/linux-command-gpt/releases)
|
## Quick start
|
||||||
|
|
||||||
## Example Usage
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
> lcg I want to extract linux-command-gpt.tar.gz file
|
|
||||||
Completed in 0.92 seconds
|
|
||||||
|
|
||||||
tar -xvzf linux-command-gpt.tar.gz
|
|
||||||
|
|
||||||
Do you want to (c)opy, (r)egenerate, or take (N)o action on the command? (c/r/N):
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
> LCG_PROMPT='Provide full response' LCG_MODEL=codellama:13b lcg 'i need bash script
|
|
||||||
to execute some command by ssh on some array of hosts'
|
|
||||||
Completed in 181.16 seconds
|
|
||||||
|
|
||||||
Here is a sample Bash script that demonstrates how to execute commands over SSH on an array of hosts:
|
|
||||||
```bash
|
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
hosts=(host1 host2 host3)
|
|
||||||
|
|
||||||
for host in "${hosts[@]}"; do
|
|
||||||
ssh $host "echo 'Hello, world!' > /tmp/hello.txt"
|
|
||||||
done
|
|
||||||
```
|
|
||||||
|
|
||||||
This script defines an array `hosts` that contains the names of the hosts to connect to. The loop iterates over each element in the array and uses the `ssh` command to execute a simple command on the remote host. In this case, the command is `echo 'Hello, world!' > /tmp/hello.txt`, which writes the string "Hello, world!" to a file called `/tmp/hello.txt`.
|
|
||||||
|
|
||||||
You can modify the script to run any command you like by replacing the `echo` command with your desired command. For example, if you want to run a Python script on each host, you could use the following command:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ssh $host "python /path/to/script.py"
|
|
||||||
```
|
|
||||||
|
|
||||||
This will execute the Python script located at `/path/to/script.py` on the remote host.
|
|
||||||
|
|
||||||
You can also modify the script to run multiple commands in a single SSH session by using the `&&` operator to chain the commands together. For example:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ssh $host "echo 'Hello, world!' > /tmp/hello.txt && python /path/to/script.py"
|
|
||||||
```
|
|
||||||
|
|
||||||
This will execute both the `echo` command and the Python script in a single SSH session.
|
|
||||||
|
|
||||||
I hope this helps! Let me know if you have any questions or need further assistance.
|
|
||||||
|
|
||||||
Do you want to (c)opy, (r)egenerate, or take (N)o action on the command? (c/r/N):
|
|
||||||
|
|
||||||
``` text
|
|
||||||
|
|
||||||
To use the "copy to clipboard" feature, you need to install either the `xclip` or `xsel` package.
|
|
||||||
|
|
||||||
### Options
|
|
||||||
```bash
|
|
||||||
> lcg [options]
|
|
||||||
|
|
||||||
--help -h output usage information
|
|
||||||
--version -v output the version number
|
|
||||||
--file -f read command from file
|
|
||||||
--update-key -u update the API key
|
|
||||||
--delete-key -d delete the API key
|
|
||||||
|
|
||||||
# ollama example
|
|
||||||
export LCG_PROVIDER=ollama
|
|
||||||
export LCG_HOST=http://192.168.87.108:11434/
|
|
||||||
export LCG_MODEL=codegeex4
|
|
||||||
|
|
||||||
lcg "I want to extract linux-command-gpt.tar.gz file"
|
lcg "I want to extract linux-command-gpt.tar.gz file"
|
||||||
|
|
||||||
export LCG_PROVIDER=proxy
|
|
||||||
export LCG_HOST=http://localhost:8080
|
|
||||||
export LCG_MODEL=GigaChat-2
|
|
||||||
export LCG_JWT_TOKEN=your_jwt_token_here
|
|
||||||
|
|
||||||
lcg "I want to extract linux-command-gpt.tar.gz file"
|
|
||||||
|
|
||||||
lcg health
|
|
||||||
|
|
||||||
lcg config
|
|
||||||
|
|
||||||
lcg update-jwt
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
After generation you will see a CAPS warning that the answer is from AI and must be verified, the command, and the action menu:
|
||||||
|
|
||||||
|
```text
|
||||||
|
ACTIONS: (c)opy, (s)ave, (r)egenerate, (e)xecute, (v|vv|vvv)explain, (n)othing
|
||||||
|
```
|
||||||
|
|
||||||
|
Explanations:
|
||||||
|
|
||||||
|
- `v` — short; `vv` — medium; `vvv` — detailed with alternatives.
|
||||||
|
|
||||||
|
Clipboard support requires `xclip` or `xsel`.
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
|
||||||
|
- `LCG_PROVIDER` (ollama|proxy), `LCG_HOST`, `LCG_MODEL`, `LCG_PROMPT`
|
||||||
|
- `LCG_TIMEOUT` (default 120), `LCG_RESULT_FOLDER` (default ./gpt_results)
|
||||||
|
- `LCG_RESULT_HISTORY` (default $(LCG_RESULT_FOLDER)/lcg_history.json)
|
||||||
|
- `LCG_JWT_TOKEN` (for proxy)
|
||||||
|
|
||||||
|
## Flags
|
||||||
|
|
||||||
|
- `--file, -f` read part of prompt from file
|
||||||
|
- `--sys, -s` system prompt content or ID
|
||||||
|
- `--prompt-id, --pid` choose built-in prompt (1–5)
|
||||||
|
- `--timeout, -t` request timeout (sec)
|
||||||
|
- `--version, -v` print version; `--help, -h` help
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
- `models`, `health`, `config`
|
||||||
|
- `prompts list|add|delete`
|
||||||
|
- `test-prompt <prompt-id> <command>`
|
||||||
|
- `update-jwt`, `delete-jwt` (proxy)
|
||||||
|
- `update-key`, `delete-key` (not needed for ollama/proxy)
|
||||||
|
- `history list` — list history from JSON
|
||||||
|
- `history view <index>` — view by index
|
||||||
|
- `history delete <index>` — delete by index (re-numbering)
|
||||||
|
|
||||||
|
## Saving results
|
||||||
|
|
||||||
|
Files are saved to `LCG_RESULT_FOLDER`.
|
||||||
|
|
||||||
|
- Command result: `gpt_request_<MODEL>_YYYY-MM-DD_HH-MM-SS.md`
|
||||||
|
- `# <title>` — H1 with original request (trimmed to 120 chars: first 116 + `...`)
|
||||||
|
- `## Prompt`
|
||||||
|
- `## Response`
|
||||||
|
|
||||||
|
- Detailed explanation: `gpt_explanation_<MODEL>_YYYY-MM-DD_HH-MM-SS.md`
|
||||||
|
- `# <title>`
|
||||||
|
- `## Prompt`
|
||||||
|
- `## Command`
|
||||||
|
- `## Explanation and Alternatives (model: <MODEL>)`
|
||||||
|
|
||||||
|
## History
|
||||||
|
|
||||||
|
- Stored as JSON array in `LCG_RESULT_HISTORY`.
|
||||||
|
- On new request, if the same command exists, you will be prompted to view or overwrite.
|
||||||
|
- Showing from history does not call the API; the standard action menu is shown.
|
||||||
|
|
||||||
|
For full guide in Russian, see `USAGE_GUIDE.md`.
|
||||||
|
|||||||
@@ -51,10 +51,12 @@ lcg --file /path/to/context.txt "хочу вывести список дирек
|
|||||||
🤖 Запрос: <ваше описание>
|
🤖 Запрос: <ваше описание>
|
||||||
✅ Выполнено за X.XX сек
|
✅ Выполнено за X.XX сек
|
||||||
|
|
||||||
|
ВНИМАНИЕ: ОТВЕТ СФОРМИРОВАН ИИ. ТРЕБУЕТСЯ ПРОВЕРКА И КРИТИЧЕСКИЙ АНАЛИЗ. ВОЗМОЖНЫ ОШИБКИ И ГАЛЛЮЦИНАЦИИ.
|
||||||
|
|
||||||
📋 Команда:
|
📋 Команда:
|
||||||
<сгенерированная команда>
|
<сгенерированная команда>
|
||||||
|
|
||||||
Действия: (c)копировать, (s)сохранить, (r)перегенерировать, (e)выполнить, (n)ничего:
|
Действия: (c)копировать, (s)сохранить, (r)перегенерировать, (e)выполнить, (v|vv|vvv)подробно, (n)ничего:
|
||||||
```
|
```
|
||||||
|
|
||||||
## Переменные окружения
|
## Переменные окружения
|
||||||
@@ -73,6 +75,8 @@ lcg --file /path/to/context.txt "хочу вывести список дирек
|
|||||||
| `LCG_JWT_TOKEN` | пусто | JWT токен для `proxy` провайдера (альтернатива — файл `~/.proxy_jwt_token`). |
|
| `LCG_JWT_TOKEN` | пусто | JWT токен для `proxy` провайдера (альтернатива — файл `~/.proxy_jwt_token`). |
|
||||||
| `LCG_PROMPT_ID` | `1` | ID системного промпта по умолчанию. |
|
| `LCG_PROMPT_ID` | `1` | ID системного промпта по умолчанию. |
|
||||||
| `LCG_TIMEOUT` | `120` | Таймаут запроса в секундах. |
|
| `LCG_TIMEOUT` | `120` | Таймаут запроса в секундах. |
|
||||||
|
| `LCG_RESULT_HISTORY` | `$(LCG_RESULT_FOLDER)/lcg_history.json` | Путь к JSON‑истории запросов. |
|
||||||
|
| `LCG_NO_HISTORY` | пусто | Если `1`/`true` — полностью отключает запись/обновление истории. |
|
||||||
|
|
||||||
Примеры настройки:
|
Примеры настройки:
|
||||||
|
|
||||||
@@ -113,13 +117,24 @@ lcg [глобальные опции] <описание команды>
|
|||||||
- `lcg models` (`-m`): показать доступные модели у текущего провайдера.
|
- `lcg models` (`-m`): показать доступные модели у текущего провайдера.
|
||||||
- `lcg health` (`-he`): проверить доступность API провайдера.
|
- `lcg health` (`-he`): проверить доступность API провайдера.
|
||||||
- `lcg config` (`-co`): показать текущую конфигурацию и состояние JWT.
|
- `lcg config` (`-co`): показать текущую конфигурацию и состояние JWT.
|
||||||
- `lcg history` (`-hist`): показать историю запросов за текущий запуск (до 100 записей, не сохраняется между запусками).
|
- `lcg history list` (`-l`): показать историю из JSON‑файла (`LCG_RESULT_HISTORY`).
|
||||||
|
- `lcg history view <id>` (`-v`): показать запись истории по `index`.
|
||||||
|
- `lcg history delete <id>` (`-d`): удалить запись истории по `index` (с перенумерацией).
|
||||||
|
- Флаг `--no-history` (`-nh`) отключает запись истории для текущего запуска и имеет приоритет над `LCG_NO_HISTORY`.
|
||||||
- `lcg prompts ...` (`-p`): управление системными промптами:
|
- `lcg prompts ...` (`-p`): управление системными промптами:
|
||||||
- `lcg prompts list` (`-l`) — список всех промптов.
|
- `lcg prompts list` (`-l`) — список всех промптов.
|
||||||
- `lcg prompts add` (`-a`) — добавить пользовательский промпт (по шагам в интерактиве).
|
- `lcg prompts add` (`-a`) — добавить пользовательский промпт (по шагам в интерактиве).
|
||||||
- `lcg prompts delete <id>` (`-d`) — удалить пользовательский промпт по ID (>5).
|
- `lcg prompts delete <id>` (`-d`) — удалить пользовательский промпт по ID (>5).
|
||||||
- `lcg test-prompt <prompt-id> <описание>` (`-tp`): показать детали выбранного системного промпта и протестировать его на заданном описании.
|
- `lcg test-prompt <prompt-id> <описание>` (`-tp`): показать детали выбранного системного промпта и протестировать его на заданном описании.
|
||||||
|
|
||||||
|
### Подробные объяснения (v/vv/vvv)
|
||||||
|
|
||||||
|
- `v` — кратко: что делает команда и ключевые опции, без альтернатив.
|
||||||
|
- `vv` — средне: назначение, основные ключи, 1–2 примера, кратко об альтернативах.
|
||||||
|
- `vvv` — максимально подробно: полный разбор ключей, сценариев, примеры, разбор альтернатив и сравнений.
|
||||||
|
|
||||||
|
После вывода подробного объяснения доступно вторичное меню: `Действия: (c)копировать, (s)сохранить, (r)перегенерировать, (n)ничего:`
|
||||||
|
|
||||||
## Провайдеры
|
## Провайдеры
|
||||||
|
|
||||||
### Ollama (`LCG_PROVIDER=ollama`)
|
### Ollama (`LCG_PROVIDER=ollama`)
|
||||||
@@ -156,10 +171,18 @@ lcg [глобальные опции] <описание команды>
|
|||||||
gpt_request_<MODEL>_YYYY-MM-DD_HH-MM-SS.md
|
gpt_request_<MODEL>_YYYY-MM-DD_HH-MM-SS.md
|
||||||
```
|
```
|
||||||
|
|
||||||
Структура файла:
|
Структура файла (команда):
|
||||||
|
|
||||||
- `## Prompt:` — ваш запрос (включая системный промпт).
|
- `# <заголовок>` — H1, это ваш запрос, при длине >120 символов обрезается до 116 + `...`.
|
||||||
- `## Response:` — полученный ответ.
|
- `## Prompt` — запрос (включая системный промпт).
|
||||||
|
- `## Response` — сгенерированная команда.
|
||||||
|
|
||||||
|
Структура файла (подробное объяснение):
|
||||||
|
|
||||||
|
- `# <заголовок>` — H1, ваш исходный запрос (с тем же правилом обрезки).
|
||||||
|
- `## Prompt` — исходный запрос.
|
||||||
|
- `## Command` — первая сгенерированная команда.
|
||||||
|
- `## Explanation and Alternatives (model: <MODEL>)` — подробное объяснение и альтернативы.
|
||||||
|
|
||||||
## Выполнение сгенерированной команды
|
## Выполнение сгенерированной команды
|
||||||
|
|
||||||
@@ -219,7 +242,7 @@ lcg models
|
|||||||
`lcg history` выводит историю текущего процесса (не сохраняется между запусками, максимум 100 записей):
|
`lcg history` выводит историю текущего процесса (не сохраняется между запусками, максимум 100 записей):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
lcg history
|
lcg history list
|
||||||
```
|
```
|
||||||
|
|
||||||
## Типичные проблемы
|
## Типичные проблемы
|
||||||
@@ -230,6 +253,30 @@ lcg history
|
|||||||
- Нет допуска к папке результатов: настройте `LCG_RESULT_FOLDER` или права доступа.
|
- Нет допуска к папке результатов: настройте `LCG_RESULT_FOLDER` или права доступа.
|
||||||
- Для `ollama`/`proxy` API‑ключ не нужен; команды `update-key`/`delete-key` просто уведомят об этом.
|
- Для `ollama`/`proxy` API‑ключ не нужен; команды `update-key`/`delete-key` просто уведомят об этом.
|
||||||
|
|
||||||
|
## JSON‑история запросов
|
||||||
|
|
||||||
|
- Путь задаётся `LCG_RESULT_HISTORY` (по умолчанию: `$(LCG_RESULT_FOLDER)/lcg_history.json`).
|
||||||
|
- Формат — массив объектов:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"command": "хочу извлечь linux-command-gpt.tar.gz",
|
||||||
|
"response": "tar -xvzf linux-command-gpt.tar.gz",
|
||||||
|
"explanation": "... если запрашивалось v/vv/vvv ...",
|
||||||
|
"system_prompt": "Reply with linux command and nothing else ...",
|
||||||
|
"timestamp": "2025-10-19T13:05:39.000000000Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
- Перед новым запросом, если такой уже встречался, будет предложено вывести сохранённый результат из истории с указанием даты.
|
||||||
|
- Сохранение в файл истории выполняется автоматически после завершения работы (любое действие, кроме `v|vv|vvv`).
|
||||||
|
- При совпадении запроса в истории спрашивается о перезаписи записи.
|
||||||
|
- Подкоманды истории работают по полю `index` внутри JSON (а не по позиции массива): используйте `lcg history view <index>` и `lcg history delete <index>`.
|
||||||
|
- При показе из истории запрос к API не выполняется: выводится CAPS‑предупреждение и далее доступно обычное меню действий над командой/объяснением.
|
||||||
|
|
||||||
## Лицензия и исходники
|
## Лицензия и исходники
|
||||||
|
|
||||||
См. README и репозиторий проекта. Предложения и баг‑репорты приветствуются в Issues.
|
См. README и репозиторий проекта. Предложения и баг‑репорты приветствуются в Issues.
|
||||||
|
|||||||
443
main.go
443
main.go
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
_ "embed"
|
_ "embed"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
@@ -22,19 +23,24 @@ import (
|
|||||||
var Version string
|
var Version string
|
||||||
|
|
||||||
var (
|
var (
|
||||||
cwd, _ = os.Getwd()
|
cwd, _ = os.Getwd()
|
||||||
HOST = getEnv("LCG_HOST", "http://192.168.87.108:11434/")
|
HOST = getEnv("LCG_HOST", "http://192.168.87.108:11434/")
|
||||||
COMPLETIONS = getEnv("LCG_COMPLETIONS_PATH", "api/chat")
|
COMPLETIONS = getEnv("LCG_COMPLETIONS_PATH", "api/chat")
|
||||||
MODEL = getEnv("LCG_MODEL", "codegeex4")
|
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.")
|
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")
|
API_KEY_FILE = getEnv("LCG_API_KEY_FILE", ".openai_api_key")
|
||||||
RESULT_FOLDER = getEnv("LCG_RESULT_FOLDER", path.Join(cwd, "gpt_results"))
|
RESULT_FOLDER = getEnv("LCG_RESULT_FOLDER", path.Join(cwd, "gpt_results"))
|
||||||
PROVIDER_TYPE = getEnv("LCG_PROVIDER", "ollama") // "ollama", "proxy"
|
PROVIDER_TYPE = getEnv("LCG_PROVIDER", "ollama") // "ollama", "proxy"
|
||||||
JWT_TOKEN = getEnv("LCG_JWT_TOKEN", "")
|
JWT_TOKEN = getEnv("LCG_JWT_TOKEN", "")
|
||||||
PROMPT_ID = getEnv("LCG_PROMPT_ID", "1") // ID промпта по умолчанию
|
PROMPT_ID = getEnv("LCG_PROMPT_ID", "1") // ID промпта по умолчанию
|
||||||
TIMEOUT = getEnv("LCG_TIMEOUT", "120") // Таймаут в секундах по умолчанию
|
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)
|
||||||
|
var disableHistory bool
|
||||||
|
|
||||||
const (
|
const (
|
||||||
colorRed = "\033[31m"
|
colorRed = "\033[31m"
|
||||||
colorGreen = "\033[32m"
|
colorGreen = "\033[32m"
|
||||||
@@ -76,6 +82,12 @@ Linux Command GPT - инструмент для генерации Linux ком
|
|||||||
Aliases: []string{"f"},
|
Aliases: []string{"f"},
|
||||||
Usage: "Read part of the command from a file",
|
Usage: "Read part of the command from a file",
|
||||||
},
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "no-history",
|
||||||
|
Aliases: []string{"nh"},
|
||||||
|
Usage: "Disable writing/updating command history (overrides LCG_NO_HISTORY)",
|
||||||
|
Value: false,
|
||||||
|
},
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "sys",
|
Name: "sys",
|
||||||
Aliases: []string{"s"},
|
Aliases: []string{"s"},
|
||||||
@@ -101,6 +113,7 @@ 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()
|
||||||
promptID := c.Int("prompt-id")
|
promptID := c.Int("prompt-id")
|
||||||
timeout := c.Int("timeout")
|
timeout := c.Int("timeout")
|
||||||
args := c.Args().Slice()
|
args := c.Args().Slice()
|
||||||
@@ -299,9 +312,52 @@ func getCommands() []*cli.Command {
|
|||||||
Name: "history",
|
Name: "history",
|
||||||
Aliases: []string{"hist"},
|
Aliases: []string{"hist"},
|
||||||
Usage: "Show command history",
|
Usage: "Show command history",
|
||||||
Action: func(c *cli.Context) error {
|
Subcommands: []*cli.Command{
|
||||||
showHistory()
|
{
|
||||||
return nil
|
Name: "list",
|
||||||
|
Aliases: []string{"l"},
|
||||||
|
Usage: "List history entries",
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
showHistory()
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "view",
|
||||||
|
Aliases: []string{"v"},
|
||||||
|
Usage: "View history entry by ID",
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
if c.NArg() == 0 {
|
||||||
|
fmt.Println("Укажите ID записи истории")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var id int
|
||||||
|
if _, err := fmt.Sscanf(c.Args().First(), "%d", &id); err != nil || id <= 0 {
|
||||||
|
fmt.Println("Неверный ID")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
viewHistoryEntry(id)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "delete",
|
||||||
|
Aliases: []string{"d"},
|
||||||
|
Usage: "Delete history entry by ID",
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
if c.NArg() == 0 {
|
||||||
|
fmt.Println("Укажите ID записи истории")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var id int
|
||||||
|
if _, err := fmt.Sscanf(c.Args().First(), "%d", &id); err != nil || id <= 0 {
|
||||||
|
fmt.Println("Неверный ID")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
deleteHistoryEntry(id)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -436,6 +492,7 @@ func executeMain(file, system, commandInput string, timeout int) {
|
|||||||
system = PROMPT
|
system = PROMPT
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Обеспечим папку результатов заранее (может понадобиться при действиях)
|
||||||
if _, err := os.Stat(RESULT_FOLDER); os.IsNotExist(err) {
|
if _, err := os.Stat(RESULT_FOLDER); os.IsNotExist(err) {
|
||||||
if err := os.MkdirAll(RESULT_FOLDER, 0755); err != nil {
|
if err := os.MkdirAll(RESULT_FOLDER, 0755); err != nil {
|
||||||
printColored(fmt.Sprintf("❌ Ошибка создания папки результатов: %v\n", err), colorRed)
|
printColored(fmt.Sprintf("❌ Ошибка создания папки результатов: %v\n", err), colorRed)
|
||||||
@@ -443,6 +500,25 @@ func executeMain(file, system, commandInput string, timeout int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Проверка истории: если такой запрос уже встречался — предложить открыть из истории
|
||||||
|
if !disableHistory {
|
||||||
|
if found, hist := checkAndSuggestFromHistory(commandInput); found && hist != nil {
|
||||||
|
gpt3 := initGPT(system, timeout)
|
||||||
|
printColored("\nВНИМАНИЕ: ОТВЕТ СФОРМИРОВАН ИИ. ТРЕБУЕТСЯ ПРОВЕРКА И КРИТИЧЕСКИЙ АНАЛИЗ. ВОЗМОЖНЫ ОШИБКИ И ГАЛЛЮЦИНАЦИИ.\n", colorRed)
|
||||||
|
printColored("\n📋 Команда (из истории):\n", colorYellow)
|
||||||
|
printColored(fmt.Sprintf(" %s\n\n", hist.Response), colorBold+colorGreen)
|
||||||
|
if strings.TrimSpace(hist.Explanation) != "" {
|
||||||
|
printColored("\n📖 Подробное объяснение (из истории):\n\n", colorYellow)
|
||||||
|
fmt.Println(hist.Explanation)
|
||||||
|
}
|
||||||
|
// Показали из истории — не выполняем запрос к API, сразу меню действий
|
||||||
|
handlePostResponse(hist.Response, gpt3, system, commandInput, timeout)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Папка уже создана выше
|
||||||
|
|
||||||
gpt3 := initGPT(system, timeout)
|
gpt3 := initGPT(system, timeout)
|
||||||
|
|
||||||
printColored("🤖 Запрос: ", colorCyan)
|
printColored("🤖 Запрос: ", colorCyan)
|
||||||
@@ -455,11 +531,41 @@ func executeMain(file, system, commandInput string, timeout int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
printColored(fmt.Sprintf("✅ Выполнено за %.2f сек\n", elapsed), colorGreen)
|
printColored(fmt.Sprintf("✅ Выполнено за %.2f сек\n", elapsed), colorGreen)
|
||||||
|
// Обязательное предупреждение перед первым ответом
|
||||||
|
printColored("\nВНИМАНИЕ: ОТВЕТ СФОРМИРОВАН ИИ. ТРЕБУЕТСЯ ПРОВЕРКА И КРИТИЧЕСКИЙ АНАЛИЗ. ВОЗМОЖНЫ ОШИБКИ И ГАЛЛЮЦИНАЦИИ.\n", colorRed)
|
||||||
printColored("\n📋 Команда:\n", colorYellow)
|
printColored("\n📋 Команда:\n", colorYellow)
|
||||||
printColored(fmt.Sprintf(" %s\n\n", response), colorBold+colorGreen)
|
printColored(fmt.Sprintf(" %s\n\n", response), colorBold+colorGreen)
|
||||||
|
|
||||||
saveToHistory(commandInput, response)
|
// Сохраняем в историю (после завершения работы – т.е. позже, в зависимости от выбора действия)
|
||||||
handlePostResponse(response, gpt3, system, commandInput)
|
// Здесь не сохраняем, чтобы учесть правило: сохранять после действия, отличного от v/vv/vvv
|
||||||
|
handlePostResponse(response, gpt3, system, commandInput, timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkAndSuggestFromHistory проверяет файл истории и при совпадении запроса предлагает показать сохраненный результат
|
||||||
|
func checkAndSuggestFromHistory(cmd string) (bool, *CommandHistory) {
|
||||||
|
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 {
|
||||||
@@ -510,8 +616,8 @@ func getCommand(gpt3 gpt.Gpt3, cmd string) (string, float64) {
|
|||||||
return response, elapsed
|
return response, elapsed
|
||||||
}
|
}
|
||||||
|
|
||||||
func handlePostResponse(response string, gpt3 gpt.Gpt3, system, cmd string) {
|
func handlePostResponse(response string, gpt3 gpt.Gpt3, system, cmd string, timeout int) {
|
||||||
fmt.Printf("Действия: (c)копировать, (s)сохранить, (r)перегенерировать, (e)выполнить, (n)ничего: ")
|
fmt.Printf("Действия: (c)копировать, (s)сохранить, (r)перегенерировать, (e)выполнить, (v|vv|vvv)подробно, (n)ничего: ")
|
||||||
var choice string
|
var choice string
|
||||||
fmt.Scanln(&choice)
|
fmt.Scanln(&choice)
|
||||||
|
|
||||||
@@ -519,15 +625,30 @@ func handlePostResponse(response string, gpt3 gpt.Gpt3, system, cmd string) {
|
|||||||
case "c":
|
case "c":
|
||||||
clipboard.WriteAll(response)
|
clipboard.WriteAll(response)
|
||||||
fmt.Println("✅ Команда скопирована в буфер обмена")
|
fmt.Println("✅ Команда скопирована в буфер обмена")
|
||||||
|
if !disableHistory {
|
||||||
|
saveToHistory(cmd, response, gpt3.Prompt)
|
||||||
|
}
|
||||||
case "s":
|
case "s":
|
||||||
saveResponse(response, gpt3, cmd)
|
saveResponse(response, gpt3, cmd)
|
||||||
|
if !disableHistory {
|
||||||
|
saveToHistory(cmd, response, gpt3.Prompt)
|
||||||
|
}
|
||||||
case "r":
|
case "r":
|
||||||
fmt.Println("🔄 Перегенерирую...")
|
fmt.Println("🔄 Перегенерирую...")
|
||||||
executeMain("", system, cmd, 120) // Use default timeout for regeneration
|
executeMain("", system, cmd, timeout)
|
||||||
case "e":
|
case "e":
|
||||||
executeCommand(response)
|
executeCommand(response)
|
||||||
|
if !disableHistory {
|
||||||
|
saveToHistory(cmd, response, gpt3.Prompt)
|
||||||
|
}
|
||||||
|
case "v", "vv", "vvv":
|
||||||
|
level := len(choice) // 1, 2, 3
|
||||||
|
showDetailedExplanation(response, gpt3, system, cmd, timeout, level)
|
||||||
default:
|
default:
|
||||||
fmt.Println(" До свидания!")
|
fmt.Println(" До свидания!")
|
||||||
|
if !disableHistory {
|
||||||
|
saveToHistory(cmd, response, gpt3.Prompt)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -535,7 +656,9 @@ func saveResponse(response string, gpt3 gpt.Gpt3, cmd string) {
|
|||||||
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
||||||
filename := fmt.Sprintf("gpt_request_%s_%s.md", gpt3.Model, timestamp)
|
filename := fmt.Sprintf("gpt_request_%s_%s.md", gpt3.Model, timestamp)
|
||||||
filePath := path.Join(RESULT_FOLDER, filename)
|
filePath := path.Join(RESULT_FOLDER, filename)
|
||||||
content := fmt.Sprintf("## Prompt:\n\n%s\n\n## Response:\n\n%s\n", cmd+". "+gpt3.Prompt, response)
|
// Заголовок — сокращенный текст запроса пользователя
|
||||||
|
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 {
|
if err := os.WriteFile(filePath, []byte(content), 0644); err != nil {
|
||||||
fmt.Println("Failed to save response:", err)
|
fmt.Println("Failed to save response:", err)
|
||||||
@@ -544,6 +667,98 @@ func saveResponse(response string, gpt3 gpt.Gpt3, cmd string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// saveExplanation сохраняет подробное объяснение и альтернативные способы
|
||||||
|
func saveExplanation(explanation string, model string, originalCmd string, commandResponse string) {
|
||||||
|
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 символов (по рунам), добавляя " ..." при усечении
|
||||||
|
func truncateTitle(s string) string {
|
||||||
|
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 делает дополнительный запрос с подробным описанием и альтернативами
|
||||||
|
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)
|
||||||
fmt.Print("Продолжить? (y/N): ")
|
fmt.Print("Продолжить? (y/N): ")
|
||||||
@@ -572,28 +787,118 @@ func getEnv(key, defaultValue string) string {
|
|||||||
return defaultValue
|
return defaultValue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isNoHistoryEnv() bool {
|
||||||
|
v := strings.TrimSpace(NO_HISTORY_ENV)
|
||||||
|
vLower := strings.ToLower(v)
|
||||||
|
return vLower == "1" || vLower == "true"
|
||||||
|
}
|
||||||
|
|
||||||
type CommandHistory struct {
|
type CommandHistory struct {
|
||||||
Command string
|
Index int `json:"index"`
|
||||||
Response string
|
Command string `json:"command"`
|
||||||
Timestamp time.Time
|
Response string `json:"response"`
|
||||||
|
Explanation string `json:"explanation,omitempty"`
|
||||||
|
System string `json:"system_prompt"`
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var commandHistory []CommandHistory
|
var commandHistory []CommandHistory
|
||||||
|
|
||||||
func saveToHistory(cmd, response string) {
|
func saveToHistory(cmd, response, system string, explanationOptional ...string) {
|
||||||
commandHistory = append(commandHistory, CommandHistory{
|
if disableHistory {
|
||||||
Command: cmd,
|
return
|
||||||
Response: response,
|
}
|
||||||
Timestamp: time.Now(),
|
var explanation string
|
||||||
})
|
if len(explanationOptional) > 0 {
|
||||||
|
explanation = explanationOptional[0]
|
||||||
|
}
|
||||||
|
|
||||||
// Ограничиваем историю 100 командами
|
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 {
|
if len(commandHistory) > 100 {
|
||||||
commandHistory = commandHistory[1:]
|
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() {
|
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 {
|
if len(commandHistory) == 0 {
|
||||||
printColored("📝 История пуста\n", colorYellow)
|
printColored("📝 История пуста\n", colorYellow)
|
||||||
return
|
return
|
||||||
@@ -609,6 +914,81 @@ func showHistory() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
@@ -619,8 +999,9 @@ func showTips() {
|
|||||||
fmt.Println(" • Используйте --sys для изменения системного промпта")
|
fmt.Println(" • Используйте --sys для изменения системного промпта")
|
||||||
fmt.Println(" • Используйте --prompt-id для выбора предустановленного промпта")
|
fmt.Println(" • Используйте --prompt-id для выбора предустановленного промпта")
|
||||||
fmt.Println(" • Используйте --timeout для установки таймаута запроса")
|
fmt.Println(" • Используйте --timeout для установки таймаута запроса")
|
||||||
|
fmt.Println(" • Укажите --no-history чтобы не записывать историю (аналог LCG_NO_HISTORY)")
|
||||||
fmt.Println(" • Команда 'prompts list' покажет все доступные промпты")
|
fmt.Println(" • Команда 'prompts list' покажет все доступные промпты")
|
||||||
fmt.Println(" • Команда 'history' покажет историю запросов")
|
fmt.Println(" • Команда 'history list' покажет историю запросов")
|
||||||
fmt.Println(" • Команда 'config' покажет текущие настройки")
|
fmt.Println(" • Команда 'config' покажет текущие настройки")
|
||||||
fmt.Println(" • Команда 'health' проверит доступность API")
|
fmt.Println(" • Команда 'health' проверит доступность API")
|
||||||
}
|
}
|
||||||
|
|||||||
6
run_ollama.sh
Normal file
6
run_ollama.sh
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
#! /usr/bin/bash
|
||||||
|
|
||||||
|
LCG_PROVIDER=ollama LCG_HOST=http://192.168.87.108:11434/ \
|
||||||
|
LCG_MODEL=hf.co/yandex/YandexGPT-5-Lite-8B-instruct-GGUF:Q4_K_M \
|
||||||
|
go run . $1 $2 $3 $4 $5 $6 $7 $8 $9
|
||||||
|
|
||||||
Reference in New Issue
Block a user