diff --git a/Dockerfiles/ImageBuild/Dockerfile b/Dockerfiles/ImageBuild/Dockerfile index ec12b98..847bd6d 100644 --- a/Dockerfiles/ImageBuild/Dockerfile +++ b/Dockerfiles/ImageBuild/Dockerfile @@ -1,4 +1,4 @@ -FROM --platform=${BUILDPLATFORM} golang:1.23-alpine AS builder +FROM --platform=${BUILDPLATFORM} golang:1.24.6-alpine3.22 AS builder ARG TARGETARCH diff --git a/Dockerfiles/LocalCompile/Dockerfile b/Dockerfiles/LocalCompile/Dockerfile index 26f0a9a..f5e4c74 100644 --- a/Dockerfiles/LocalCompile/Dockerfile +++ b/Dockerfiles/LocalCompile/Dockerfile @@ -1,4 +1,4 @@ -FROM --platform=${BUILDPLATFORM} golang:1.23-alpine AS build +FROM --platform=${BUILDPLATFORM} golang:1.24.6-alpine3.22 AS build ARG TARGETOS ARG TARGETARCH RUN apk add git && go install mvdan.cc/garble@latest diff --git a/README.md b/README.md index 5d5754f..a403a24 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,11 @@ -## Linux Command GPT (lcg) +# Linux Command GPT (lcg) + Get Linux commands in natural language with the power of ChatGPT. -### Installation +## Installation + Build from source + ```bash > git clone --depth 1 https://github.com/asrul10/linux-command-gpt.git ~/.linux-command-gpt > cd ~/.linux-command-gpt @@ -13,7 +16,7 @@ Build from source Or you can [download lcg executable file](https://github.com/asrul10/linux-command-gpt/releases) -### Example Usage +## Example Usage ```bash > lcg I want to extract linux-command-gpt.tar.gz file @@ -39,24 +42,30 @@ 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. @@ -69,4 +78,25 @@ To use the "copy to clipboard" feature, you need to install either the `xclip` o --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" + +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 + ``` diff --git a/VERSION.txt b/VERSION.txt index 3e7bcf0..b18d465 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -v1.0.4 +v1.0.1 diff --git a/go.mod b/go.mod index 1ac128d..c8ce165 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,6 @@ require github.com/atotto/clipboard v0.1.4 require ( github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/urfave/cli/v2 v2.27.5 // indirect + github.com/urfave/cli/v2 v2.27.5 github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect ) diff --git a/gpt/gpt.go b/gpt/gpt.go index 2555d38..7cf978a 100644 --- a/gpt/gpt.go +++ b/gpt/gpt.go @@ -1,24 +1,40 @@ package gpt import ( - "bytes" - "encoding/json" "fmt" - "io" - "net/http" "os" "path/filepath" "strings" ) +// ProxySimpleChatRequest структура для простого запроса +type ProxySimpleChatRequest struct { + Message string `json:"message"` + Model string `json:"model,omitempty"` +} + +// ProxySimpleChatResponse структура ответа для простого запроса +type ProxySimpleChatResponse struct { + Response string `json:"response"` + Usage struct { + PromptTokens int `json:"prompt_tokens"` + CompletionTokens int `json:"completion_tokens"` + TotalTokens int `json:"total_tokens"` + } `json:"usage,omitempty"` + Model string `json:"model,omitempty"` + Timeout int `json:"timeout_seconds,omitempty"` +} + +// Gpt3 обновленная структура с поддержкой разных провайдеров type Gpt3 struct { - CompletionUrl string - Prompt string - Model string - HomeDir string - ApiKeyFile string - ApiKey string - Temperature float64 + Provider Provider + Prompt string + Model string + HomeDir string + ApiKeyFile string + ApiKey string + Temperature float64 + ProviderType string // "ollama", "proxy" } type Chat struct { @@ -135,6 +151,11 @@ func (gpt3 *Gpt3) DeleteKey() { } func (gpt3 *Gpt3) InitKey() { + // Для proxy провайдера не нужен API ключ, используется JWT токен + if gpt3.ProviderType == "proxy" { + return + } + load := gpt3.loadApiKey() if load { return @@ -145,55 +166,46 @@ func (gpt3 *Gpt3) InitKey() { gpt3.storeApiKey(apiKey) } -func (gpt3 *Gpt3) Completions(ask string) string { - req, err := http.NewRequest("POST", gpt3.CompletionUrl, nil) - if err != nil { - panic(err) - } - req.Header.Set("Content-Type", "application/json") - // req.Header.Set("Authorization", "Bearer "+strings.TrimSpace(gpt3.ApiKey)) +// NewGpt3 создает новый экземпляр GPT с выбранным провайдером +func NewGpt3(providerType, host, apiKey, model, prompt string, temperature float64) *Gpt3 { + var provider Provider + switch providerType { + case "proxy": + provider = NewProxyAPIProvider(host, apiKey, model) // apiKey используется как JWT токен + case "ollama": + provider = NewOllamaProvider(host, model, temperature) + default: + provider = NewOllamaProvider(host, model, temperature) + } + + return &Gpt3{ + Provider: provider, + Prompt: prompt, + Model: model, + ApiKey: apiKey, + Temperature: temperature, + ProviderType: providerType, + } +} + +// Completions обновленный метод с поддержкой разных провайдеров +func (gpt3 *Gpt3) Completions(ask string) string { messages := []Chat{ {"system", gpt3.Prompt}, - {"user", ask + "." + gpt3.Prompt}, - } - payload := Gpt3Request{ - Model: gpt3.Model, - Messages: messages, - Stream: false, - Options: Gpt3Options{gpt3.Temperature}, + {"user", ask + ". " + gpt3.Prompt}, } - payloadJson, err := json.Marshal(payload) + response, err := gpt3.Provider.Chat(messages) if err != nil { - panic(err) - } - req.Body = io.NopCloser(bytes.NewBuffer(payloadJson)) - - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - panic(err) - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - panic(err) - } - - if resp.StatusCode != http.StatusOK { - fmt.Println(string(body)) + fmt.Printf("Ошибка при выполнении запроса: %v\n", err) return "" } - // var res Gpt3Response - var res OllamaResponse - err = json.Unmarshal(body, &res) - if err != nil { - panic(err) - } - - // return strings.TrimSpace(res.Choices[0].Message.Content) - return strings.TrimSpace(res.Message.Content) + return response +} + +// Health проверяет состояние провайдера +func (gpt3 *Gpt3) Health() error { + return gpt3.Provider.Health() } diff --git a/gpt/providers.go b/gpt/providers.go new file mode 100644 index 0000000..da66ce6 --- /dev/null +++ b/gpt/providers.go @@ -0,0 +1,246 @@ +package gpt + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "time" +) + +// Provider интерфейс для работы с разными LLM провайдерами +type Provider interface { + Chat(messages []Chat) (string, error) + Health() error +} + +// ProxyAPIProvider реализация для прокси API (gin-restapi) +type ProxyAPIProvider struct { + BaseURL string + JWTToken string + Model string + HTTPClient *http.Client +} + +// ProxyChatRequest структура запроса к прокси API +type ProxyChatRequest struct { + Messages []Chat `json:"messages"` + Model string `json:"model,omitempty"` + Temperature float64 `json:"temperature,omitempty"` + TopP float64 `json:"top_p,omitempty"` + Stream bool `json:"stream,omitempty"` + SystemContent string `json:"system_content,omitempty"` + UserContent string `json:"user_content,omitempty"` + RandomWords []string `json:"random_words,omitempty"` + FallbackString string `json:"fallback_string,omitempty"` +} + +// ProxyChatResponse структура ответа от прокси API +type ProxyChatResponse struct { + Response string `json:"response"` + Usage struct { + PromptTokens int `json:"prompt_tokens"` + CompletionTokens int `json:"completion_tokens"` + TotalTokens int `json:"total_tokens"` + } `json:"usage,omitempty"` + Error string `json:"error,omitempty"` + Model string `json:"model,omitempty"` + Timeout int `json:"timeout_seconds,omitempty"` +} + +// ProxyHealthResponse структура ответа health check +type ProxyHealthResponse struct { + Status string `json:"status"` + Message string `json:"message"` + Model string `json:"default_model,omitempty"` + Timeout int `json:"default_timeout_seconds,omitempty"` +} + +// OllamaProvider реализация для Ollama API +type OllamaProvider struct { + BaseURL string + Model string + Temperature float64 + HTTPClient *http.Client +} + +func NewProxyAPIProvider(baseURL, jwtToken, model string) *ProxyAPIProvider { + return &ProxyAPIProvider{ + BaseURL: strings.TrimSuffix(baseURL, "/"), + JWTToken: jwtToken, + Model: model, + HTTPClient: &http.Client{Timeout: 120 * time.Second}, + } +} + +func NewOllamaProvider(baseURL, model string, temperature float64) *OllamaProvider { + return &OllamaProvider{ + BaseURL: strings.TrimSuffix(baseURL, "/"), + Model: model, + Temperature: temperature, + HTTPClient: &http.Client{Timeout: 120 * time.Second}, + } +} + +// Chat для ProxyAPIProvider +func (p *ProxyAPIProvider) Chat(messages []Chat) (string, error) { + // Используем основной endpoint /api/v1/protected/sberchat/chat + payload := ProxyChatRequest{ + Messages: messages, + Model: p.Model, + Temperature: 0.5, + TopP: 0.5, + Stream: false, + RandomWords: []string{"linux", "command", "gpt"}, + FallbackString: "I'm sorry, I can't help with that. Please try again.", + } + + jsonData, err := json.Marshal(payload) + if err != nil { + return "", fmt.Errorf("ошибка маршалинга запроса: %w", err) + } + + req, err := http.NewRequest("POST", p.BaseURL+"/api/v1/protected/sberchat/chat", bytes.NewBuffer(jsonData)) + if err != nil { + return "", fmt.Errorf("ошибка создания запроса: %w", err) + } + + req.Header.Set("Content-Type", "application/json") + if p.JWTToken != "" { + req.Header.Set("Authorization", "Bearer "+p.JWTToken) + } + + resp, err := p.HTTPClient.Do(req) + if err != nil { + return "", fmt.Errorf("ошибка выполнения запроса: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("ошибка чтения ответа: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("ошибка API: %d - %s", resp.StatusCode, string(body)) + } + + var response ProxyChatResponse + if err := json.Unmarshal(body, &response); err != nil { + return "", fmt.Errorf("ошибка парсинга ответа: %w", err) + } + + if response.Error != "" { + return "", fmt.Errorf("ошибка прокси API: %s", response.Error) + } + + if response.Response == "" { + return "", fmt.Errorf("пустой ответ от API") + } + + return strings.TrimSpace(response.Response), nil +} + +// Health для ProxyAPIProvider +func (p *ProxyAPIProvider) Health() error { + req, err := http.NewRequest("GET", p.BaseURL+"/api/v1/protected/sberchat/health", nil) + if err != nil { + return fmt.Errorf("ошибка создания health check запроса: %w", err) + } + + if p.JWTToken != "" { + req.Header.Set("Authorization", "Bearer "+p.JWTToken) + } + + resp, err := p.HTTPClient.Do(req) + if err != nil { + return fmt.Errorf("ошибка health check: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("health check failed: %d", resp.StatusCode) + } + + var healthResponse ProxyHealthResponse + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("ошибка чтения health check ответа: %w", err) + } + + if err := json.Unmarshal(body, &healthResponse); err != nil { + return fmt.Errorf("ошибка парсинга health check ответа: %w", err) + } + + if healthResponse.Status != "ok" { + return fmt.Errorf("health check status: %s - %s", healthResponse.Status, healthResponse.Message) + } + + return nil +} + +// Chat для OllamaProvider +func (o *OllamaProvider) Chat(messages []Chat) (string, error) { + payload := Gpt3Request{ + Model: o.Model, + Messages: messages, + Stream: false, + Options: Gpt3Options{o.Temperature}, + } + + jsonData, err := json.Marshal(payload) + if err != nil { + return "", fmt.Errorf("ошибка маршалинга запроса: %w", err) + } + + req, err := http.NewRequest("POST", o.BaseURL+"/api/chat", bytes.NewBuffer(jsonData)) + if err != nil { + return "", fmt.Errorf("ошибка создания запроса: %w", err) + } + + req.Header.Set("Content-Type", "application/json") + + resp, err := o.HTTPClient.Do(req) + if err != nil { + return "", fmt.Errorf("ошибка выполнения запроса: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("ошибка чтения ответа: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("ошибка API: %d - %s", resp.StatusCode, string(body)) + } + + var response OllamaResponse + if err := json.Unmarshal(body, &response); err != nil { + return "", fmt.Errorf("ошибка парсинга ответа: %w", err) + } + + return strings.TrimSpace(response.Message.Content), nil +} + +// Health для OllamaProvider +func (o *OllamaProvider) Health() error { + req, err := http.NewRequest("GET", o.BaseURL+"/api/tags", nil) + if err != nil { + return fmt.Errorf("ошибка создания health check запроса: %w", err) + } + + resp, err := o.HTTPClient.Do(req) + if err != nil { + return fmt.Errorf("ошибка health check: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("health check failed: %d", resp.StatusCode) + } + + return nil +} diff --git a/main.go b/main.go index b02815e..36c47ee 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,7 @@ import ( "fmt" "math" "os" + "os/exec" "os/user" "path" "strings" @@ -27,33 +28,44 @@ var ( 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", "") +) + +const ( + colorRed = "\033[31m" + colorGreen = "\033[32m" + colorYellow = "\033[33m" + colorBlue = "\033[34m" + colorPurple = "\033[35m" + colorCyan = "\033[36m" + colorReset = "\033[0m" + colorBold = "\033[1m" ) func main() { app := &cli.App{ Name: "lcg", - Usage: "Linux Command GPT - Generate Linux commands from descriptions", + Usage: "Linux Command GPT - Генерация Linux команд из описаний", Version: Version, Commands: getCommands(), UsageText: ` -lcg [global options] +lcg [опции] <описание команды> -Examples: - lcg "I want to extract linux-command-gpt.tar.gz file" - lcg --file /path/to/file.txt "I want to list all directories with ls" +Примеры: + lcg "хочу извлечь файл linux-command-gpt.tar.gz" + lcg --file /path/to/file.txt "хочу вывести все директории с помощью ls" `, Description: ` -Linux Command GPT is a tool for generating Linux commands from natural language descriptions. -It supports reading parts of the prompt from files and allows saving, copying, or regenerating results. -Additional commands are available for managing API keys. +Linux Command GPT - инструмент для генерации Linux команд из описаний на естественном языке. +Поддерживает чтение частей промпта из файлов и позволяет сохранять, копировать или перегенерировать результаты. -Environment Variables: - LCG_HOST Endpoint for LLM API (default: http://192.168.87.108:11434/) - LCG_COMPLETIONS_PATH Relative API path (default: api/chat) - LCG_MODEL Model name (default: codegeex4) - LCG_PROMPT Default prompt text - LCG_API_KEY_FILE API key storage file (default: ~/.openai_api_key) - LCG_RESULT_FOLDER Results folder (default: ./gpt_results) +Переменные окружения: + LCG_HOST Endpoint для LLM API (по умолчанию: http://192.168.87.108:11434/) + LCG_MODEL Название модели (по умолчанию: codegeex4) + LCG_PROMPT Текст промпта по умолчанию + LCG_PROVIDER Тип провайдера: "ollama" или "proxy" (по умолчанию: ollama) + LCG_JWT_TOKEN JWT токен для proxy провайдера `, Flags: []cli.Flag{ &cli.StringFlag{ @@ -75,6 +87,7 @@ Environment Variables: args := c.Args().Slice() if len(args) == 0 { cli.ShowAppHelp(c) + showTips() return nil } executeMain(file, system, strings.Join(args, " ")) @@ -121,67 +134,174 @@ func getCommands() []*cli.Command { return nil }, }, + { + Name: "update-jwt", + Aliases: []string{"j"}, + Usage: "Update the JWT token for proxy API", + Action: func(c *cli.Context) error { + if PROVIDER_TYPE != "proxy" { + fmt.Println("JWT token is only needed for proxy provider") + return nil + } + + var jwtToken string + fmt.Print("JWT Token: ") + fmt.Scanln(&jwtToken) + + currentUser, _ := user.Current() + jwtFile := currentUser.HomeDir + "/.proxy_jwt_token" + if err := os.WriteFile(jwtFile, []byte(strings.TrimSpace(jwtToken)), 0600); err != nil { + fmt.Printf("Ошибка сохранения JWT токена: %v\n", err) + return err + } + + fmt.Println("JWT token updated.") + return nil + }, + }, + { + Name: "delete-jwt", + Aliases: []string{"dj"}, + Usage: "Delete the JWT token for proxy API", + Action: func(c *cli.Context) error { + if PROVIDER_TYPE != "proxy" { + fmt.Println("JWT token is only needed for proxy provider") + return nil + } + + currentUser, _ := user.Current() + jwtFile := currentUser.HomeDir + "/.proxy_jwt_token" + if err := os.Remove(jwtFile); err != nil && !os.IsNotExist(err) { + fmt.Printf("Ошибка удаления JWT токена: %v\n", err) + return err + } + + fmt.Println("JWT token deleted.") + return nil + }, + }, + { + Name: "health", + Aliases: []string{"he"}, // Изменено с "h" на "he" + Usage: "Check API health", + Action: func(c *cli.Context) error { + gpt3 := initGPT(PROMPT) + if err := gpt3.Health(); err != nil { + fmt.Printf("Health check failed: %v\n", err) + return err + } + fmt.Println("API is healthy.") + return nil + }, + }, + { + Name: "config", + 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) + if PROVIDER_TYPE == "proxy" { + fmt.Printf("JWT Token: %s\n", func() string { + if JWT_TOKEN != "" { + return "***set***" + } + currentUser, _ := user.Current() + jwtFile := currentUser.HomeDir + "/.proxy_jwt_token" + if _, err := os.Stat(jwtFile); err == nil { + return "***from file***" + } + return "***not set***" + }()) + } + return nil + }, + }, + { + Name: "history", + Aliases: []string{"hist"}, + Usage: "Show command history", + Action: func(c *cli.Context) error { + showHistory() + return nil + }, + }, } } func executeMain(file, system, commandInput string) { - // fmt.Println(system, commandInput) - // os.Exit(0) if file != "" { if err := reader.FileToPrompt(&commandInput, file); err != nil { - fmt.Println("Error reading file:", err) + printColored(fmt.Sprintf("❌ Ошибка чтения файла: %v\n", err), colorRed) return } } if _, err := os.Stat(RESULT_FOLDER); os.IsNotExist(err) { - os.MkdirAll(RESULT_FOLDER, 0755) + if err := os.MkdirAll(RESULT_FOLDER, 0755); err != nil { + printColored(fmt.Sprintf("❌ Ошибка создания папки результатов: %v\n", err), colorRed) + return + } } gpt3 := initGPT(system) - // if system != PROMPT { - // commandInput += ". " + system - // } - fmt.Println(commandInput) + printColored("🤖 Запрос: ", colorCyan) + fmt.Printf("%s\n", commandInput) + response, elapsed := getCommand(gpt3, commandInput) if response == "" { - fmt.Println("No response received.") + printColored("❌ Ответ не получен. Проверьте подключение к API.\n", colorRed) return } - fmt.Printf("Completed in %v seconds\n\n%s\n", elapsed, response) + printColored(fmt.Sprintf("✅ Выполнено за %.2f сек\n", elapsed), colorGreen) + printColored("\n📋 Команда:\n", colorYellow) + printColored(fmt.Sprintf(" %s\n\n", response), colorBold+colorGreen) + + saveToHistory(commandInput, response) handlePostResponse(response, gpt3, system, commandInput) } func initGPT(system string) gpt.Gpt3 { currentUser, _ := user.Current() - return gpt.Gpt3{ - CompletionUrl: HOST + COMPLETIONS, - Model: MODEL, - Prompt: system, - HomeDir: currentUser.HomeDir, - ApiKeyFile: API_KEY_FILE, - Temperature: 0.01, + + // Загружаем JWT токен в зависимости от провайдера + var jwtToken string + if PROVIDER_TYPE == "proxy" { + jwtToken = JWT_TOKEN + if jwtToken == "" { + // Пытаемся загрузить из файла + jwtFile := currentUser.HomeDir + "/.proxy_jwt_token" + if data, err := os.ReadFile(jwtFile); err == nil { + jwtToken = strings.TrimSpace(string(data)) + } + } } + + return *gpt.NewGpt3(PROVIDER_TYPE, HOST, jwtToken, MODEL, system, 0.01) } func getCommand(gpt3 gpt.Gpt3, cmd string) (string, float64) { gpt3.InitKey() start := time.Now() done := make(chan bool) + go func() { - loadingChars := []rune{'-', '\\', '|', '/'} + loadingChars := []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"} i := 0 for { select { case <-done: - fmt.Printf("\r") + fmt.Printf("\r%s", strings.Repeat(" ", 50)) + fmt.Print("\r") return default: - fmt.Printf("\rLoading %c", loadingChars[i]) + fmt.Printf("\r%s Обрабатываю запрос...", loadingChars[i]) i = (i + 1) % len(loadingChars) - time.Sleep(30 * time.Millisecond) + time.Sleep(100 * time.Millisecond) } } }() @@ -194,20 +314,23 @@ func getCommand(gpt3 gpt.Gpt3, cmd string) (string, float64) { } func handlePostResponse(response string, gpt3 gpt.Gpt3, system, cmd string) { - fmt.Print("\nOptions: (c)opy, (s)ave, (r)egenerate, (n)one: ") + fmt.Printf("Действия: (c)копировать, (s)сохранить, (r)перегенерировать, (e)выполнить, (n)ничего: ") var choice string fmt.Scanln(&choice) switch strings.ToLower(choice) { case "c": clipboard.WriteAll(response) - fmt.Println("Response copied to clipboard.") + fmt.Println("✅ Команда скопирована в буфер обмена") case "s": saveResponse(response, gpt3, cmd) case "r": + fmt.Println("🔄 Перегенерирую...") executeMain("", system, cmd) + case "e": + executeCommand(response) default: - fmt.Println("No action taken.") + fmt.Println(" До свидания!") } } @@ -224,9 +347,80 @@ func saveResponse(response string, gpt3 gpt.Gpt3, cmd string) { } } +func executeCommand(command string) { + fmt.Printf("🚀 Выполняю: %s\n", command) + fmt.Print("Продолжить? (y/N): ") + var confirm string + fmt.Scanln(&confirm) + + if strings.ToLower(confirm) == "y" || strings.ToLower(confirm) == "yes" { + cmd := exec.Command("bash", "-c", command) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + fmt.Printf("❌ Ошибка выполнения: %v\n", err) + } else { + fmt.Println("✅ Команда выполнена успешно") + } + } else { + fmt.Println("❌ Выполнение отменено") + } +} + func getEnv(key, defaultValue string) string { if value, exists := os.LookupEnv(key); exists { return value } return defaultValue } + +type CommandHistory struct { + Command string + Response string + Timestamp time.Time +} + +var commandHistory []CommandHistory + +func saveToHistory(cmd, response string) { + commandHistory = append(commandHistory, CommandHistory{ + Command: cmd, + Response: response, + Timestamp: time.Now(), + }) + + // Ограничиваем историю 100 командами + if len(commandHistory) > 100 { + commandHistory = commandHistory[1:] + } +} + +func showHistory() { + 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 printColored(text, color string) { + fmt.Printf("%s%s%s", color, text, colorReset) +} + +func showTips() { + printColored("💡 Подсказки:\n", colorCyan) + fmt.Println(" • Используйте --file для чтения из файла") + fmt.Println(" • Используйте --sys для изменения системного промпта") + fmt.Println(" • Команда 'history' покажет историю запросов") + fmt.Println(" • Команда 'config' покажет текущие настройки") + fmt.Println(" • Команда 'health' проверит доступность API") +} diff --git a/shell-code/build-full.sh b/shell-code/build-full.sh index db2ee9a..49ffa60 100644 --- a/shell-code/build-full.sh +++ b/shell-code/build-full.sh @@ -4,7 +4,7 @@ REPO=kuznetcovay/go-lcg VERSION=$1 if [ -z "$VERSION" ]; then - VERSION=v1.0.1 + VERSION=v1.1.0 fi BRANCH=main