Исправления в ветке main

This commit is contained in:
2026-10-22 12:42:32 +06:00
parent 933e5ccb58
commit 9b05edc263
31 changed files with 2433 additions and 114 deletions

155
main.go
View File

@@ -98,16 +98,23 @@ lcg [опции] <описание команды>
Переменные окружения:
Основные настройки:
LCG_HOST Endpoint для LLM API (по умолчанию: http://192.168.87.108:11434/)
LCG_MODEL Название модели (по умолчанию: hf.co/yandex/YandexGPT-5-Lite-8B-instruct-GGUF:Q4_K_M)
LCG_HOST Endpoint для LLM API (по умолчанию: http://192.168.87.108:11434/; для apimira: https://apimira.com)
LCG_MODEL Название модели (по умолчанию: hf.co/yandex/YandexGPT-5-Lite-8B-instruct-GGUF:Q4_K_M;
для apimira обязателен формат поставщик/имя_модели, например anthropic/claude-opus-4.7)
LCG_PROMPT Текст промпта по умолчанию
LCG_PROVIDER Тип провайдера: "ollama" или "proxy" (по умолчанию: ollama)
LCG_JWT_TOKEN JWT токен для proxy провайдера
LCG_PROVIDER Тип провайдера: "ollama", "proxy" или "apimira" (по умолчанию: ollama)
LCG_JWT_TOKEN API ключ для proxy и apimira провайдеров
LCG_PROMPT_ID ID промпта по умолчанию (по умолчанию: 1)
LCG_TIMEOUT Таймаут запроса в секундах (по умолчанию: 300)
LCG_COMPLETIONS_PATH Путь к API для завершений (по умолчанию: api/chat)
LCG_RETRIES Количество повторных попыток при временных ошибках (сеть, 429, 5xx;
по умолчанию: 3, задержки 5/10/20 сек; 0 — без повторов; флаг --retries имеет приоритет)
LCG_MODE Режим вывода результата: stdout или file (по умолчанию: stdout)
LCG_OUTPUT_FILE Выходной файл для --mode file (по умолчанию: ./lcg-out-script.sh)
LCG_CONTEXT_MAX_BYTES Лимит байт содержимого выходного файла, передаваемого модели как контекст (по умолчанию: 16000)
LCG_BACKUP_COUNT Количество резервных копий выходного файла (по умолчанию: 3)
LCG_COMPLETIONS_PATH Путь к API для завершений (по умолчанию: api/chat; для apimira: /v1/chat/completions)
LCG_PROXY_URL URL прокси для proxy провайдера (по умолчанию: /api/v1/protected/sberchat/chat)
LCG_API_KEY_FILE Файл с API ключом (по умолчанию: .openai_api_key)
LCG_API_KEY_FILE Файл с API ключом (по умолчанию: .openai_api_key; для apimira: .apimira_api_key, права 0600)
LCG_APP_NAME Название приложения (по умолчанию: Linux Command GPT)
LCG_ALLOW_THINK только для ollama: разрешить модели отправлять свои размышления ("1" или "true" = разрешено, пусто = запрещено). Имеет смысл для моделей, которые поддерживают эти действия: qwen3, deepseek.
@@ -144,6 +151,8 @@ lcg [опции] <описание команды>
Отладка и браузер:
LCG_DEBUG Включить режим отладки ("1" или "true" = включено, пусто = выключено)
LCG_DEBUG_FILE Путь к файлу отладки: писать туда всю активность — запросы, промпты,
тела запросов и ответов (аналог флага --debug-file)
LCG_BROWSER_PATH Путь к браузеру для автоматического открытия (команда serve --browser)
`,
Flags: []cli.Flag{
@@ -198,13 +207,39 @@ lcg [опции] <описание команды>
DefaultText: "120",
Value: 120,
},
&cli.BoolFlag{
Name: "debug",
Aliases: []string{"d"},
Usage: "Show debug information (request parameters and prompts)",
Value: false,
},
&cli.BoolFlag{
Name: "debug",
Aliases: []string{"d"},
Usage: "Show debug information (request parameters and prompts)",
Value: false,
},
&cli.StringFlag{
Name: "mode",
Usage: "Where to put the generated script: stdout or file",
Value: "stdout",
DefaultText: "stdout",
},
&cli.StringFlag{
Name: "output-file",
Aliases: []string{"o"},
Usage: "Output file for --mode file",
DefaultText: "./lcg-out-script.sh",
},
&cli.IntFlag{
Name: "backup-count",
Usage: "Number of backup copies (<file>.bak, <file>.bak.1, …) kept for --mode file (overrides LCG_BACKUP_COUNT)",
DefaultText: "LCG_BACKUP_COUNT or 3",
},
&cli.IntFlag{
Name: "retries",
Usage: "Number of retry attempts on transient errors, delays 5s/10s/20s (overrides LCG_RETRIES)",
DefaultText: "LCG_RETRIES or 3",
},
&cli.StringFlag{
Name: "debug-file",
Usage: "Append full activity log (requests and responses) to the given file (or use LCG_DEBUG_FILE)",
},
},
Action: func(c *cli.Context) error {
file := c.String("file")
system := c.String("sys")
@@ -237,6 +272,7 @@ lcg [опции] <описание команды>
PromptID: promptID,
Timeout: timeout,
Debug: c.Bool("debug"),
DebugFile: c.String("debug-file"),
}
disableHistory = config.AppConfig.MainFlags.NoHistory || config.AppConfig.IsNoHistoryEnabled()
@@ -268,12 +304,20 @@ lcg [опции] <описание команды>
}
}
if config.AppConfig.Query != "" {
executeMain(file, system, config.AppConfig.Query, timeout)
// Проверяем режим вывода до запроса к API
if mode := outputMode(); mode != modeStdout && mode != modeFile {
printColored(fmt.Sprintf("❌ Неизвестный режим вывода: %q (допустимо: stdout, file)\n", config.AppConfig.OutputMode), colorRed)
os.Exit(1)
}
// Позиционный запрос имеет приоритет над -Q
if len(args) > 0 {
executeMain(file, system, strings.Join(args, " "), timeout)
return nil
}
executeMain(file, system, strings.Join(args, " "), timeout)
// Сюда попадаем только если args пуст и Query != "" (пустой случай обработан выше)
executeMain(file, system, config.AppConfig.Query, timeout)
return nil
},
}
@@ -296,6 +340,29 @@ lcg [опции] <описание команды>
// applyAppFlagsToConfig применяет флаги приложения к конфигурации
// Работает как для основного Action, так и для команд
func applyAppFlagsToConfig(c *cli.Context) {
// Debug-файл: флаг --debug-file (env LCG_DEBUG_FILE подхватывает config.DebugLogf)
if df := c.String("debug-file"); df != "" {
config.AppConfig.MainFlags.DebugFile = df
}
// Количество бэкапов выходного файла: флаг --backup-count имеет приоритет над LCG_BACKUP_COUNT
if c.IsSet("backup-count") {
config.AppConfig.BackupCount = c.Int("backup-count")
}
// Количество ререквестов: флаг --retries имеет приоритет над LCG_RETRIES
if c.IsSet("retries") {
config.AppConfig.Retries = c.Int("retries")
}
// Режим вывода: --mode (stdout|file) и --output-file
if c.IsSet("mode") {
config.AppConfig.OutputMode = c.String("mode")
}
if outputFile := c.String("output-file"); outputFile != "" {
config.AppConfig.OutputFile = outputFile
}
// Применяем флаг model - проверяем и через IsSet, и значение напрямую
// так как IsSet может не работать для флагов без значения по умолчанию
if model := c.String("model"); model != "" {
@@ -312,9 +379,12 @@ func applyAppFlagsToConfig(c *cli.Context) {
config.AppConfig.Timeout = fmt.Sprintf("%d", c.Int("timeout"))
}
// Применяем флаг query (игнорируем значение по умолчанию)
if query := c.String("query"); query != "" && query != "Hello? what day is it today?" {
config.AppConfig.Query = query
// Применяем флаг query только если он задан явно:
// значение по умолчанию не должно затирать позиционный запрос
if c.IsSet("query") {
if query := c.String("query"); query != "" {
config.AppConfig.Query = query
}
}
}
@@ -416,6 +486,7 @@ func getCommands() []*cli.Command {
gpt3 := initGPT(config.AppConfig.Prompt, timeout)
models, err := gpt3.GetAvailableModels()
if err != nil {
config.DebugLogf("[ERROR] models: %v", err)
fmt.Printf("Ошибка получения моделей: %v\n", err)
return err
}
@@ -438,6 +509,7 @@ func getCommands() []*cli.Command {
}
gpt3 := initGPT(config.AppConfig.Prompt, timeout)
if err := gpt3.Health(); err != nil {
config.DebugLogf("[ERROR] health: %v", err)
fmt.Printf("Health check failed: %v\n", err)
return err
}
@@ -789,6 +861,11 @@ func executeMain(file, system, commandInput string, timeout int) {
system = config.AppConfig.Prompt
}
config.DebugLogf("[RUN] provider=%s host=%s model=%s timeout=%ds no-history=%t file=%q",
config.AppConfig.ProviderType, config.AppConfig.Host, config.AppConfig.Model, timeout, disableHistory, file)
config.DebugLogf("[SYSTEM] %s", system)
config.DebugLogf("[QUERY] %s", commandInput)
// Обеспечим папку результатов заранее (может понадобиться при действиях)
if _, err := os.Stat(config.AppConfig.ResultFolder); os.IsNotExist(err) {
if err := os.MkdirAll(config.AppConfig.ResultFolder, 0755); err != nil {
@@ -801,6 +878,8 @@ func executeMain(file, system, commandInput string, timeout int) {
if !disableHistory {
if found, hist := cmdPackage.CheckAndSuggestFromHistory(config.AppConfig.ResultHistory, commandInput); found && hist != nil {
fromHistory = true // Устанавливаем флаг, что ответ из истории
config.DebugLogf("[HISTORY] запрос уже встречался в истории: ответ показан из неё, запрос к API не выполнялся")
writeOutputIfFileMode(commandInput, hist.Response)
gpt3 := initGPT(system, timeout)
printColored("\nВНИМАНИЕ: ОТВЕТ СФОРМИРОВАН ИИ. ТРЕБУЕТСЯ ПРОВЕРКА И КРИТИЧЕСКИЙ АНАЛИЗ. ВОЗМОЖНЫ ОШИБКИ И ГАЛЛЮЦИНАЦИИ.\n", colorRed)
printColored("\n📋 Команда (из истории):\n", colorYellow)
@@ -817,12 +896,14 @@ func executeMain(file, system, commandInput string, timeout int) {
// Папка уже создана выше
ask, context := buildPromptWithContext(commandInput)
gpt3 := initGPT(system, timeout)
gpt3.Context = context
printColored("🤖 Запрос: ", colorCyan)
fmt.Printf("%s\n", commandInput)
response, elapsed := getCommand(gpt3, commandInput)
response, elapsed := getCommand(gpt3, ask)
if response == "" {
printColored("❌ Ответ не получен. Проверьте подключение к API.\n", colorRed)
return
@@ -834,6 +915,8 @@ func executeMain(file, system, commandInput string, timeout int) {
printColored("\n📋 Команда:\n", colorYellow)
printColored(fmt.Sprintf(" %s\n\n", response), colorBold+colorGreen)
writeOutputIfFileMode(commandInput, response)
// Сохраняем в историю (после завершения работы – т.е. позже, в зависимости от выбора действия)
// Здесь не сохраняем, чтобы учесть правило: сохранять после действия, отличного от v/vv/vvv
fromHistory = false // Сбрасываем флаг для новых запросов
@@ -846,16 +929,13 @@ func executeMain(file, system, commandInput string, timeout int) {
func initGPT(system string, timeout int) gpt.Gpt3 {
currentUser, _ := user.Current()
// Загружаем JWT токен в зависимости от провайдера
var jwtToken string
if config.AppConfig.ProviderType == "proxy" {
jwtToken = config.AppConfig.JwtToken
if jwtToken == "" {
// Пытаемся загрузить из файла
jwtFile := currentUser.HomeDir + "/.proxy_jwt_token"
if data, err := os.ReadFile(jwtFile); err == nil {
jwtToken = strings.TrimSpace(string(data))
}
// Загружаем токен/ключ в зависимости от провайдера
jwtToken := config.AppConfig.JwtToken
if config.AppConfig.ProviderType == "proxy" && jwtToken == "" {
// Пытаемся загрузить из файла (для apimira файл читает сам провайдер)
jwtFile := currentUser.HomeDir + "/.proxy_jwt_token"
if data, err := os.ReadFile(jwtFile); err == nil {
jwtToken = strings.TrimSpace(string(data))
}
}
@@ -888,6 +968,7 @@ func getCommand(gpt3 gpt.Gpt3, cmd string) (string, float64) {
done <- true
elapsed := math.Round(time.Since(start).Seconds()*100) / 100
config.DebugLogf("[TIMING] elapsed=%.2fs", elapsed)
return response, elapsed
}
@@ -1108,14 +1189,19 @@ func showShortConfig() {
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 config.AppConfig.ProviderType == "proxy" || config.AppConfig.ProviderType == "apimira" {
label := "JWT Token"
keyFile := "/.proxy_jwt_token"
if config.AppConfig.ProviderType == "apimira" {
label = "API Key"
keyFile = "/" + config.AppConfig.ApiKeyFile
}
fmt.Printf("%s: %s\n", label, func() string {
if config.AppConfig.JwtToken != "" {
return "***set***"
}
currentUser, _ := user.Current()
jwtFile := currentUser.HomeDir + "/.proxy_jwt_token"
if _, err := os.Stat(jwtFile); err == nil {
if _, err := os.Stat(currentUser.HomeDir + keyFile); err == nil {
return "***from file***"
}
return "***not set***"
@@ -1164,6 +1250,9 @@ func showFullConfig() {
}
currentUser, _ := user.Current()
jwtFile := currentUser.HomeDir + "/.proxy_jwt_token"
if config.AppConfig.ProviderType == "apimira" {
jwtFile = currentUser.HomeDir + "/" + config.AppConfig.ApiKeyFile
}
if _, err := os.Stat(jwtFile); err == nil {
return "***from file***"
}