mirror of
https://github.com/Direct-Dev-Ru/go-lcg.git
synced 2026-11-09 17:06:40 +00:00
Исправления в ветке main
This commit is contained in:
155
main.go
155
main.go
@@ -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***"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user