before result functions add

This commit is contained in:
2025-10-21 09:10:22 +06:00
parent 04d785db77
commit 47671eb566
8 changed files with 692 additions and 389 deletions

476
main.go
View File

@@ -2,18 +2,18 @@ package main
import (
_ "embed"
"encoding/json"
"fmt"
"math"
"os"
"os/exec"
"os/user"
"path"
"strconv"
"strings"
"time"
"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/reader"
"github.com/urfave/cli/v2"
@@ -22,21 +22,7 @@ import (
//go:embed VERSION.txt
var Version string
var (
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", "")
)
// используем глобальный экземпляр конфига из пакета config
// disableHistory управляет записью/обновлением истории на уровне процесса (флаг имеет приоритет над env)
var disableHistory bool
@@ -53,6 +39,8 @@ const (
)
func main() {
_ = colorBlue
app := &cli.App{
Name: "lcg",
Usage: "Linux Command GPT - Генерация Linux команд из описаний",
@@ -68,7 +56,7 @@ lcg [опции] <описание команды>
Description: `
Linux Command GPT - инструмент для генерации Linux команд из описаний на естественном языке.
Поддерживает чтение частей промпта из файлов и позволяет сохранять, копировать или перегенерировать результаты.
может задавать системный промпт или выбирать из предустановленных промптов.
Переменные окружения:
LCG_HOST Endpoint для LLM API (по умолчанию: http://192.168.87.108:11434/)
LCG_MODEL Название модели (по умолчанию: codegeex4)
@@ -113,9 +101,24 @@ Linux Command GPT - инструмент для генерации Linux ком
Action: func(c *cli.Context) error {
file := c.String("file")
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")
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()
if len(args) == 0 {
@@ -162,15 +165,15 @@ func getCommands() []*cli.Command {
Aliases: []string{"u"},
Usage: "Update the API key",
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")
return nil
}
timeout := 120 // default timeout
if t, err := strconv.Atoi(TIMEOUT); err == nil {
if t, err := strconv.Atoi(config.AppConfig.Timeout); err == nil {
timeout = t
}
gpt3 := initGPT(PROMPT, timeout)
gpt3 := initGPT(config.AppConfig.Prompt, timeout)
gpt3.UpdateKey()
fmt.Println("API key updated.")
return nil
@@ -181,15 +184,15 @@ func getCommands() []*cli.Command {
Aliases: []string{"d"},
Usage: "Delete the API key",
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")
return nil
}
timeout := 120 // default timeout
if t, err := strconv.Atoi(TIMEOUT); err == nil {
if t, err := strconv.Atoi(config.AppConfig.Timeout); err == nil {
timeout = t
}
gpt3 := initGPT(PROMPT, timeout)
gpt3 := initGPT(config.AppConfig.Prompt, timeout)
gpt3.DeleteKey()
fmt.Println("API key deleted.")
return nil
@@ -200,7 +203,7 @@ func getCommands() []*cli.Command {
Aliases: []string{"j"},
Usage: "Update the JWT token for proxy API",
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")
return nil
}
@@ -225,7 +228,7 @@ func getCommands() []*cli.Command {
Aliases: []string{"dj"},
Usage: "Delete the JWT token for proxy API",
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")
return nil
}
@@ -247,17 +250,17 @@ func getCommands() []*cli.Command {
Usage: "Show available models",
Action: func(c *cli.Context) error {
timeout := 120 // default timeout
if t, err := strconv.Atoi(TIMEOUT); err == nil {
if t, err := strconv.Atoi(config.AppConfig.Timeout); err == nil {
timeout = t
}
gpt3 := initGPT(PROMPT, timeout)
gpt3 := initGPT(config.AppConfig.Prompt, timeout)
models, err := gpt3.GetAvailableModels()
if err != nil {
fmt.Printf("Ошибка получения моделей: %v\n", err)
return err
}
fmt.Printf("Доступные модели для провайдера %s:\n", PROVIDER_TYPE)
fmt.Printf("Доступные модели для провайдера %s:\n", config.AppConfig.ProviderType)
for i, model := range models {
fmt.Printf(" %d. %s\n", i+1, model)
}
@@ -270,10 +273,10 @@ func getCommands() []*cli.Command {
Usage: "Check API health",
Action: func(c *cli.Context) error {
timeout := 120 // default timeout
if t, err := strconv.Atoi(TIMEOUT); err == nil {
if t, err := strconv.Atoi(config.AppConfig.Timeout); err == nil {
timeout = t
}
gpt3 := initGPT(PROMPT, timeout)
gpt3 := initGPT(config.AppConfig.Prompt, timeout)
if err := gpt3.Health(); err != nil {
fmt.Printf("Health check failed: %v\n", err)
return err
@@ -287,14 +290,14 @@ func getCommands() []*cli.Command {
Aliases: []string{"co"}, // Изменено с "c" на "co"
Usage: "Show current configuration",
Action: func(c *cli.Context) error {
fmt.Printf("Provider: %s\n", PROVIDER_TYPE)
fmt.Printf("Host: %s\n", HOST)
fmt.Printf("Model: %s\n", MODEL)
fmt.Printf("Prompt: %s\n", PROMPT)
fmt.Printf("Timeout: %s seconds\n", TIMEOUT)
if PROVIDER_TYPE == "proxy" {
fmt.Printf("Provider: %s\n", config.AppConfig.ProviderType)
fmt.Printf("Host: %s\n", config.AppConfig.Host)
fmt.Printf("Model: %s\n", config.AppConfig.Model)
fmt.Printf("Prompt: %s\n", config.AppConfig.Prompt)
fmt.Printf("Timeout: %s seconds\n", config.AppConfig.Timeout)
if config.AppConfig.ProviderType == "proxy" {
fmt.Printf("JWT Token: %s\n", func() string {
if JWT_TOKEN != "" {
if config.AppConfig.JwtToken != "" {
return "***set***"
}
currentUser, _ := user.Current()
@@ -318,7 +321,11 @@ func getCommands() []*cli.Command {
Aliases: []string{"l"},
Usage: "List history entries",
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
},
},
@@ -336,7 +343,11 @@ func getCommands() []*cli.Command {
fmt.Println("Неверный ID")
return nil
}
viewHistoryEntry(id)
if disableHistory {
fmt.Println("История отключена")
} else {
cmdPackage.ViewHistoryEntry(config.AppConfig.ResultHistory, id, printColored, colorYellow, colorBold, colorGreen)
}
return nil
},
},
@@ -354,7 +365,11 @@ func getCommands() []*cli.Command {
fmt.Println("Неверный ID")
return nil
}
deleteHistoryEntry(id)
if disableHistory {
fmt.Println("История отключена")
} else if err := cmdPackage.DeleteHistoryEntry(config.AppConfig.ResultHistory, id); err != nil {
fmt.Println(err)
}
return nil
},
},
@@ -467,7 +482,7 @@ func getCommands() []*cli.Command {
command := strings.Join(c.Args().Slice()[1:], " ")
fmt.Printf("\nTesting with command: %s\n", command)
timeout := 120 // default timeout
if t, err := strconv.Atoi(TIMEOUT); err == nil {
if t, err := strconv.Atoi(config.AppConfig.Timeout); err == nil {
timeout = t
}
executeMain("", prompt.Content, command, timeout)
@@ -489,12 +504,12 @@ func executeMain(file, system, commandInput string, timeout int) {
// Если system пустой, используем дефолтный промпт
if system == "" {
system = PROMPT
system = config.AppConfig.Prompt
}
// Обеспечим папку результатов заранее (может понадобиться при действиях)
if _, err := os.Stat(RESULT_FOLDER); os.IsNotExist(err) {
if err := os.MkdirAll(RESULT_FOLDER, 0755); err != nil {
if _, err := os.Stat(config.AppConfig.ResultFolder); os.IsNotExist(err) {
if err := os.MkdirAll(config.AppConfig.ResultFolder, 0755); err != nil {
printColored(fmt.Sprintf("❌ Ошибка создания папки результатов: %v\n", err), colorRed)
return
}
@@ -502,7 +517,7 @@ func executeMain(file, system, commandInput string, timeout int) {
// Проверка истории: если такой запрос уже встречался — предложить открыть из истории
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)
printColored("\nВНИМАНИЕ: ОТВЕТ СФОРМИРОВАН ИИ. ТРЕБУЕТСЯ ПРОВЕРКА И КРИТИЧЕСКИЙ АНАЛИЗ. ВОЗМОЖНЫ ОШИБКИ И ГАЛЛЮЦИНАЦИИ.\n", colorRed)
printColored("\n📋 Команда (из истории):\n", colorYellow)
@@ -542,39 +557,15 @@ func executeMain(file, system, commandInput string, timeout int) {
}
// 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
}
// moved to history.go
func initGPT(system string, timeout int) gpt.Gpt3 {
currentUser, _ := user.Current()
// Загружаем JWT токен в зависимости от провайдера
var jwtToken string
if PROVIDER_TYPE == "proxy" {
jwtToken = JWT_TOKEN
if config.AppConfig.ProviderType == "proxy" {
jwtToken = config.AppConfig.JwtToken
if jwtToken == "" {
// Пытаемся загрузить из файла
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) {
@@ -626,12 +617,12 @@ func handlePostResponse(response string, gpt3 gpt.Gpt3, system, cmd string, time
clipboard.WriteAll(response)
fmt.Println("✅ Команда скопирована в буфер обмена")
if !disableHistory {
saveToHistory(cmd, response, gpt3.Prompt)
cmdPackage.SaveToHistory(config.AppConfig.ResultHistory, config.AppConfig.ResultFolder, cmd, response, gpt3.Prompt)
}
case "s":
saveResponse(response, gpt3, cmd)
saveResponse(response, gpt3.Model, gpt3.Prompt, cmd)
if !disableHistory {
saveToHistory(cmd, response, gpt3.Prompt)
cmdPackage.SaveToHistory(config.AppConfig.ResultHistory, config.AppConfig.ResultFolder, cmd, response, gpt3.Prompt)
}
case "r":
fmt.Println("🔄 Перегенерирую...")
@@ -639,125 +630,37 @@ func handlePostResponse(response string, gpt3 gpt.Gpt3, system, cmd string, time
case "e":
executeCommand(response)
if !disableHistory {
saveToHistory(cmd, response, gpt3.Prompt)
cmdPackage.SaveToHistory(config.AppConfig.ResultHistory, config.AppConfig.ResultFolder, cmd, response, gpt3.Prompt)
}
case "v", "vv", "vvv":
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:
fmt.Println(" До свидания!")
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) {
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)
}
}
// moved to response.go
// 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)
}
}
// moved to explain.go
// 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]) + " ..."
}
// moved to response.go
// 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)
}
}
// moved to explain.go
func executeCommand(command string) {
fmt.Printf("🚀 Выполняю: %s\n", command)
@@ -780,214 +683,9 @@ func executeCommand(command string) {
}
}
func getEnv(key, defaultValue string) string {
if value, exists := os.LookupEnv(key); exists {
return value
}
return defaultValue
}
// env helpers moved to config package
func isNoHistoryEnv() bool {
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)
}
}
// moved to history.go
func printColored(text, color string) {
fmt.Printf("%s%s%s", color, text, colorReset)