Compare commits

..

14 Commits

Author SHA1 Message Date
d7a53c218e release v2.0.0 2025-10-21 18:58:11 +06:00
58c2934924 alpha v.2 2025-10-21 18:51:30 +06:00
47671eb566 before result functions add 2025-10-21 09:10:22 +06:00
04d785db77 before refactoring 2025-10-19 23:41:11 +06:00
c57d981804 before add feature v - verbose 2025-10-18 22:40:07 +06:00
01f8adc979 release v1.1.5 2025-08-11 13:10:44 +06:00
e99fe76bef release v1.1.0 2025-08-11 13:06:14 +06:00
cc242e1192 improoved version 2025-08-11 13:01:27 +06:00
3e1c4594b1 added new proxy llm provider 2025-08-08 17:42:26 +06:00
ec2486ce3d release v1.0.4 2024-12-05 15:48:53 +06:00
46a0d9e45a release v1.0.3 2024-12-05 13:25:58 +06:00
12cd3fe6db moved to cli framework 2024-12-05 13:17:35 +06:00
7136fe4607 before refactor to cli framework 2024-12-05 11:15:38 +06:00
fa0a8565c3 release v1.0.2 2024-12-03 18:00:10 +06:00
33 changed files with 5811 additions and 459 deletions

6
.gitignore vendored
View File

@@ -12,4 +12,8 @@ dist/
shell-code/build.env shell-code/build.env
bin-linux-amd64/* bin-linux-amd64/*
bin-linux-arm64/* bin-linux-arm64/*
binaries-for-upload/* binaries-for-upload/*
gpt_results
shell-code/jwt.admin.token
run.sh
lcg_history.json

183
API_CONTRACT.md Normal file
View File

@@ -0,0 +1,183 @@
# Контракт API для провайдеров (proxy и ollama)
Этот документ описывает минимально необходимый API, который должен предоставлять сервер-провайдер (режимы: "proxy" и "ollama"), чтобы CLI-приложение работало корректно.
## Общие требования
- **Базовый URL** берётся из `config.AppConfig.Host`. Трейлинг-слэш на стороне клиента обрезается.
- **Таймаут** HTTP-запросов задаётся в секундах через конфигурацию (см. `config.AppConfig.Timeout`).
- **Кодирование**: все тела запросов и ответов — `application/json; charset=utf-8`.
- **Стриминг**: на данный момент клиент всегда запрашивает `stream=false`; стриминг не используется.
---
## Режим proxy
### Аутентификация
- Все защищённые эндпоинты требуют заголовок: `Authorization: Bearer <JWT>`.
- Токен берётся из `config.AppConfig.JwtToken`, либо из файла `~/.proxy_jwt_token`.
### 1) POST `/api/v1/protected/sberchat/chat`
- **Назначение**: получить единственный текстовый ответ LLM.
- **Заголовки**:
- `Content-Type: application/json`
- `Authorization: Bearer <JWT>` (обязательно)
- **Тело запроса** (минимально необходимые поля):
```json
{
"messages": [
{ "role": "system", "content": "<system_prompt>" },
{ "role": "user", "content": "<ask>" }
],
"model": "<model_name>",
"temperature": 0.5,
"top_p": 0.5,
"stream": false,
"random_words": ["linux", "command", "gpt"],
"fallback_string": "I'm sorry, I can't help with that. Please try again."
}
```
- **Ответ 200 OK**:
```json
{
"response": "<string>",
"usage": {
"prompt_tokens": 0,
"completion_tokens": 0,
"total_tokens": 0
},
"error": "",
"model": "<model_name>",
"timeout_seconds": 0
}
```
- **Ошибки**: любой статус != 200 воспринимается как ошибка. Желательно вернуть JSON вида:
```json
{ "error": "<message>" }
```
- **Пример cURL**:
```bash
curl -sS -X POST \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
"$HOST/api/v1/protected/sberchat/chat" \
-d '{
"messages": [
{"role":"system","content":"system prompt"},
{"role":"user","content":"user ask"}
],
"model":"GigaChat-2-Max",
"temperature":0.5,
"top_p":0.5,
"stream":false,
"random_words":["linux","command","gpt"],
"fallback_string":"I'm sorry, I can't help with that. Please try again."
}'
```
### 2) GET `/api/v1/protected/sberchat/health`
- **Назначение**: health-check API и получение части метаданных по умолчанию.
- **Заголовки**:
- `Authorization: Bearer <JWT>` (если сервер требует авторизацию на health)
- **Ответ 200 OK**:
```json
{
"status": "ok",
"message": "<string>",
"default_model": "<string>",
"default_timeout_seconds": 120
}
```
- **Ошибки**: любой статус != 200 считается падением health.
### Модели
- В текущей реализации клиент не запрашивает список моделей у proxy и использует фиксированный набор.
- Опционально можно реализовать эндпоинт для списка моделей (например, `GET /api/v1/protected/sberchat/models`) и расширить клиента позже.
---
## Режим ollama
### 1) POST `/api/chat`
- **Назначение**: синхронная генерация одного ответа (без стрима).
- **Заголовки**:
- `Content-Type: application/json`
- **Тело запроса**:
```json
{
"model": "<model_name>",
"stream": false,
"messages": [
{ "role": "system", "content": "<system_prompt>" },
{ "role": "user", "content": "<ask>" }
],
"options": {"temperature": 0.2}
}
```
- **Ответ 200 OK** (минимальный, который поддерживает клиент):
```json
{
"model": "<model_name>",
"message": { "role": "assistant", "content": "<string>" },
"done": true
}
```
- Прочие поля ответа (`total_duration`, `eval_count` и т.д.) допускаются, но клиент использует только `message.content`.
- **Ошибки**: любой статус != 200 считается ошибкой. Желательно возвращать читаемое тело.
### 2) GET `/api/tags`
- **Назначение**: используется как health-check и для получения списка моделей.
- **Ответ 200 OK**:
```json
{
"models": [
{ "name": "llama3:8b", "modified_at": "2024-01-01T00:00:00Z", "size": 123456789 },
{ "name": "qwen2.5:7b", "modified_at": "2024-01-02T00:00:00Z", "size": 987654321 }
]
}
```
- Любой другой статус трактуется как ошибка health.
---
## Семантика сообщений
- `messages` — массив объектов `{ "role": "system"|"user"|"assistant", "content": "<string>" }`.
- Клиент всегда отправляет как минимум 2 сообщения: системное и пользовательское.
- Ответ должен содержать один финальный текст в виде `response` (proxy) или `message.content` (ollama).
## Поведение при таймаутах
- Сервер должен завершать запрос в пределах `config.AppConfig.Timeout` секунд (значение передаётся клиентом в настройки HTTP-клиента; отдельным полем в запросе оно не отправляется, исключение — `proxy` может возвращать `timeout_seconds` в ответе как справочную информацию).
## Коды ответов и ошибки
- 200 — успешный ответ с телом согласно контракту.
- !=200 — ошибка; тело желательно в JSON с полем `error`.
## Изменения контракта
- Добавление новых полей в ответах, не используемых клиентом, допустимо при сохранении существующих.
- Переименование или удаление полей `response` (proxy) и `message.content` (ollama) нарушит совместимость.

View File

@@ -1,8 +1,9 @@
FROM --platform=${BUILDPLATFORM} golang:1.23-alpine AS builder FROM --platform=${BUILDPLATFORM} golang:1.24.6-alpine3.22 AS builder
ARG TARGETARCH ARG TARGETARCH
RUN apk add git && go install mvdan.cc/garble@latest # RUN apk add git
#&& go install mvdan.cc/garble@latest
WORKDIR /app WORKDIR /app
@@ -11,8 +12,8 @@ COPY . .
RUN echo $BUILDPLATFORM > buildplatform RUN echo $BUILDPLATFORM > buildplatform
RUN echo $TARGETARCH > targetarch RUN echo $TARGETARCH > targetarch
# RUN GOOS=linux GOARCH=$TARGETARCH go build -o output/go-ansible-vault RUN GOOS=linux GOARCH=$TARGETARCH go build -ldflags="-w -s" -o /app/go-lcg .
RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} garble -literals -tiny build -ldflags="-w -s" -o /app/go-lcg . #RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} garble -literals -tiny build -ldflags="-w -s" -o /app/go-lcg .
FROM alpine:latest FROM alpine:latest

View File

@@ -1,15 +1,16 @@
FROM --platform=${BUILDPLATFORM} golang:1.23-alpine AS build FROM --platform=${BUILDPLATFORM} golang:1.24.6-alpine3.22 AS build
ARG TARGETOS ARG TARGETOS
ARG TARGETARCH ARG TARGETARCH
RUN apk add git && go install mvdan.cc/garble@latest # RUN apk add git
#&& go install mvdan.cc/garble@latest
WORKDIR /src WORKDIR /src
ENV CGO_ENABLED=0 ENV CGO_ENABLED=0
COPY go.* . COPY go.* .
RUN go mod download RUN go mod download
COPY . . COPY . .
# RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o /out/go-ansible-vault . RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -ldflags="-w -s -buildid=" -trimpath -o /out/go-lcg .
RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} garble -literals -tiny build -ldflags="-w -s" -o /out/go-lcg . # RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} garble -literals -tiny build -ldflags="-w -s" -o /out/go-lcg .
FROM scratch AS bin-unix FROM scratch AS bin-unix
COPY --from=build /out/go-lcg /lcg COPY --from=build /out/go-lcg /lcg

View File

@@ -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

131
README.md
View File

@@ -1,72 +1,85 @@
## Linux Command GPT (lcg) # Linux Command GPT (lcg)
Get Linux commands in natural language with the power of ChatGPT.
### Installation This repo is forked from <https://github.com/asrul10/linux-command-gpt.git>
Build from source
```bash
> git clone --depth 1 https://github.com/asrul10/linux-command-gpt.git ~/.linux-command-gpt
> cd ~/.linux-command-gpt
> go build -o lcg
# Add to your environment $PATH
> ln -s ~/.linux-command-gpt/lcg ~/.local/bin
```
Or you can [download lcg executable file](https://github.com/asrul10/linux-command-gpt/releases) Generate Linux commands from natural language. Supports Ollama and Proxy backends, system prompts, different explanation levels (v/vv/vvv), and JSON history.
### Example Usage ## Installation
Build from source:
```bash ```bash
> lcg I want to extract linux-command-gpt.tar.gz file git clone --depth 1 https://github.com/Direct-Dev-Ru/go-lcg.git ~/.linux-command-gpt
Completed in 0.92 seconds cd ~/.linux-command-gpt
go build -o lcg
tar -xvzf linux-command-gpt.tar.gz # Add to your PATH
ln -s ~/.linux-command-gpt/lcg ~/.local/bin
Do you want to (c)opy, (r)egenerate, or take (N)o action on the command? (c/r/N):
``` ```
## Quick start
```bash ```bash
> LCG_PROMPT='Provide full response' LCG_MODEL=codellama:13b lcg 'i need bash script lcg "I want to extract linux-command-gpt.tar.gz file"
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):
``` ```
To use the "copy to clipboard" feature, you need to install either the `xclip` or `xsel` package. After generation you will see a CAPS warning that the answer is from AI and must be verified, the command, and the action menu:
### Options ```text
```bash ACTIONS: (c)opy, (s)ave, (r)egenerate, (e)xecute, (v|vv|vvv)explain, (n)othing
> 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
``` ```
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 (15)
- `--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`.

389
USAGE_GUIDE.md Normal file
View File

@@ -0,0 +1,389 @@
# Руководство по использованию (USAGE_GUIDE)
## Что это
Linux Command GPT (`lcg`) преобразует описание на естественном языке в готовую Linuxкоманду. Инструмент поддерживает сменные провайдеры LLM (Ollama или Proxy), управление системными промптами, историю запросов, сохранение результатов, HTTP сервер для просмотра результатов и интерактивные действия над сгенерированной командой.
## Требования
- Установленный Go (для сборки из исходников) или готовый бинарник.
- Для функции «скопировать в буфер обмена»: установите `xclip` или `xsel`.
```bash
# Debian/Ubuntu
sudo apt-get install xclip
# или
sudo apt-get install xsel
```
## Установка
Сборка из исходников:
```bash
git clone --depth 1 https://github.com/asrul10/linux-command-gpt.git ~/.linux-command-gpt
cd ~/.linux-command-gpt
go build -o lcg
# Добавьте бинарник в PATH
ln -s ~/.linux-command-gpt/lcg ~/.local/bin
```
Или скачайте готовый бинарник из раздела релизов.
## Быстрый старт
Простой запрос:
```bash
lcg "хочу извлечь файл linux-command-gpt.tar.gz"
```
Смешанный ввод: часть из файла, часть — текстом:
```bash
lcg --file /path/to/context.txt "хочу вывести список директорий с помощью ls"
```
После генерации вы увидите:
```text
🤖 Запрос: <ваше описание>
✅ Выполнено за X.XX сек
ВНИМАНИЕ: ОТВЕТ СФОРМИРОВАН ИИ. ТРЕБУЕТСЯ ПРОВЕРКА И КРИТИЧЕСКИЙ АНАЛИЗ. ВОЗМОЖНЫ ОШИБКИ И ГАЛЛЮЦИНАЦИИ.
📋 Команда:
<сгенерированная команда>
Действия: (c)копировать, (s)сохранить, (r)перегенерировать, (e)выполнить, (v|vv|vvv)подробно, (n)ничего:
```
## Переменные окружения
Можно настроить поведение без изменения командной строки.
| Переменная | Значение по умолчанию | Назначение |
| --- | --- | --- |
| `LCG_HOST` | `http://192.168.87.108:11434/` | Базовый URL API провайдера (для Ollama поставьте, например, `http://localhost:11434/`). |
| `LCG_PROXY_URL` | `/api/v1/protected/sberchat/chat` | Относительный путь эндпоинта для Proxy провайдера. |
| `LCG_COMPLETIONS_PATH` | `api/chat` | Относительный путь эндпоинта для Ollama. |
| `LCG_MODEL` | `hf.co/yandex/YandexGPT-5-Lite-8B-instruct-GGUF:Q4_K_M` | Имя модели у выбранного провайдера. |
| `LCG_PROMPT` | См. значение в коде | Содержимое системного промпта по умолчанию. |
| `LCG_API_KEY_FILE` | `.openai_api_key` | Файл с APIключом (для Ollama/Proxy не требуется). |
| `LCG_RESULT_FOLDER` | `~/.config/lcg/gpt_results` | Папка для сохранения результатов. |
| `LCG_PROVIDER` | `ollama` | Тип провайдера: `ollama` или `proxy`. |
| `LCG_JWT_TOKEN` | пусто | JWT токен для `proxy` провайдера (альтернатива — файл `~/.proxy_jwt_token`). |
| `LCG_PROMPT_ID` | `1` | ID системного промпта по умолчанию. |
| `LCG_TIMEOUT` | `300` | Таймаут запроса в секундах. |
| `LCG_RESULT_HISTORY` | `$(LCG_RESULT_FOLDER)/lcg_history.json` | Путь к JSONистории запросов. |
| `LCG_PROMPT_FOLDER` | `~/.config/lcg/gpt_sys_prompts` | Папка для хранения системных промптов. |
| `LCG_NO_HISTORY` | пусто | Если `1`/`true` — полностью отключает запись/обновление истории. |
| `LCG_SERVER_PORT` | `8080` | Порт для HTTP сервера просмотра результатов. |
| `LCG_SERVER_HOST` | `localhost` | Хост для HTTP сервера просмотра результатов. |
Примеры настройки:
```bash
# Ollama
export LCG_PROVIDER=ollama
export LCG_HOST=http://localhost:11434/
export LCG_MODEL=codegeex4
# Proxy
export LCG_PROVIDER=proxy
export LCG_HOST=http://localhost:8080
export LCG_MODEL=GigaChat-2
export LCG_JWT_TOKEN=your_jwt_token_here
```
## Базовый синтаксис
```bash
lcg [глобальные опции] <описание команды>
```
Глобальные опции:
- `--file, -f string` — прочитать часть запроса из файла и добавить к описанию.
- `--sys, -s string` — системный промпт (содержимое или ID как строка). Если не задан, используется `--prompt-id` или `LCG_PROMPT`.
- `--prompt-id, --pid int` — ID системного промпта (15 для стандартных, либо ваш кастомный ID).
- `--timeout, -t int` — таймаут запроса в секундах (по умолчанию 300).
- `--no-history, --nh` — отключить запись/обновление истории для текущего запуска.
- `--debug, -d` — показать отладочную информацию (параметры запроса и промпты).
- `--version, -v` — вывести версию.
- `--help, -h` — помощь.
## Подкоманды
- `lcg update-key` (`-u`): обновить APIключ. Для `ollama` и `proxy` не требуется — команда сообщит, что ключ не нужен.
- `lcg delete-key` (`-d`): удалить APIключ (не требуется для `ollama`/`proxy`).
- `lcg update-jwt` (`-j`): обновить JWT для `proxy`. Токен будет сохранён в `~/.proxy_jwt_token` (права `0600`).
- `lcg delete-jwt` (`-dj`): удалить JWT файл для `proxy`.
- `lcg models` (`-m`): показать доступные модели у текущего провайдера.
- `lcg health` (`-he`): проверить доступность API провайдера.
- `lcg config` (`-co`): показать текущую конфигурацию и состояние JWT.
- `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 list` (`-l`) — список всех промптов с содержимым в читаемом формате.
- `lcg prompts list --full` (`-f`) — полный вывод содержимого без обрезки длинных строк.
- `lcg prompts add` (`-a`) — добавить пользовательский промпт (по шагам в интерактиве).
- `lcg prompts delete <id>` (`-d`) — удалить пользовательский промпт по ID (>5).
- `lcg test-prompt <prompt-id> <описание>` (`-tp`): показать детали выбранного системного промпта и протестировать его на заданном описании.
- `lcg serve-result` (`serve`): запустить HTTP сервер для просмотра сохраненных результатов:
- `--port, -p` — порт сервера (по умолчанию из `LCG_SERVER_PORT`)
- `--host, -H` — хост сервера (по умолчанию из `LCG_SERVER_HOST`)
### Подробные объяснения (v/vv/vvv)
- `v` — кратко: что делает команда и ключевые опции, без альтернатив.
- `vv` — средне: назначение, основные ключи, 12 примера, кратко об альтернативах.
- `vvv` — максимально подробно: полный разбор ключей, сценариев, примеры, разбор альтернатив и сравнений.
После вывода подробного объяснения доступно вторичное меню: `Действия: (c)копировать, (s)сохранить, (r)перегенерировать, (n)ничего:`
## Провайдеры
### Ollama (`LCG_PROVIDER=ollama`)
- Требуется запущенный Ollama API (`LCG_HOST`, например `http://localhost:11434/`).
- `models`, `health` и генерация используют REST Ollama (`/api/tags`, `/api/chat`).
- APIключ не нужен.
### Proxy (`LCG_PROVIDER=proxy`)
- Требуется доступ к прокси‑серверу (`LCG_HOST`) и JWT (`LCG_JWT_TOKEN` или файл `~/.proxy_jwt_token`).
- Основные эндпоинты: `/api/v1/protected/sberchat/chat` и `/api/v1/protected/sberchat/health`.
- Команды `update-jwt`/`delete-jwt` помогают управлять токеном локально.
## Рекомендации по выбору провайдера, модели и таймаутов
### Выбор провайдера
- **Ollama**: выбирайте для локальной работы (офлайн/частные данные), когда есть доступ к GPU/CPU и готовность поддерживать локальные модели. Минимальные задержки сети, полная приватность.
- **Proxy**: выбирайте для централизованного хостинга моделей, более мощных/обновляемых моделей, простоты развёртывания у команды. Обязательно используйте HTTPS и корректный `JWT`.
### Выбор модели
- Для генерации Linuxкоманд подходят компактные «code»/«general» модели (по умолчанию `codegeex4`).
- Для подробных объяснений (`v`/`vv`/`vvv`) точность выше у более крупных моделей (например, семейства LLaMA/Qwen/GigaChat), но они медленнее.
- Русскоязычные запросы часто лучше обрабатываются в `GigaChat-*` (режим proxy), английские — в популярных opensource (Ollama).
- Балансируйте: скорость (малые модели) vs качество (крупные модели). Тестируйте `lcg models` и подбирайте `LCG_MODEL`.
### Таймауты
- Стартовые значения: локально с Ollama — **120300 сек**, удалённый proxy — **300600 сек**.
- Увеличьте таймаут для больших моделей/длинных запросов. Флаг `--timeout` перекрывает `LCG_TIMEOUT` на время запуска.
- Если часто видите таймауты — проверьте здоровье API (`lcg health`) и сетевую доступность `LCG_HOST`.
### Практические советы
- Если данные чувствительные — используйте Ollama локально и `--no-history` при необходимости.
- Для «черновой» команды начните с `Ollama + небольшая модель`; для «объяснений и альтернатив» используйте более крупную модель/Proxy.
- Не вставляйте секреты в запросы. Перед выполнением (`e`) проверяйте команду вручную.
- Для структуры API см. `API_CONTRACT.md` (эндпоинты и форматы запросов/ответов).
## Системные промпты
### Управление промптами
Системные промпты хранятся в папке, указанной в переменной `LCG_PROMPT_FOLDER` (по умолчанию: `~/.config/lcg/gpt_sys_prompts`).
**Логика загрузки:**
- Если файл `sys_prompts` **не существует** — создается файл с системными промптами (ID 15) и промптами подробности (ID 68)
- Если файл `sys_prompts` **существует** — загружаются все промпты из файла
- **Промпты подробности** (v/vv/vvv) сохраняются в том же файле с ID 6, 7, 8
### Встроенные промпты (ID 15)
| ID | Name | Описание |
| --- | --- | --- |
| 1 | linux-command | «Ответь только Linuxкомандой, без форматирования и объяснений». |
| 2 | linux-command-with-explanation | Сгенерируй команду и кратко объясни, что она делает (формат: COMMAND: explanation). |
| 3 | linux-command-safe | Безопасные команды (без потери данных). Вывод — только команда. |
| 4 | linux-command-verbose | Команда с подробными объяснениями флагов и альтернатив. |
| 5 | linux-command-simple | Простые команды, избегать сложных опций. |
### Промпты подробности (ID 68)
| ID | Name | Описание |
| --- | --- | --- |
| 6 | verbose-v | Подробный режим (v) - детальное объяснение команды |
| 7 | verbose-vv | Очень подробный режим (vv) - исчерпывающее объяснение с альтернативами |
| 8 | verbose-vvv | Максимально подробный режим (vvv) - полное руководство с примерами |
### Веб-интерфейс управления
Через HTTP сервер (`lcg serve-result`) доступно полное управление промптами:
- **Просмотр всех промптов** (встроенных и пользовательских)
- **Редактирование любых промптов** (включая встроенные)
- **Добавление новых промптов**
- **Удаление промптов**
- **Автоматическое сохранение** в файл `sys_prompts`
## Сохранение результатов
При выборе действия `s` ответ сохраняется в `LCG_RESULT_FOLDER` (по умолчанию: `~/.config/lcg/gpt_results`) в файл вида:
```text
gpt_request_<MODEL>_YYYY-MM-DD_HH-MM-SS.md
```
## HTTP сервер для просмотра результатов
Команда `lcg serve-result` запускает веб-сервер для удобного просмотра всех сохраненных результатов:
```bash
# Запуск с настройками по умолчанию
lcg serve-result
# Запуск на другом порту
lcg serve-result --port 9090
# Запуск на другом хосте
lcg serve-result --host 0.0.0.0 --port 8080
# Использование переменных окружения
export LCG_SERVER_PORT=3000
export LCG_SERVER_HOST=0.0.0.0
lcg serve-result
```
### Возможности веб-интерфейса
- **Главная страница** (`/`) — отображает все сохраненные файлы с превью
- **Статистика** — количество файлов, файлы за последние 7 дней
- **Просмотр файлов** (`/file/{filename}`) — отображение содержимого конкретного файла
- **Современный дизайн** — адаптивный интерфейс с карточками файлов
- **Сортировка** — файлы отсортированы по дате изменения (новые сверху)
- **Превью содержимого** — первые 200 символов каждого файла
Структура файла (команда):
- `# <заголовок>` — H1, это ваш запрос, при длине >120 символов обрезается до 116 + `...`.
- `## Prompt` — запрос (включая системный промпт).
- `## Response` — сгенерированная команда.
Структура файла (подробное объяснение):
- `# <заголовок>` — H1, ваш исходный запрос (с тем же правилом обрезки).
- `## Prompt` — исходный запрос.
- `## Command` — первая сгенерированная команда.
- `## Explanation and Alternatives (model: <MODEL>)` — подробное объяснение и альтернативы.
## Выполнение сгенерированной команды
Действие `e` запустит команду через `bash -c`. Перед запуском потребуется подтверждение `y/yes`. Всегда проверяйте команду вручную, особенно при операциях с файлами и сетью.
## Примеры
1. Базовый запрос с Ollama:
```bash
export LCG_PROVIDER=ollama
export LCG_HOST=http://localhost:11434/
export LCG_MODEL=codegeex4
lcg "хочу извлечь linux-command-gpt.tar.gz"
```
1. Полный ответ от LLM (пример настройки):
```bash
LCG_PROMPT='Provide full response' LCG_MODEL=codellama:13b \
lcg 'i need bash script to execute command by ssh on array of hosts'
```
1. Proxyпровайдер:
```bash
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
```
1. Работа с файлами и промптами:
```bash
lcg --file ./context.txt "сгенерируй команду jq для выборки поля name"
lcg --prompt-id 2 "удали все *.tmp в текущем каталоге"
lcg --sys 1 "показать размер каталога в человеко‑читаемом виде"
```
1. Диагностика и модели:
```bash
lcg health
lcg models
```
1. HTTP сервер для просмотра результатов:
```bash
# Запуск сервера
lcg serve-result
# Запуск на другом порту
lcg serve-result --port 9090
# Запуск на всех интерфейсах
lcg serve-result --host 0.0.0.0 --port 8080
```
## История
`lcg history` выводит историю текущего процесса (не сохраняется между запусками, максимум 100 записей):
```bash
lcg history list
```
## Типичные проблемы
- Нет ответа/таймаут: увеличьте `--timeout` или `LCG_TIMEOUT`, проверьте `LCG_HOST` и сетевую доступность.
- `health` падает: проверьте, что провайдер запущен и URL верный; для `proxy` — что JWT валиден (`lcg config`).
- Копирование не работает: установите `xclip` или `xsel`.
- Нет допуска к папке результатов: настройте `LCG_RESULT_FOLDER` или права доступа.
- Для `ollama`/`proxy` APIключ не нужен; команды `update-key`/`delete-key` просто уведомят об этом.
- HTTP сервер не запускается: проверьте, что порт свободен, используйте `--port` для смены порта.
- Веб-интерфейс не отображает файлы: убедитесь, что в `LCG_RESULT_FOLDER` есть `.md` файлы.
## 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.

View File

@@ -1 +1 @@
v1.0.1 v2.0.0

224
_main.go Normal file
View File

@@ -0,0 +1,224 @@
// package main
// import (
// _ "embed"
// "fmt"
// "math"
// "os"
// "os/user"
// "path"
// "strings"
// "time"
// "github.com/atotto/clipboard"
// "github.com/direct-dev-ru/linux-command-gpt/gpt"
// "github.com/direct-dev-ru/linux-command-gpt/reader"
// )
// //go:embed VERSION.txt
// var Version string
// var cwd, _ = os.Getwd()
// var (
// HOST = getEnv("LCG_HOST", "http://192.168.87.108:11434/")
// COMPLETIONS = getEnv("LCG_COMPLETIONS_PATH", "api/chat") // relative part of endpoint
// 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"))
// // HOST = "https://api.openai.com/v1/"
// // COMPLETIONS = "chat/completions"
// // MODEL = "gpt-4o-mini"
// // MODEL = "codellama:13b"
// // This file is created in the user's home directory
// // Example: /home/username/.openai_api_key
// // API_KEY_FILE = ".openai_api_key"
// HELP = `
// Usage: lcg [options]
// --help -h output usage information
// --version -v output the version number
// --file -f read part of command from file or bash feature $(...)
// --update-key -u update the API key
// --delete-key -d delete the API key
// Example Usage: lcg I want to extract linux-command-gpt.tar.gz file
// Example Usage: lcg --file /path/to/file.json I want to print object questions with jq
// Env Vars:
// LCG_HOST - defaults to "http://192.168.87.108:11434/" - endpoint for Ollama or other LLM API
// LCG_COMPLETIONS_PATH -defaults to "api/chat" - relative part of endpoint
// LCG_MODEL - defaults to "codegeex4"
// LCG_PROMPT - defaults to Reply with linux command and nothing else. Output with plain response - no need formatting. No need explanation. No need code blocks.
// LCG_API_KEY_FILE - defaults to ${HOME}/.openai_api_key - file with API key
// LCG_RESULT_FOLDER - defaults to $(pwd)/gpt_results - folder to save results
// `
// VERSION = Version
// CMD_HELP = 100
// CMD_VERSION = 101
// CMD_UPDATE = 102
// CMD_DELETE = 103
// CMD_COMPLETION = 110
// )
// // getEnv retrieves the value of the environment variable `key` or returns `defaultValue` if not set.
// func getEnv(key, defaultValue string) string {
// if value, exists := os.LookupEnv(key); exists {
// return value
// }
// return defaultValue
// }
// func handleCommand(cmd string) int {
// if cmd == "" || cmd == "--help" || cmd == "-h" {
// return CMD_HELP
// }
// if cmd == "--version" || cmd == "-v" {
// return CMD_VERSION
// }
// if cmd == "--update-key" || cmd == "-u" {
// return CMD_UPDATE
// }
// if cmd == "--delete-key" || cmd == "-d" {
// return CMD_DELETE
// }
// return CMD_COMPLETION
// }
// func getCommand(gpt3 gpt.Gpt3, cmd string) (string, float64) {
// gpt3.InitKey()
// s := time.Now()
// done := make(chan bool)
// go func() {
// loadingChars := []rune{'-', '\\', '|', '/'}
// i := 0
// for {
// select {
// case <-done:
// fmt.Printf("\r")
// return
// default:
// fmt.Printf("\rLoading %c", loadingChars[i])
// i = (i + 1) % len(loadingChars)
// time.Sleep(30 * time.Millisecond)
// }
// }
// }()
// r := gpt3.Completions(cmd)
// done <- true
// elapsed := time.Since(s).Seconds()
// elapsed = math.Round(elapsed*100) / 100
// if r == "" {
// return "", elapsed
// }
// return r, elapsed
// }
// func main() {
// currentUser, err := user.Current()
// if err != nil {
// panic(err)
// }
// args := os.Args
// cmd := ""
// file := ""
// if len(args) > 1 {
// start := 1
// if args[1] == "--file" || args[1] == "-f" {
// file = args[2]
// start = 3
// }
// cmd = strings.Join(args[start:], " ")
// }
// if file != "" {
// err := reader.FileToPrompt(&cmd, file)
// if err != nil {
// fmt.Println(err)
// return
// }
// }
// if _, err := os.Stat(RESULT_FOLDER); os.IsNotExist(err) {
// os.MkdirAll(RESULT_FOLDER, 0755)
// }
// h := handleCommand(cmd)
// if h == CMD_HELP {
// fmt.Println(HELP)
// return
// }
// if h == CMD_VERSION {
// fmt.Println(VERSION)
// return
// }
// gpt3 := gpt.Gpt3{
// CompletionUrl: HOST + COMPLETIONS,
// Model: MODEL,
// Prompt: PROMPT,
// HomeDir: currentUser.HomeDir,
// ApiKeyFile: API_KEY_FILE,
// Temperature: 0.01,
// }
// if h == CMD_UPDATE {
// gpt3.UpdateKey()
// return
// }
// if h == CMD_DELETE {
// gpt3.DeleteKey()
// return
// }
// c := "R"
// r := ""
// elapsed := 0.0
// for c == "R" || c == "r" {
// r, elapsed = getCommand(gpt3, cmd)
// c = "N"
// fmt.Printf("Completed in %v seconds\n\n", elapsed)
// fmt.Println(r)
// fmt.Print("\nDo you want to (c)opy, (s)ave to file, (r)egenerate, or take (N)o action on the command? (c/r/N): ")
// fmt.Scanln(&c)
// // no action
// if c == "N" || c == "n" {
// return
// }
// }
// if r == "" {
// return
// }
// // Copy to clipboard
// if c == "C" || c == "c" {
// clipboard.WriteAll(r)
// fmt.Println("\033[33mCopied to clipboard")
// return
// }
// if c == "S" || c == "s" {
// timestamp := time.Now().Format("2006-01-02_15-04-05") // Format: YYYY-MM-DD_HH-MM-SS
// filename := fmt.Sprintf("gpt_request_%s(%s).md", timestamp, gpt3.Model)
// filePath := path.Join(RESULT_FOLDER, filename)
// resultString := fmt.Sprintf("## Prompt:\n\n%s\n\n------------------\n\n## Response:\n\n%s\n\n", cmd+". "+gpt3.Prompt, r)
// os.WriteFile(filePath, []byte(resultString), 0644)
// fmt.Println("\033[33mSaved to file")
// return
// }
// }

189
cmd/explain.go Normal file
View File

@@ -0,0 +1,189 @@
package cmd
import (
"fmt"
"os"
"path"
"strings"
"time"
"github.com/atotto/clipboard"
"github.com/direct-dev-ru/linux-command-gpt/config"
"github.com/direct-dev-ru/linux-command-gpt/gpt"
)
// ExplainDeps инъекция зависимостей для вывода и окружения
type ExplainDeps struct {
DisableHistory bool
PrintColored func(string, string)
ColorPurple string
ColorGreen string
ColorRed string
ColorYellow string
GetCommand func(gpt.Gpt3, string) (string, float64)
}
// ShowDetailedExplanation делает дополнительный запрос с подробным описанием и альтернативами
func ShowDetailedExplanation(command string, gpt3 gpt.Gpt3, system, originalCmd string, timeout int, level int, deps ExplainDeps) {
// Получаем домашнюю директорию пользователя
homeDir, err := os.UserHomeDir()
if err != nil {
// Fallback к встроенным промптам
detailedSystem := getBuiltinVerbosePrompt(level)
ask := getBuiltinAsk(originalCmd, command)
processExplanation(detailedSystem, ask, gpt3, timeout, deps, originalCmd, command, system, level)
return
}
// Создаем менеджер промптов
pm := gpt.NewPromptManager(homeDir)
// Получаем промпт подробности по уровню
verbosePrompt := getVerbosePromptByLevel(pm.Prompts, level)
// Формируем ask в зависимости от языка
ask := getAskByLanguage(pm.GetCurrentLanguage(), originalCmd, command)
processExplanation(verbosePrompt, ask, gpt3, timeout, deps, originalCmd, command, system, level)
}
// getVerbosePromptByLevel возвращает промпт подробности по уровню
func getVerbosePromptByLevel(prompts []gpt.SystemPrompt, level int) string {
// Ищем промпт подробности по ID
for _, prompt := range prompts {
if prompt.ID >= 6 && prompt.ID <= 8 {
switch level {
case 1: // v
if prompt.ID == 6 {
return prompt.Content
}
case 2: // vv
if prompt.ID == 7 {
return prompt.Content
}
default: // vvv
if prompt.ID == 8 {
return prompt.Content
}
}
}
}
// Fallback к встроенным промптам
return getBuiltinVerbosePrompt(level)
}
// getBuiltinVerbosePrompt возвращает встроенный промпт подробности
func getBuiltinVerbosePrompt(level int) string {
switch level {
case 1: // v — кратко
return "Ты опытный Linux-инженер. Объясни КРАТКО, по делу: что делает команда и самые важные ключи. Без сравнений и альтернатив. Минимум текста. Пиши на русском."
case 2: // vv — средне
return "Ты опытный Linux-инженер. Дай сбалансированное объяснение: назначение команды, разбор основных ключей, 1-2 примера. Кратко упомяни 1-2 альтернативы без глубокого сравнения. Пиши на русском."
default: // vvv — максимально подробно
return "Ты опытный Linux-инженер. Дай подробное объяснение команды с полным разбором ключей, подкоманд, сценариев применения, примеров. Затем предложи альтернативные способы решения задачи другой командой/инструментами (со сравнениями и когда что лучше применять). Пиши на русском."
}
}
// getAskByLanguage формирует ask в зависимости от языка
func getAskByLanguage(lang, originalCmd, command string) string {
if lang == "ru" {
return fmt.Sprintf("Объясни подробно команду и предложи альтернативы. Исходная команда: %s. Исходное задание пользователя: %s", command, originalCmd)
}
// Английский
return fmt.Sprintf("Explain the command in detail and suggest alternatives. Original command: %s. Original user request: %s", command, originalCmd)
}
// getBuiltinAsk возвращает встроенный ask
func getBuiltinAsk(originalCmd, command string) string {
return fmt.Sprintf("Объясни подробно команду и предложи альтернативы. Исходная команда: %s. Исходное задание пользователя: %s", command, originalCmd)
}
// processExplanation обрабатывает объяснение
func processExplanation(detailedSystem, ask string, gpt3 gpt.Gpt3, timeout int, deps ExplainDeps, originalCmd string, command string, system string, level int) {
// Выводим debug информацию если включен флаг
if config.AppConfig.MainFlags.Debug {
printVerboseDebugInfo(detailedSystem, ask, gpt3, timeout, level)
}
detailed := gpt.NewGpt3(gpt3.ProviderType, config.AppConfig.Host, gpt3.ApiKey, gpt3.Model, detailedSystem, 0.2, timeout)
deps.PrintColored("\n🧠 Получаю подробное объяснение...\n", deps.ColorPurple)
explanation, elapsed := deps.GetCommand(*detailed, ask)
if explanation == "" {
deps.PrintColored("❌ Не удалось получить подробное объяснение.\n", deps.ColorRed)
return
}
deps.PrintColored(fmt.Sprintf("✅ Готово за %.2f сек\n", elapsed), deps.ColorGreen)
deps.PrintColored("\nВНИМАНИЕ: ОТВЕТ СФОРМИРОВАН ИИ. ТРЕБУЕТСЯ ПРОВЕРКА И КРИТИЧЕСКИЙ АНАЛИЗ. ВОЗМОЖНЫ ОШИБКИ И ГАЛЛЮЦИНАЦИИ.\n", deps.ColorRed)
deps.PrintColored("\n📖 Подробное объяснение и альтернативы:\n\n", deps.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, config.AppConfig.ResultFolder)
case "r":
fmt.Println("🔄 Перегенерирую подробное объяснение...")
ShowDetailedExplanation(command, gpt3, system, originalCmd, timeout, level, deps)
default:
fmt.Println(" Возврат в основное меню.")
}
if !deps.DisableHistory && (strings.ToLower(choice) == "c" || strings.ToLower(choice) == "s" || strings.ToLower(choice) == "n") {
SaveToHistory(config.AppConfig.ResultHistory, config.AppConfig.ResultFolder, originalCmd, command, system, explanation)
}
}
// saveExplanation сохраняет подробное объяснение и альтернативные способы
func saveExplanation(explanation string, model string, originalCmd string, commandResponse string, resultFolder string) {
timestamp := time.Now().Format("2006-01-02_15-04-05")
filename := fmt.Sprintf("gpt_explanation_%s_%s.md", model, timestamp)
filePath := path.Join(resultFolder, 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("Saved to %s\n", filePath)
}
}
// truncateTitle сокращает строку до 120 символов (по рунам), добавляя " ..." при усечении
func truncateTitle(s string) string {
const maxLen = 120
if runeCount := len([]rune(s)); runeCount <= maxLen {
return s
}
const head = 116
r := []rune(s)
if len(r) <= head {
return s
}
return string(r[:head]) + " ..."
}
// printVerboseDebugInfo выводит отладочную информацию для режимов v/vv/vvv
func printVerboseDebugInfo(detailedSystem, ask string, gpt3 gpt.Gpt3, timeout int, level int) {
fmt.Printf("\n🔍 DEBUG VERBOSE (v%d):\n", level)
fmt.Printf("📝 Системный промпт подробности:\n%s\n", detailedSystem)
fmt.Printf("💬 Запрос подробности:\n%s\n", ask)
fmt.Printf("⏱️ Таймаут: %d сек\n", timeout)
fmt.Printf("🌐 Провайдер: %s\n", gpt3.ProviderType)
fmt.Printf("🏠 Хост: %s\n", config.AppConfig.Host)
fmt.Printf("🧠 Модель: %s\n", gpt3.Model)
fmt.Printf("🎯 Уровень подробности: %d\n", level)
fmt.Printf("────────────────────────────────────────\n")
}

185
cmd/history.go Normal file
View File

@@ -0,0 +1,185 @@
package cmd
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"time"
)
type HistoryEntry 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"`
}
func read(historyPath string) ([]HistoryEntry, error) {
data, err := os.ReadFile(historyPath)
if err != nil || len(data) == 0 {
return nil, err
}
var items []HistoryEntry
if err := json.Unmarshal(data, &items); err != nil {
return nil, err
}
return items, nil
}
func write(historyPath string, entries []HistoryEntry) error {
for i := range entries {
entries[i].Index = i + 1
}
out, err := json.MarshalIndent(entries, "", " ")
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(historyPath), 0755); err != nil {
return err
}
return os.WriteFile(historyPath, out, 0644)
}
func ShowHistory(historyPath string, printColored func(string, string), colorYellow string) {
items, err := read(historyPath)
if err != nil || len(items) == 0 {
printColored("📝 История пуста\n", colorYellow)
return
}
printColored("📝 История (из файла):\n", colorYellow)
for _, h := range items {
ts := h.Timestamp.Format("2006-01-02 15:04:05")
fmt.Printf("%d. [%s] %s → %s\n", h.Index, ts, h.Command, h.Response)
}
}
func ViewHistoryEntry(historyPath string, id int, printColored func(string, string), colorYellow, colorBold, colorGreen string) {
items, err := read(historyPath)
if err != nil || len(items) == 0 {
fmt.Println("История пуста или недоступна")
return
}
var h *HistoryEntry
for i := range items {
if items[i].Index == id {
h = &items[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(historyPath string, id int) error {
items, err := read(historyPath)
if err != nil || len(items) == 0 {
return fmt.Errorf("история пуста или недоступна")
}
pos := -1
for i := range items {
if items[i].Index == id {
pos = i
break
}
}
if pos == -1 {
return fmt.Errorf("запись не найдена")
}
items = append(items[:pos], items[pos+1:]...)
return write(historyPath, items)
}
func SaveToHistory(historyPath, resultFolder, cmdText, response, system string, explanationOptional ...string) error {
var explanation string
if len(explanationOptional) > 0 {
explanation = explanationOptional[0]
}
items, _ := read(historyPath)
duplicateIndex := -1
for i, h := range items {
if strings.EqualFold(strings.TrimSpace(h.Command), strings.TrimSpace(cmdText)) {
duplicateIndex = i
break
}
}
entry := HistoryEntry{
Index: len(items) + 1,
Command: cmdText,
Response: response,
Explanation: explanation,
System: system,
Timestamp: time.Now(),
}
if duplicateIndex == -1 {
items = append(items, entry)
return write(historyPath, items)
}
fmt.Printf("\nЗапрос уже есть в истории от %s. Перезаписать? (y/N): ", items[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 = items[duplicateIndex].Index
items[duplicateIndex] = entry
return write(historyPath, items)
}
return nil
}
// SaveToHistoryFromHistory сохраняет запись из истории без запроса о перезаписи
func SaveToHistoryFromHistory(historyPath, resultFolder, cmdText, response, system, explanation string) error {
items, _ := read(historyPath)
duplicateIndex := -1
for i, h := range items {
if strings.EqualFold(strings.TrimSpace(h.Command), strings.TrimSpace(cmdText)) {
duplicateIndex = i
break
}
}
entry := HistoryEntry{
Index: len(items) + 1,
Command: cmdText,
Response: response,
Explanation: explanation,
System: system,
Timestamp: time.Now(),
}
if duplicateIndex == -1 {
items = append(items, entry)
return write(historyPath, items)
}
// Если дубликат найден, перезаписываем без запроса
entry.Index = items[duplicateIndex].Index
items[duplicateIndex] = entry
return write(historyPath, items)
}
func CheckAndSuggestFromHistory(historyPath, cmdText string) (bool, *HistoryEntry) {
items, err := read(historyPath)
if err != nil || len(items) == 0 {
return false, nil
}
for _, h := range items {
if strings.EqualFold(strings.TrimSpace(h.Command), strings.TrimSpace(cmdText)) {
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
}

1938
cmd/serve.go Normal file

File diff suppressed because it is too large Load Diff

99
config/config.go Normal file
View File

@@ -0,0 +1,99 @@
package config
import (
"os"
"path"
"strings"
)
type Config struct {
Cwd string
Host string
ProxyUrl string
Completions string
Model string
Prompt string
ApiKeyFile string
ResultFolder string
PromptFolder string
ProviderType string
JwtToken string
PromptID string
Timeout string
ResultHistory string
NoHistoryEnv string
MainFlags MainFlags
Server ServerConfig
}
type MainFlags struct {
File string
NoHistory bool
Sys string
PromptID int
Timeout int
Debug bool
}
type ServerConfig struct {
Port string
Host string
}
func getEnv(key, defaultValue string) string {
if value, exists := os.LookupEnv(key); exists {
return value
}
return defaultValue
}
func Load() Config {
cwd, _ := os.Getwd()
homedir, err := os.UserHomeDir()
if err != nil {
homedir = cwd
}
os.MkdirAll(path.Join(homedir, ".config", "lcg", "gpt_results"), 0755)
resultFolder := getEnv("LCG_RESULT_FOLDER", path.Join(homedir, ".config", "lcg", "gpt_results"))
os.MkdirAll(path.Join(homedir, ".config", "lcg", "gpt_sys_prompts"), 0755)
promptFolder := getEnv("LCG_PROMPT_FOLDER", path.Join(homedir, ".config", "lcg", "gpt_sys_prompts"))
return Config{
Cwd: cwd,
Host: getEnv("LCG_HOST", "http://192.168.87.108:11434/"),
ProxyUrl: getEnv("LCG_PROXY_URL", "/api/v1/protected/sberchat/chat"),
Completions: getEnv("LCG_COMPLETIONS_PATH", "api/chat"),
Model: getEnv("LCG_MODEL", "hf.co/yandex/YandexGPT-5-Lite-8B-instruct-GGUF:Q4_K_M"),
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."),
ApiKeyFile: getEnv("LCG_API_KEY_FILE", ".openai_api_key"),
ResultFolder: resultFolder,
PromptFolder: promptFolder,
ProviderType: getEnv("LCG_PROVIDER", "ollama"),
JwtToken: getEnv("LCG_JWT_TOKEN", ""),
PromptID: getEnv("LCG_PROMPT_ID", "1"),
Timeout: getEnv("LCG_TIMEOUT", "300"),
ResultHistory: getEnv("LCG_RESULT_HISTORY", path.Join(resultFolder, "lcg_history.json")),
NoHistoryEnv: getEnv("LCG_NO_HISTORY", ""),
Server: ServerConfig{
Port: getEnv("LCG_SERVER_PORT", "8080"),
Host: getEnv("LCG_SERVER_HOST", "localhost"),
},
}
}
func (c Config) IsNoHistoryEnabled() bool {
v := strings.TrimSpace(c.NoHistoryEnv)
if v == "" {
return false
}
vLower := strings.ToLower(v)
return vLower == "1" || vLower == "true"
}
var AppConfig Config
func init() {
AppConfig = Load()
}

9
go.mod
View File

@@ -3,3 +3,12 @@ module github.com/direct-dev-ru/linux-command-gpt
go 1.18 go 1.18
require github.com/atotto/clipboard v0.1.4 require github.com/atotto/clipboard v0.1.4
require gopkg.in/yaml.v3 v3.0.1
require (
github.com/cpuguy83/go-md2man/v2 v2.0.5 //indirect
github.com/russross/blackfriday/v2 v2.1.0
github.com/urfave/cli/v2 v2.27.5
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
)

11
go.sum
View File

@@ -1,2 +1,13 @@
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

124
gpt/builtin_prompts.go Normal file
View File

@@ -0,0 +1,124 @@
package gpt
import (
_ "embed"
"gopkg.in/yaml.v3"
)
//go:embed builtin_prompts.yaml
var builtinPromptsYAML string
var builtinPrompts string
// BuiltinPromptsData структура для YAML файла
type BuiltinPromptsData struct {
Prompts []BuiltinPrompt `yaml:"prompts"`
}
// BuiltinPrompt структура для встроенных промптов с поддержкой языков
type BuiltinPrompt struct {
ID int `yaml:"id"`
Name string `yaml:"name"`
Description map[string]string `yaml:"description"`
Content map[string]string `yaml:"content"`
}
// ToSystemPrompt конвертирует BuiltinPrompt в SystemPrompt для указанного языка
func (bp *BuiltinPrompt) ToSystemPrompt(lang string) SystemPrompt {
// Если язык не найден, используем английский по умолчанию
if _, exists := bp.Description[lang]; !exists {
lang = "en"
}
return SystemPrompt{
ID: bp.ID,
Name: bp.Name,
Description: bp.Description[lang],
Content: bp.Content[lang],
}
}
// GetBuiltinPrompts возвращает встроенные промпты из YAML (по умолчанию английские)
func GetBuiltinPrompts() []SystemPrompt {
return GetBuiltinPromptsByLanguage("en")
}
// GetBuiltinPromptsByLanguage возвращает встроенные промпты для указанного языка
func GetBuiltinPromptsByLanguage(lang string) []SystemPrompt {
var data BuiltinPromptsData
if err := yaml.Unmarshal([]byte(builtinPrompts), &data); err != nil {
// В случае ошибки возвращаем пустой массив
return []SystemPrompt{}
}
var result []SystemPrompt
for _, prompt := range data.Prompts {
result = append(result, prompt.ToSystemPrompt(lang))
}
return result
}
// IsBuiltinPrompt проверяет, является ли промпт встроенным
func IsBuiltinPrompt(prompt SystemPrompt) bool {
// Проверяем английскую версию
englishPrompts := GetBuiltinPromptsByLanguage("en")
for _, builtin := range englishPrompts {
if builtin.ID == prompt.ID {
if builtin.Content == prompt.Content &&
builtin.Name == prompt.Name &&
builtin.Description == prompt.Description {
return true
}
}
}
// Проверяем русскую версию
russianPrompts := GetBuiltinPromptsByLanguage("ru")
for _, builtin := range russianPrompts {
if builtin.ID == prompt.ID {
if builtin.Content == prompt.Content &&
builtin.Name == prompt.Name &&
builtin.Description == prompt.Description {
return true
}
}
}
return false
}
// GetBuiltinPromptByID возвращает встроенный промпт по ID (английская версия)
func GetBuiltinPromptByID(id int) *SystemPrompt {
builtinPrompts := GetBuiltinPrompts()
for _, prompt := range builtinPrompts {
if prompt.ID == id {
return &prompt
}
}
return nil
}
// GetBuiltinPromptByIDAndLanguage возвращает встроенный промпт по ID и языку
func GetBuiltinPromptByIDAndLanguage(id int, lang string) *SystemPrompt {
builtinPrompts := GetBuiltinPromptsByLanguage(lang)
for _, prompt := range builtinPrompts {
if prompt.ID == id {
return &prompt
}
}
return nil
}
func InitBuiltinPrompts(embeddedBuiltinPromptsYAML string) {
// Используем встроенный YAML, если переданный параметр пустой
if embeddedBuiltinPromptsYAML == "" {
builtinPrompts = builtinPromptsYAML
} else {
builtinPrompts = embeddedBuiltinPromptsYAML
}
}

262
gpt/builtin_prompts.yaml Normal file
View File

@@ -0,0 +1,262 @@
prompts:
- id: 1
name: "linux-command"
description:
en: "Main prompt for generating Linux commands"
ru: "Основной промпт для генерации Linux команд"
content:
en: |
You are a Linux command line expert.
Analyze the user's task, given in natural language, and suggest
a Linux command that will help accomplish this task, and provide a detailed explanation of what it does,
its parameters and possible use cases.
Focus on practical examples and best practices.
In the response, you should only provide the commands or sequence of commands ready to copy and execute
in the command line without any explanationformatting or code blocks, without ```bash``` or ```sh```, ` or ``` symbols.
ru: |
Вы эксперт по Linux командам и командной строке.
Проанализируйте задачу пользователя на естественном языке и предложите Linux команду или набор команд, которые помогут выполнить эту задачу, и предоставьте подробное объяснение того, что она делает, её параметры и возможные случаи использования.
Сосредоточьтесь на практических примерах и лучших практиках.
В ответе должна присутствовать только команда или последовательность команд,
готовая к копированию и выполнению в командной строке
без объяснений, выделений и форматирования наподобие ```bash``` или ```sh```, без символов ` или ```.
- id: 2
name: "linux-command-with-explanation"
description:
en: "Prompt with detailed command explanation"
ru: "Промпт с подробным объяснением команд"
content:
en: |
You are a Linux system administrator with extensive experience.
Generate Linux commands based on user task descriptions and provide comprehensive explanations.
Provide a detailed analysis including:
1. **Generated Command**: The Linux command that accomplishes the task
2. **Command Breakdown**: Explain each part of the command
3. **Parameters**: Explain each flag and option used
4. **Examples**: Show practical usage scenarios
5. **Security**: Highlight any security considerations
6. **Alternatives**: Suggest similar commands if applicable
7. **Best Practices**: Recommend optimal usage
Use clear formatting with headers and bullet points for readability.
ru: |
Вы системный администратор Linux с обширным опытом.
Генерируйте Linux команды на основе описаний задач пользователей и предоставляйте исчерпывающие объяснения.
Предоставьте подробный анализ, включая:
1. **Сгенерированная команда**: Linux команда, которая выполняет задачу
2. **Разбор команды**: Объясните каждую часть команды
3. **Параметры**: Объясните каждый используемый флаг и опцию
4. **Примеры**: Покажите практические сценарии использования
5. **Безопасность**: Выделите любые соображения безопасности
6. **Альтернативы**: Предложите похожие команды, если применимо
7. **Лучшие практики**: Рекомендуйте оптимальное использование
Используйте четкое форматирование с заголовками и маркерами для читаемости.
- id: 3
name: "linux-command-safe"
description:
en: "Safe command analysis with warnings"
ru: "Безопасный анализ команд с предупреждениями"
content:
en: |
You are a Linux security expert. Generate safe Linux commands based on user task descriptions with a focus on safety and security implications.
Provide a security-focused analysis:
1. **Generated Safe Command**: The secure Linux command for the task
2. **Safety Assessment**: Why this command is safe to run
3. **Potential Risks**: What could go wrong and how to mitigate
4. **Data Impact**: What files or data might be affected
5. **Permissions**: What permissions are required
6. **Recovery**: How to undo changes if needed
7. **Best Practices**: Safe alternatives or precautions
8. **Warnings**: Critical safety considerations
Always prioritize user safety and data protection.
ru: |
Вы эксперт по безопасности Linux. Генерируйте безопасные Linux команды на основе описаний задач пользователей с акцентом на безопасность и последствия для безопасности.
Предоставьте анализ, ориентированный на безопасность:
1. **Сгенерированная безопасная команда**: Безопасная Linux команда для задачи
2. **Оценка безопасности**: Почему эта команда безопасна для выполнения
3. **Потенциальные риски**: Что может пойти не так и как это смягчить
4. **Воздействие на данные**: Какие файлы или данные могут быть затронуты
5. **Разрешения**: Какие разрешения требуются
6. **Восстановление**: Как отменить изменения при необходимости
7. **Лучшие практики**: Безопасные альтернативы или меры предосторожности
8. **Предупреждения**: Критические соображения безопасности
Всегда приоритизируйте безопасность пользователя и защиту данных.
- id: 4
name: "linux-command-verbose"
description:
en: "Detailed analysis with technical details"
ru: "Подробный анализ с техническими деталями"
content:
en: |
You are a Linux kernel and system expert. Generate Linux commands based on user task descriptions and provide an in-depth technical analysis.
Deliver a comprehensive technical breakdown:
1. **Generated Command**: The Linux command that accomplishes the task
2. **System Level**: How the command interacts with the kernel
3. **Process Flow**: Step-by-step execution details
4. **Resource Usage**: CPU, memory, I/O implications
5. **File System**: Impact on files and directories
6. **Network**: Network operations if applicable
7. **Performance**: Optimization considerations
8. **Debugging**: Troubleshooting approaches
9. **Advanced Usage**: Expert-level techniques
Include technical details, system calls, and low-level operations.
ru: |
Вы эксперт по ядру Linux и системам. Генерируйте Linux команды на основе описаний задач пользователей и предоставляйте глубокий технический анализ.
Предоставьте исчерпывающий технический разбор:
1. **Сгенерированная команда**: Linux команда, которая выполняет задачу
2. **Системный уровень**: Как команда взаимодействует с ядром
3. **Поток выполнения**: Детали пошагового выполнения
4. **Использование ресурсов**: Последствия для CPU, памяти, I/O
5. **Файловая система**: Воздействие на файлы и каталоги
6. **Сеть**: Сетевые операции, если применимо
7. **Производительность**: Соображения по оптимизации
8. **Отладка**: Подходы к устранению неполадок
9. **Продвинутое использование**: Техники экспертного уровня
Включите технические детали, системные вызовы и низкоуровневые операции.
- id: 5
name: "linux-command-simple"
description:
en: "Simple and clear explanation"
ru: "Простое и понятное объяснение"
content:
en: |
You are a friendly Linux mentor. Explain the given command in simple, easy-to-understand terms.
Command: {{.command}}
Provide a beginner-friendly explanation:
1. **What it does**: Simple, clear description
2. **Why use it**: Common reasons to use this command
3. **Basic example**: Simple usage example
4. **What to expect**: Expected output or behavior
5. **Tips**: Helpful hints for beginners
Use plain language, avoid jargon, and focus on practical understanding.
ru: |
Вы дружелюбный наставник по Linux. Объясните данную команду простыми, понятными терминами.
Команда: {{.command}}
Предоставьте объяснение, подходящее для начинающих:
1. **Что она делает**: Простое, четкое описание
2. **Зачем использовать**: Общие причины использования этой команды
3. **Базовый пример**: Простой пример использования
4. **Что ожидать**: Ожидаемый вывод или поведение
5. **Советы**: Полезные подсказки для начинающих
Используйте простой язык, избегайте жаргона и сосредоточьтесь на практическом понимании.
- id: 6
name: "verbose-v"
description:
en: "Prompt for v mode (basic explanation)"
ru: "Промпт для режима v (базовое объяснение)"
content:
en: |
You are a Linux command expert. You can provide a clear and concise explanation of the given Linux command.
Your explanation should include:
1. What this command does for the task
2. Main parameters and their purpose
3. Common use cases
4. Any important warnings or considerations
ru: |
Вы эксперт по Linux командам. Вы можете предоставьте четкое и краткое объяснение заданной Linux команды.
Ваши краткие объяснения должны включать:
1. Что делает эта команда
2. Основные параметры и их назначение
3. Общие случаи использования
4. Любые важные предупреждения или соображения
- id: 7
name: "verbose-vv"
description:
en: "Prompt for vv mode (detailed explanation)"
ru: "Промпт для режима vv (подробное объяснение)"
content:
en: |
You are a Linux system expert. Provide a detailed technical explanation of the given command.
Provide a comprehensive analysis:
1. **Command Purpose**: What it accomplishes
2. **Syntax Breakdown**: Detailed parameter analysis
3. **Technical Details**: How it works internally
4. **Use Cases**: Practical scenarios and examples
5. **Performance Impact**: Resource usage and optimization
6. **Security Considerations**: Potential risks and mitigations
7. **Advanced Usage**: Expert techniques and tips
8. **Troubleshooting**: Common issues and solutions
Include technical depth while maintaining clarity.
ru: |
Вы эксперт по Linux системам. Предоставьте подробное техническое объяснение заданной команды.
Предоставьте исчерпывающий анализ:
1. **Цель команды**: Что она достигает
2. **Разбор синтаксиса**: Подробный анализ параметров
3. **Технические детали**: Как она работает внутренне
4. **Случаи использования**: Практические сценарии и примеры
5. **Влияние на производительность**: Использование ресурсов и оптимизация
6. **Соображения безопасности**: Потенциальные риски и меры по их снижению
7. **Продвинутое использование**: Экспертные техники и советы
8. **Устранение неполадок**: Общие проблемы и решения
Включите техническую глубину, сохраняя ясность.
- id: 8
name: "verbose-vvv"
description:
en: "Prompt for vvv mode (maximum detailed explanation)"
ru: "Промпт для режима vvv (максимально подробное объяснение)"
content:
en: |
You are a Linux kernel and system architecture expert. Provide an exhaustive technical analysis of the given command.
Deliver a comprehensive technical deep-dive:
1. **System Architecture**: How it fits into the Linux ecosystem
2. **Kernel Interaction**: System calls and kernel operations
3. **Process Management**: Process creation, scheduling, and lifecycle
4. **Memory Management**: Memory allocation and management
5. **File System Operations**: I/O operations and file system impact
6. **Network Stack**: Network operations and protocols
7. **Security Model**: Permissions, capabilities, and security implications
8. **Performance Analysis**: CPU, memory, I/O, and network impact
9. **Debugging and Profiling**: Advanced troubleshooting techniques
10. **Source Code Analysis**: Key implementation details
11. **Alternative Implementations**: Different approaches and trade-offs
12. **Historical Context**: Evolution and development history
Provide maximum technical depth with system-level insights, code examples, and architectural understanding.
ru: |
Вы эксперт по ядру Linux и системной архитектуре. Предоставьте исчерпывающий технический анализ заданной команды.
Предоставьте исчерпывающий технический глубокий анализ:
1. **Системная архитектура**: Как она вписывается в экосистему Linux
2. **Взаимодействие с ядром**: Системные вызовы и операции ядра
3. **Управление процессами**: Создание, планирование и жизненный цикл процессов
4. **Управление памятью**: Выделение и управление памятью
5. **Операции файловой системы**: I/O операции и воздействие на файловую систему
6. **Сетевой стек**: Сетевые операции и протоколы
7. **Модель безопасности**: Разрешения, возможности и последствия безопасности
8. **Анализ производительности**: Воздействие на CPU, память, I/O и сеть
9. **Отладка и профилирование**: Продвинутые техники устранения неполадок
10. **Анализ исходного кода**: Ключевые детали реализации
11. **Альтернативные реализации**: Разные подходы и компромиссы
12. **Исторический контекст**: Эволюция и история разработки
Предоставьте максимальную техническую глубину с системными инсайтами, примерами кода и архитектурным пониманием.

View File

@@ -1,24 +1,40 @@
package gpt package gpt
import ( import (
"bytes"
"encoding/json"
"fmt" "fmt"
"io"
"net/http"
"os" "os"
"path/filepath" "path/filepath"
"strings" "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 { type Gpt3 struct {
CompletionUrl string Provider Provider
Prompt string Prompt string
Model string Model string
HomeDir string HomeDir string
ApiKeyFile string ApiKeyFile string
ApiKey string ApiKey string
Temperature float64 Temperature float64
ProviderType string // "ollama", "proxy"
} }
type Chat struct { type Chat struct {
@@ -135,6 +151,11 @@ func (gpt3 *Gpt3) DeleteKey() {
} }
func (gpt3 *Gpt3) InitKey() { func (gpt3 *Gpt3) InitKey() {
// Для ollama и proxy провайдеров не нужен API ключ
if gpt3.ProviderType == "ollama" || gpt3.ProviderType == "proxy" {
return
}
load := gpt3.loadApiKey() load := gpt3.loadApiKey()
if load { if load {
return return
@@ -145,55 +166,51 @@ func (gpt3 *Gpt3) InitKey() {
gpt3.storeApiKey(apiKey) gpt3.storeApiKey(apiKey)
} }
func (gpt3 *Gpt3) Completions(ask string) string { // NewGpt3 создает новый экземпляр GPT с выбранным провайдером
req, err := http.NewRequest("POST", gpt3.CompletionUrl, nil) func NewGpt3(providerType, host, apiKey, model, prompt string, temperature float64, timeout int) *Gpt3 {
if err != nil { var provider Provider
panic(err)
}
req.Header.Set("Content-Type", "application/json")
// req.Header.Set("Authorization", "Bearer "+strings.TrimSpace(gpt3.ApiKey))
switch providerType {
case "proxy":
provider = NewProxyAPIProvider(host, apiKey, model, timeout) // apiKey используется как JWT токен
case "ollama":
provider = NewOllamaProvider(host, model, temperature, timeout)
default:
provider = NewOllamaProvider(host, model, temperature, timeout)
}
return &Gpt3{
Provider: provider,
Prompt: prompt,
Model: model,
ApiKey: apiKey,
Temperature: temperature,
ProviderType: providerType,
}
}
// Completions обновленный метод с поддержкой разных провайдеров
func (gpt3 *Gpt3) Completions(ask string) string {
messages := []Chat{ messages := []Chat{
{"system", gpt3.Prompt}, {"system", gpt3.Prompt},
{"user", ask + "." + gpt3.Prompt}, {"user", ask + ". " + gpt3.Prompt},
}
payload := Gpt3Request{
Model: gpt3.Model,
Messages: messages,
Stream: false,
Options: Gpt3Options{gpt3.Temperature},
} }
payloadJson, err := json.Marshal(payload) response, err := gpt3.Provider.Chat(messages)
if err != nil { if err != nil {
panic(err) fmt.Printf("Ошибка при выполнении запроса: %v\n", 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))
return "" return ""
} }
// var res Gpt3Response return response
var res OllamaResponse }
err = json.Unmarshal(body, &res)
if err != nil { // Health проверяет состояние провайдера
panic(err) func (gpt3 *Gpt3) Health() error {
} return gpt3.Provider.Health()
}
// return strings.TrimSpace(res.Choices[0].Message.Content)
return strings.TrimSpace(res.Message.Content) // GetAvailableModels возвращает список доступных моделей
func (gpt3 *Gpt3) GetAvailableModels() ([]string, error) {
return gpt3.Provider.GetAvailableModels()
} }

381
gpt/prompts.go Normal file
View File

@@ -0,0 +1,381 @@
package gpt
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/direct-dev-ru/linux-command-gpt/config"
)
// SystemPrompt представляет системный промпт
type SystemPrompt struct {
ID int `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Content string `json:"content"`
}
// PromptManager управляет системными промптами
type PromptManager struct {
Prompts []SystemPrompt
ConfigFile string
HomeDir string
Language string // Текущий язык для файла sys_prompts (en/ru)
}
// NewPromptManager создает новый менеджер промптов
func NewPromptManager(homeDir string) *PromptManager {
// Используем конфигурацию из модуля config
promptFolder := config.AppConfig.PromptFolder
// Путь к файлу sys_prompts
sysPromptsFile := filepath.Join(promptFolder, "sys_prompts")
pm := &PromptManager{
ConfigFile: sysPromptsFile,
HomeDir: homeDir,
}
// Проверяем, существует ли файл sys_prompts
if _, err := os.Stat(sysPromptsFile); os.IsNotExist(err) {
// Если файла нет, создаем его с системными промптами и промптами подробности
pm.createInitialPromptsFile()
}
// Загружаем все промпты из файла
pm.loadAllPrompts()
return pm
}
// createInitialPromptsFile создает начальный файл с системными промптами и промптами подробности
func (pm *PromptManager) createInitialPromptsFile() {
// Загружаем все встроенные промпты из YAML (английские по умолчанию)
pm.Prompts = GetBuiltinPrompts()
// Фикс: при первичном сохранении явно выставляем язык файла
if pm.Language == "" {
pm.Language = "en"
}
// Сохраняем все промпты в файл
pm.saveAllPrompts()
}
// loadDefaultPrompts загружает предустановленные промпты
func (pm *PromptManager) LoadDefaultPrompts() {
defaultPrompts := []SystemPrompt{
{
ID: 1,
Name: "linux-command",
Description: "Generate Linux commands (default)",
Content: "Reply with linux command and nothing else. Output with plain response - no need formatting. No need explanation. No need code blocks. No need ` symbols.",
},
{
ID: 2,
Name: "linux-command-with-explanation",
Description: "Generate Linux commands with explanation",
Content: "Generate a Linux command and provide a brief explanation of what it does. Format: COMMAND: explanation",
},
{
ID: 3,
Name: "linux-command-safe",
Description: "Generate safe Linux commands",
Content: "Generate a safe Linux command that won't cause data loss or system damage. Reply with linux command and nothing else. Output with plain response - no need formatting.",
},
{
ID: 4,
Name: "linux-command-verbose",
Description: "Generate Linux commands with detailed explanation",
Content: "Generate a Linux command and provide detailed explanation including what each flag does and potential alternatives.",
},
{
ID: 5,
Name: "linux-command-simple",
Description: "Generate simple Linux commands",
Content: "Generate a simple, easy-to-understand Linux command. Avoid complex flags and options when possible.",
},
}
pm.Prompts = defaultPrompts
}
// loadAllPrompts загружает все промпты из файла sys_prompts
func (pm *PromptManager) loadAllPrompts() {
if _, err := os.Stat(pm.ConfigFile); os.IsNotExist(err) {
return
}
data, err := os.ReadFile(pm.ConfigFile)
if err != nil {
return
}
// Новый формат: объект с полями language и prompts
var pf promptsFile
if err := json.Unmarshal(data, &pf); err == nil && len(pf.Prompts) > 0 {
pm.Language = pf.Language
pm.Prompts = pf.Prompts
return
}
// Старый формат: просто массив промптов
var prompts []SystemPrompt
if err := json.Unmarshal(data, &prompts); err == nil {
pm.Prompts = prompts
pm.Language = "en"
// Миграция в новый формат при следующем сохранении
}
}
// saveAllPrompts сохраняет все промпты в файл sys_prompts
// внутренний формат хранения файла sys_prompts
type promptsFile struct {
Language string `json:"language,omitempty"`
Prompts []SystemPrompt `json:"prompts"`
}
func (pm *PromptManager) saveAllPrompts() error {
pf := promptsFile{
Language: pm.Language,
Prompts: pm.Prompts,
}
data, err := json.MarshalIndent(pf, "", " ")
if err != nil {
return err
}
return os.WriteFile(pm.ConfigFile, data, 0644)
}
// SaveAllPrompts экспортированная версия saveAllPrompts
func (pm *PromptManager) SaveAllPrompts() error {
return pm.saveAllPrompts()
}
// GetCurrentLanguage возвращает текущий язык из файла промптов
func (pm *PromptManager) GetCurrentLanguage() string {
if pm.Language == "" {
return "en"
}
return pm.Language
}
// SetLanguage устанавливает язык для всех промптов
func (pm *PromptManager) SetLanguage(lang string) {
pm.Language = lang
}
// saveCustomPrompts сохраняет пользовательские промпты
func (pm *PromptManager) saveCustomPrompts() error {
// Находим пользовательские промпты (ID > 5)
var customPrompts []SystemPrompt
for _, prompt := range pm.Prompts {
if prompt.ID > 5 {
customPrompts = append(customPrompts, prompt)
}
}
data, err := json.MarshalIndent(customPrompts, "", " ")
if err != nil {
return err
}
return os.WriteFile(pm.ConfigFile, data, 0644)
}
// GetPromptByID возвращает промпт по ID
func (pm *PromptManager) GetPromptByID(id int) (*SystemPrompt, error) {
for _, prompt := range pm.Prompts {
if prompt.ID == id {
return &prompt, nil
}
}
return nil, fmt.Errorf("промпт с ID %d не найден", id)
}
// GetPromptByName возвращает промпт по имени
func (pm *PromptManager) GetPromptByName(name string) (*SystemPrompt, error) {
for _, prompt := range pm.Prompts {
if strings.EqualFold(prompt.Name, name) {
return &prompt, nil
}
}
return nil, fmt.Errorf("промпт с именем '%s' не найден", name)
}
// AddPrompt добавляет новый промпт
func (pm *PromptManager) AddPrompt(name, description, content string) error {
// Находим максимальный ID
maxID := 0
for _, prompt := range pm.Prompts {
if prompt.ID > maxID {
maxID = prompt.ID
}
}
newPrompt := SystemPrompt{
ID: maxID + 1,
Name: name,
Description: description,
Content: content,
}
pm.Prompts = append(pm.Prompts, newPrompt)
return pm.saveAllPrompts()
}
// UpdatePrompt обновляет существующий промпт
func (pm *PromptManager) UpdatePrompt(id int, name, description, content string) error {
for i, prompt := range pm.Prompts {
if prompt.ID == id {
pm.Prompts[i].Name = name
pm.Prompts[i].Description = description
pm.Prompts[i].Content = content
return pm.saveAllPrompts()
}
}
return fmt.Errorf("промпт с ID %d не найден", id)
}
// DeletePrompt удаляет промпт по ID
func (pm *PromptManager) DeletePrompt(id int) error {
for i, prompt := range pm.Prompts {
if prompt.ID == id {
pm.Prompts = append(pm.Prompts[:i], pm.Prompts[i+1:]...)
return pm.saveAllPrompts()
}
}
return fmt.Errorf("промпт с ID %d не найден", id)
}
// ListPrompts выводит список всех доступных промптов
func (pm *PromptManager) ListPrompts() {
pm.ListPromptsWithFull(false)
}
// ListPromptsWithFull выводит список промптов с опцией полного вывода
func (pm *PromptManager) ListPromptsWithFull(full bool) {
fmt.Println("📝 Доступные системные промпты:")
fmt.Println()
for i, prompt := range pm.Prompts {
// Разделитель между промптами
if i > 0 {
fmt.Println("─" + strings.Repeat("─", 60))
}
// Проверяем, является ли промпт встроенным и неизмененным
isDefault := pm.isDefaultPrompt(prompt)
// Заголовок промпта
if isDefault {
fmt.Printf("🔹 ID: %d | Название: %s | Встроенный\n", prompt.ID, prompt.Name)
} else {
fmt.Printf("🔹 ID: %d | Название: %s\n", prompt.ID, prompt.Name)
}
// Описание
if prompt.Description != "" {
fmt.Printf("📋 Описание: %s\n", prompt.Description)
}
// Содержимое промпта
fmt.Println("📄 Содержимое:")
fmt.Println("┌" + strings.Repeat("─", 58) + "┐")
// Разбиваем содержимое на строки и выводим с отступами
lines := strings.Split(prompt.Content, "\n")
for _, line := range lines {
if full {
// Полный вывод без обрезки - разбиваем длинные строки
if len(line) > 56 {
// Разбиваем длинную строку на части
for i := 0; i < len(line); i += 56 {
end := i + 56
if end > len(line) {
end = len(line)
}
fmt.Printf("│ %-56s │\n", line[i:end])
}
} else {
fmt.Printf("│ %-56s │\n", line)
}
} else {
// Обычный вывод с обрезкой
fmt.Printf("│ %-56s │\n", truncateString(line, 56))
}
}
fmt.Println("└" + strings.Repeat("─", 58) + "┘")
fmt.Println()
}
}
// isDefaultPrompt проверяет, является ли промпт встроенным и неизмененным
func (pm *PromptManager) isDefaultPrompt(prompt SystemPrompt) bool {
// Используем новую функцию из builtin_prompts.go
return IsBuiltinPrompt(prompt)
}
// IsDefaultPromptByID проверяет, является ли промпт встроенным только по ID (игнорирует содержимое)
func (pm *PromptManager) IsDefaultPromptByID(prompt SystemPrompt) bool {
// Проверяем, что ID находится в диапазоне встроенных промптов (1-8)
return prompt.ID >= 1 && prompt.ID <= 8
}
// GetRussianDefaultPrompts возвращает русские версии встроенных промптов
func GetRussianDefaultPrompts() []SystemPrompt {
return GetBuiltinPromptsByLanguage("ru")
}
// getDefaultPrompts возвращает оригинальные встроенные промпты
func (pm *PromptManager) GetDefaultPrompts() []SystemPrompt {
return GetBuiltinPrompts()
}
// AddCustomPrompt добавляет новый пользовательский промпт
func (pm *PromptManager) AddCustomPrompt(name, description, content string) error {
// Проверяем, что имя уникально
for _, prompt := range pm.Prompts {
if strings.EqualFold(prompt.Name, name) {
return fmt.Errorf("промпт с именем '%s' уже существует", name)
}
}
newPrompt := SystemPrompt{
ID: len(pm.Prompts) + 1,
Name: name,
Description: description,
Content: content,
}
pm.Prompts = append(pm.Prompts, newPrompt)
return pm.saveCustomPrompts()
}
// DeleteCustomPrompt удаляет пользовательский промпт
func (pm *PromptManager) DeleteCustomPrompt(id int) error {
if id <= 5 {
return fmt.Errorf("нельзя удалить предустановленный промпт")
}
for i, prompt := range pm.Prompts {
if prompt.ID == id {
pm.Prompts = append(pm.Prompts[:i], pm.Prompts[i+1:]...)
return pm.saveCustomPrompts()
}
}
return fmt.Errorf("промпт с ID %d не найден", id)
}
// truncateString обрезает строку до указанной длины
func truncateString(s string, maxLen int) string {
if len(s) <= maxLen {
return s
}
return s[:maxLen-3] + "..."
}

296
gpt/providers.go Normal file
View File

@@ -0,0 +1,296 @@
package gpt
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
)
// Provider интерфейс для работы с разными LLM провайдерами
type Provider interface {
Chat(messages []Chat) (string, error)
Health() error
GetAvailableModels() ([]string, 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
}
// OllamaTagsResponse структура ответа для получения списка моделей
type OllamaTagsResponse struct {
Models []struct {
Name string `json:"name"`
ModifiedAt string `json:"modified_at"`
Size int64 `json:"size"`
} `json:"models"`
}
func NewProxyAPIProvider(baseURL, jwtToken, model string, timeout int) *ProxyAPIProvider {
return &ProxyAPIProvider{
BaseURL: strings.TrimSuffix(baseURL, "/"),
JWTToken: jwtToken,
Model: model,
HTTPClient: &http.Client{Timeout: time.Duration(timeout) * time.Second},
}
}
func NewOllamaProvider(baseURL, model string, temperature float64, timeout int) *OllamaProvider {
return &OllamaProvider{
BaseURL: strings.TrimSuffix(baseURL, "/"),
Model: model,
Temperature: temperature,
HTTPClient: &http.Client{Timeout: time.Duration(timeout) * 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
}
// GetAvailableModels для ProxyAPIProvider возвращает фиксированный список
func (p *ProxyAPIProvider) GetAvailableModels() ([]string, error) {
return []string{"GigaChat-2", "GigaChat-2-Pro", "GigaChat-2-Max"}, nil
}
// GetAvailableModels возвращает список доступных моделей для провайдера
func (o *OllamaProvider) GetAvailableModels() ([]string, error) {
req, err := http.NewRequest("GET", o.BaseURL+"/api/tags", nil)
if err != nil {
return nil, fmt.Errorf("ошибка создания запроса: %w", err)
}
resp, err := o.HTTPClient.Do(req)
if err != nil {
return nil, fmt.Errorf("ошибка получения моделей: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("ошибка чтения ответа: %w", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("ошибка API: %d - %s", resp.StatusCode, string(body))
}
var response OllamaTagsResponse
if err := json.Unmarshal(body, &response); err != nil {
return nil, fmt.Errorf("ошибка парсинга ответа: %w", err)
}
var models []string
for _, model := range response.Models {
models = append(models, model.Name)
}
return models, nil
}

900
main.go
View File

@@ -5,212 +5,796 @@ import (
"fmt" "fmt"
"math" "math"
"os" "os"
"os/exec"
"os/user" "os/user"
"path" "strconv"
"strings" "strings"
"time" "time"
"github.com/atotto/clipboard" "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/gpt"
"github.com/direct-dev-ru/linux-command-gpt/reader" "github.com/direct-dev-ru/linux-command-gpt/reader"
"github.com/urfave/cli/v2"
) )
//go:embed VERSION.txt //go:embed VERSION.txt
var Version string var Version string
var cwd, _ = os.Getwd() // используем глобальный экземпляр конфига из пакета config
var ( // disableHistory управляет записью/обновлением истории на уровне процесса (флаг имеет приоритет над env)
HOST = getEnv("LCG_HOST", "http://192.168.87.108:11434/") var disableHistory bool
COMPLETIONS = getEnv("LCG_COMPLETIONS_PATH", "api/chat") // relative part of endpoint
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"))
// HOST = "https://api.openai.com/v1/" // fromHistory указывает, что текущий ответ взят из истории
// COMPLETIONS = "chat/completions" var fromHistory bool
// MODEL = "gpt-4o-mini" const (
// MODEL = "codellama:13b" colorRed = "\033[31m"
colorGreen = "\033[32m"
// This file is created in the user's home directory colorYellow = "\033[33m"
// Example: /home/username/.openai_api_key colorBlue = "\033[34m"
// API_KEY_FILE = ".openai_api_key" colorPurple = "\033[35m"
colorCyan = "\033[36m"
HELP = ` colorReset = "\033[0m"
colorBold = "\033[1m"
Usage: 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
Example Usage: lcg I want to extract linux-command-gpt.tar.gz file
Example Usage: lcg --file /path/to/file.json I want to print object questions with jq
`
VERSION = Version
CMD_HELP = 100
CMD_VERSION = 101
CMD_UPDATE = 102
CMD_DELETE = 103
CMD_COMPLETION = 110
) )
// getEnv retrieves the value of the environment variable `key` or returns `defaultValue` if not set. func main() {
func getEnv(key, defaultValue string) string { _ = colorBlue
if value, exists := os.LookupEnv(key); exists {
return value gpt.InitBuiltinPrompts("")
// Авто-инициализация sys_prompts при старте CLI (создаст файл при отсутствии)
if currentUser, err := user.Current(); err == nil {
_ = gpt.NewPromptManager(currentUser.HomeDir)
}
app := &cli.App{
Name: "lcg",
Usage: "Linux Command GPT - Генерация Linux команд из описаний",
Version: Version,
Commands: getCommands(),
UsageText: `
lcg [опции] <описание команды>
Примеры:
lcg "хочу извлечь файл linux-command-gpt.tar.gz"
lcg --file /path/to/file.txt "хочу вывести все директории с помощью ls"
`,
Description: `
Linux Command GPT - инструмент для генерации Linux команд из описаний на естественном языке.
Поддерживает чтение частей промпта из файлов и позволяет сохранять, копировать или перегенерировать результаты.
может задавать системный промпт или выбирать из предустановленных промптов.
Переменные окружения:
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{
Name: "file",
Aliases: []string{"f"},
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{
Name: "sys",
Aliases: []string{"s"},
Usage: "System prompt content or ID",
DefaultText: "Use prompt ID from LCG_PROMPT_ID or default prompt",
Value: "",
},
&cli.IntFlag{
Name: "prompt-id",
Aliases: []string{"pid"},
Usage: "System prompt ID (1-5 for default prompts)",
DefaultText: "1",
Value: 1,
},
&cli.IntFlag{
Name: "timeout",
Aliases: []string{"t"},
Usage: "Request timeout in seconds",
DefaultText: "120",
Value: 120,
},
&cli.BoolFlag{
Name: "debug",
Aliases: []string{"d"},
Usage: "Show debug information (request parameters and prompts)",
Value: false,
},
},
Action: func(c *cli.Context) error {
file := c.String("file")
system := c.String("sys")
// обновляем конфиг на основе флагов
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,
Debug: c.Bool("debug"),
}
disableHistory = config.AppConfig.MainFlags.NoHistory || config.AppConfig.IsNoHistoryEnabled()
args := c.Args().Slice()
if len(args) == 0 {
cli.ShowAppHelp(c)
showTips()
return nil
}
// Если указан prompt-id, загружаем соответствующий промпт
if system == "" && promptID > 0 {
currentUser, _ := user.Current()
pm := gpt.NewPromptManager(currentUser.HomeDir)
if prompt, err := pm.GetPromptByID(promptID); err == nil {
system = prompt.Content
} else {
fmt.Printf("Warning: Prompt ID %d not found, using default prompt\n", promptID)
}
}
executeMain(file, system, strings.Join(args, " "), timeout)
return nil
},
}
cli.VersionFlag = &cli.BoolFlag{
Name: "version",
Aliases: []string{"V", "v"},
Usage: "prints out version",
}
cli.VersionPrinter = func(cCtx *cli.Context) {
fmt.Printf("%s\n", cCtx.App.Version)
}
if err := app.Run(os.Args); err != nil {
fmt.Println("Error:", err)
os.Exit(1)
} }
return defaultValue
} }
func handleCommand(cmd string) int { func getCommands() []*cli.Command {
if cmd == "" || cmd == "--help" || cmd == "-h" { return []*cli.Command{
return CMD_HELP {
Name: "update-key",
Aliases: []string{"u"},
Usage: "Update the API key",
Action: func(c *cli.Context) error {
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(config.AppConfig.Timeout); err == nil {
timeout = t
}
gpt3 := initGPT(config.AppConfig.Prompt, timeout)
gpt3.UpdateKey()
fmt.Println("API key updated.")
return nil
},
},
{
Name: "delete-key",
Aliases: []string{"d"},
Usage: "Delete the API key",
Action: func(c *cli.Context) error {
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(config.AppConfig.Timeout); err == nil {
timeout = t
}
gpt3 := initGPT(config.AppConfig.Prompt, timeout)
gpt3.DeleteKey()
fmt.Println("API key deleted.")
return nil
},
},
{
Name: "update-jwt",
Aliases: []string{"j"},
Usage: "Update the JWT token for proxy API",
Action: func(c *cli.Context) error {
if config.AppConfig.ProviderType != "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 config.AppConfig.ProviderType != "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: "models",
Aliases: []string{"m"},
Usage: "Show available models",
Action: func(c *cli.Context) error {
timeout := 120 // default timeout
if t, err := strconv.Atoi(config.AppConfig.Timeout); err == nil {
timeout = t
}
gpt3 := initGPT(config.AppConfig.Prompt, timeout)
models, err := gpt3.GetAvailableModels()
if err != nil {
fmt.Printf("Ошибка получения моделей: %v\n", err)
return err
}
fmt.Printf("Доступные модели для провайдера %s:\n", config.AppConfig.ProviderType)
for i, model := range models {
fmt.Printf(" %d. %s\n", i+1, model)
}
return nil
},
},
{
Name: "health",
Aliases: []string{"he"}, // Изменено с "h" на "he"
Usage: "Check API health",
Action: func(c *cli.Context) error {
timeout := 120 // default timeout
if t, err := strconv.Atoi(config.AppConfig.Timeout); err == nil {
timeout = t
}
gpt3 := initGPT(config.AppConfig.Prompt, timeout)
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", 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 config.AppConfig.JwtToken != "" {
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",
Subcommands: []*cli.Command{
{
Name: "list",
Aliases: []string{"l"},
Usage: "List history entries",
Action: func(c *cli.Context) error {
if disableHistory {
printColored("📝 История отключена (--no-history / LCG_NO_HISTORY)\n", colorYellow)
} else {
cmdPackage.ShowHistory(config.AppConfig.ResultHistory, printColored, colorYellow)
}
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
}
if disableHistory {
fmt.Println("История отключена")
} else {
cmdPackage.ViewHistoryEntry(config.AppConfig.ResultHistory, id, printColored, colorYellow, colorBold, colorGreen)
}
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
}
if disableHistory {
fmt.Println("История отключена")
} else if err := cmdPackage.DeleteHistoryEntry(config.AppConfig.ResultHistory, id); err != nil {
fmt.Println(err)
}
return nil
},
},
},
},
{
Name: "prompts",
Aliases: []string{"p"},
Usage: "Manage system prompts",
Subcommands: []*cli.Command{
{
Name: "list",
Aliases: []string{"l"},
Usage: "List all available prompts",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "full",
Aliases: []string{"f"},
Usage: "Show full content without truncation",
},
},
Action: func(c *cli.Context) error {
currentUser, _ := user.Current()
pm := gpt.NewPromptManager(currentUser.HomeDir)
full := c.Bool("full")
pm.ListPromptsWithFull(full)
return nil
},
},
{
Name: "add",
Aliases: []string{"a"},
Usage: "Add a new custom prompt",
Action: func(c *cli.Context) error {
currentUser, _ := user.Current()
pm := gpt.NewPromptManager(currentUser.HomeDir)
var name, description, content string
fmt.Print("Название промпта: ")
fmt.Scanln(&name)
fmt.Print("Описание: ")
fmt.Scanln(&description)
fmt.Print("Содержание промпта: ")
fmt.Scanln(&content)
if err := pm.AddCustomPrompt(name, description, content); err != nil {
fmt.Printf("Ошибка добавления промпта: %v\n", err)
return err
}
fmt.Println("Промпт успешно добавлен!")
return nil
},
},
{
Name: "delete",
Aliases: []string{"d"},
Usage: "Delete a custom prompt",
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 {
fmt.Println("Неверный ID промпта")
return err
}
currentUser, _ := user.Current()
pm := gpt.NewPromptManager(currentUser.HomeDir)
if err := pm.DeleteCustomPrompt(id); err != nil {
fmt.Printf("Ошибка удаления промпта: %v\n", err)
return err
}
fmt.Println("Промпт успешно удален!")
return nil
},
},
},
},
{
Name: "test-prompt",
Aliases: []string{"tp"},
Usage: "Test a specific prompt ID",
Action: func(c *cli.Context) error {
if c.NArg() == 0 {
fmt.Println("Usage: lcg test-prompt <prompt-id> <command>")
return nil
}
var promptID int
if _, err := fmt.Sscanf(c.Args().First(), "%d", &promptID); err != nil {
fmt.Println("Invalid prompt ID")
return err
}
currentUser, _ := user.Current()
pm := gpt.NewPromptManager(currentUser.HomeDir)
prompt, err := pm.GetPromptByID(promptID)
if err != nil {
fmt.Printf("Prompt ID %d not found\n", promptID)
return err
}
fmt.Printf("Testing prompt ID %d: %s\n", promptID, prompt.Name)
fmt.Printf("Description: %s\n", prompt.Description)
fmt.Printf("Content: %s\n", prompt.Content)
if len(c.Args().Slice()) > 1 {
command := strings.Join(c.Args().Slice()[1:], " ")
fmt.Printf("\nTesting with command: %s\n", command)
timeout := 120 // default timeout
if t, err := strconv.Atoi(config.AppConfig.Timeout); err == nil {
timeout = t
}
executeMain("", prompt.Content, command, timeout)
}
return nil
},
},
{
Name: "serve-result",
Aliases: []string{"serve"},
Usage: "Start HTTP server to browse saved results",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "port",
Aliases: []string{"p"},
Usage: "Server port",
Value: config.AppConfig.Server.Port,
},
&cli.StringFlag{
Name: "host",
Aliases: []string{"H"},
Usage: "Server host",
Value: config.AppConfig.Server.Host,
},
},
Action: func(c *cli.Context) error {
port := c.String("port")
host := c.String("host")
printColored(fmt.Sprintf("🌐 Запускаю HTTP сервер на %s:%s\n", host, port), colorCyan)
printColored(fmt.Sprintf("📁 Папка результатов: %s\n", config.AppConfig.ResultFolder), colorYellow)
printColored(fmt.Sprintf("🔗 Откройте в браузере: http://%s:%s\n", host, port), colorGreen)
return cmdPackage.StartResultServer(host, port)
},
},
} }
if cmd == "--version" || cmd == "-v" { }
return CMD_VERSION
func executeMain(file, system, commandInput string, timeout int) {
// Выводим debug информацию если включен флаг
if config.AppConfig.MainFlags.Debug {
printDebugInfo(file, system, commandInput, timeout)
} }
if cmd == "--update-key" || cmd == "-u" { if file != "" {
return CMD_UPDATE if err := reader.FileToPrompt(&commandInput, file); err != nil {
printColored(fmt.Sprintf("❌ Ошибка чтения файла: %v\n", err), colorRed)
return
}
} }
if cmd == "--delete-key" || cmd == "-d" {
return CMD_DELETE // Если system пустой, используем дефолтный промпт
if system == "" {
system = config.AppConfig.Prompt
} }
return CMD_COMPLETION
// Обеспечим папку результатов заранее (может понадобиться при действиях)
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
}
}
// Проверка истории: если такой запрос уже встречался — предложить открыть из истории
if !disableHistory {
if found, hist := cmdPackage.CheckAndSuggestFromHistory(config.AppConfig.ResultHistory, commandInput); found && hist != nil {
fromHistory = true // Устанавливаем флаг, что ответ из истории
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, hist.Explanation)
return
}
}
// Папка уже создана выше
gpt3 := initGPT(system, timeout)
printColored("🤖 Запрос: ", colorCyan)
fmt.Printf("%s\n", commandInput)
response, elapsed := getCommand(gpt3, commandInput)
if response == "" {
printColored("❌ Ответ не получен. Проверьте подключение к API.\n", colorRed)
return
}
printColored(fmt.Sprintf("✅ Выполнено за %.2f сек\n", elapsed), colorGreen)
// Обязательное предупреждение перед первым ответом
printColored("\nВНИМАНИЕ: ОТВЕТ СФОРМИРОВАН ИИ. ТРЕБУЕТСЯ ПРОВЕРКА И КРИТИЧЕСКИЙ АНАЛИЗ. ВОЗМОЖНЫ ОШИБКИ И ГАЛЛЮЦИНАЦИИ.\n", colorRed)
printColored("\n📋 Команда:\n", colorYellow)
printColored(fmt.Sprintf(" %s\n\n", response), colorBold+colorGreen)
// Сохраняем в историю (после завершения работы т.е. позже, в зависимости от выбора действия)
// Здесь не сохраняем, чтобы учесть правило: сохранять после действия, отличного от v/vv/vvv
fromHistory = false // Сбрасываем флаг для новых запросов
handlePostResponse(response, gpt3, system, commandInput, timeout, "")
}
// checkAndSuggestFromHistory проверяет файл истории и при совпадении запроса предлагает показать сохраненный результат
// moved to history.go
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))
}
}
}
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) { func getCommand(gpt3 gpt.Gpt3, cmd string) (string, float64) {
gpt3.InitKey() gpt3.InitKey()
s := time.Now() start := time.Now()
done := make(chan bool) done := make(chan bool)
go func() { go func() {
loadingChars := []rune{'-', '\\', '|', '/'} loadingChars := []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
i := 0 i := 0
for { for {
select { select {
case <-done: case <-done:
fmt.Printf("\r") fmt.Printf("\r%s", strings.Repeat(" ", 50))
fmt.Print("\r")
return return
default: default:
fmt.Printf("\rLoading %c", loadingChars[i]) fmt.Printf("\r%s Обрабатываю запрос...", loadingChars[i])
i = (i + 1) % len(loadingChars) i = (i + 1) % len(loadingChars)
time.Sleep(30 * time.Millisecond) time.Sleep(100 * time.Millisecond)
} }
} }
}() }()
r := gpt3.Completions(cmd) response := gpt3.Completions(cmd)
done <- true done <- true
elapsed := time.Since(s).Seconds() elapsed := math.Round(time.Since(start).Seconds()*100) / 100
elapsed = math.Round(elapsed*100) / 100
if r == "" { return response, elapsed
return "", elapsed
}
return r, elapsed
} }
func main() { func handlePostResponse(response string, gpt3 gpt.Gpt3, system, cmd string, timeout int, explanation string) {
currentUser, err := user.Current() fmt.Printf("Действия: (c)копировать, (s)сохранить, (r)перегенерировать, (e)выполнить, (v|vv|vvv)подробно, (n)ничего: ")
if err != nil { var choice string
panic(err) fmt.Scanln(&choice)
}
args := os.Args switch strings.ToLower(choice) {
cmd := "" case "c":
file := "" clipboard.WriteAll(response)
if len(args) > 1 { fmt.Println("✅ Команда скопирована в буфер обмена")
start := 1 if !disableHistory {
if args[1] == "--file" || args[1] == "-f" { if fromHistory {
file = args[2] cmdPackage.SaveToHistoryFromHistory(config.AppConfig.ResultHistory, config.AppConfig.ResultFolder, cmd, response, gpt3.Prompt, explanation)
start = 3 } else {
cmdPackage.SaveToHistory(config.AppConfig.ResultHistory, config.AppConfig.ResultFolder, cmd, response, gpt3.Prompt)
}
} }
cmd = strings.Join(args[start:], " ") case "s":
} if fromHistory && strings.TrimSpace(explanation) != "" {
saveResponse(response, gpt3.Model, gpt3.Prompt, cmd, explanation)
if file != "" { } else {
err := reader.FileToPrompt(&cmd, file) saveResponse(response, gpt3.Model, gpt3.Prompt, cmd)
if err != nil {
fmt.Println(err)
return
} }
} if !disableHistory {
if fromHistory {
if _, err := os.Stat(RESULT_FOLDER); os.IsNotExist(err) { cmdPackage.SaveToHistoryFromHistory(config.AppConfig.ResultHistory, config.AppConfig.ResultFolder, cmd, response, gpt3.Prompt, explanation)
os.MkdirAll(RESULT_FOLDER, 0755) } else {
} cmdPackage.SaveToHistory(config.AppConfig.ResultHistory, config.AppConfig.ResultFolder, cmd, response, gpt3.Prompt)
}
h := handleCommand(cmd) }
case "r":
if h == CMD_HELP { fmt.Println("🔄 Перегенерирую...")
fmt.Println(HELP) executeMain("", system, cmd, timeout)
return case "e":
} executeCommand(response)
if !disableHistory {
if h == CMD_VERSION { if fromHistory {
fmt.Println(VERSION) cmdPackage.SaveToHistoryFromHistory(config.AppConfig.ResultHistory, config.AppConfig.ResultFolder, cmd, response, gpt3.Prompt, explanation)
return } else {
} cmdPackage.SaveToHistory(config.AppConfig.ResultHistory, config.AppConfig.ResultFolder, cmd, response, gpt3.Prompt)
}
gpt3 := gpt.Gpt3{ }
CompletionUrl: HOST + COMPLETIONS, case "v", "vv", "vvv":
Model: MODEL, level := len(choice) // 1, 2, 3
Prompt: PROMPT, deps := cmdPackage.ExplainDeps{
HomeDir: currentUser.HomeDir, DisableHistory: disableHistory,
ApiKeyFile: API_KEY_FILE, PrintColored: printColored,
Temperature: 0.01, ColorPurple: colorPurple,
} ColorGreen: colorGreen,
ColorRed: colorRed,
if h == CMD_UPDATE { ColorYellow: colorYellow,
gpt3.UpdateKey() GetCommand: getCommand,
return }
} cmdPackage.ShowDetailedExplanation(response, gpt3, system, cmd, timeout, level, deps)
default:
if h == CMD_DELETE { fmt.Println(" До свидания!")
gpt3.DeleteKey() if !disableHistory {
return if fromHistory {
} cmdPackage.SaveToHistoryFromHistory(config.AppConfig.ResultHistory, config.AppConfig.ResultFolder, cmd, response, gpt3.Prompt, explanation)
} else {
c := "R" cmdPackage.SaveToHistory(config.AppConfig.ResultHistory, config.AppConfig.ResultFolder, cmd, response, gpt3.Prompt)
r := "" }
elapsed := 0.0
for c == "R" || c == "r" {
r, elapsed = getCommand(gpt3, cmd)
c = "N"
fmt.Printf("Completed in %v seconds\n\n", elapsed)
fmt.Println(r)
fmt.Print("\nDo you want to (c)opy, (s)ave to file, (r)egenerate, or take (N)o action on the command? (c/r/N): ")
fmt.Scanln(&c)
// no action
if c == "N" || c == "n" {
return
} }
}
if r == "" {
return
}
// Copy to clipboard
if c == "C" || c == "c" {
clipboard.WriteAll(r)
fmt.Println("\033[33mCopied to clipboard")
return
}
if c == "S" || c == "s" {
timestamp := time.Now().Format("2006-01-02_15-04-05") // Format: YYYY-MM-DD_HH-MM-SS
filename := fmt.Sprintf("gpt_request_%s(%s).md", timestamp, gpt3.Model)
filePath := path.Join(RESULT_FOLDER, filename)
resultString := fmt.Sprintf("## Prompt:\n\n%s\n\n------------------\n\n## Response:\n\n%s\n\n", cmd+". "+gpt3.Prompt, r)
os.WriteFile(filePath, []byte(resultString), 0644)
fmt.Println("\033[33mSaved to file")
return
} }
} }
// moved to response.go
// saveExplanation сохраняет подробное объяснение и альтернативные способы
// moved to explain.go
// truncateTitle сокращает строку до 120 символов (по рунам), добавляя " ..." при усечении
// moved to response.go
// moved to explain.go
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("❌ Выполнение отменено")
}
}
// env helpers moved to config package
// moved to history.go
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(" • Используйте --prompt-id для выбора предустановленного промпта")
fmt.Println(" • Используйте --timeout для установки таймаута запроса")
fmt.Println(" • Укажите --no-history чтобы не записывать историю (аналог LCG_NO_HISTORY)")
fmt.Println(" • Команда 'prompts list' покажет все доступные промпты")
fmt.Println(" • Команда 'history list' покажет историю запросов")
fmt.Println(" • Команда 'config' покажет текущие настройки")
fmt.Println(" • Команда 'health' проверит доступность API")
fmt.Println(" • Команда 'serve-result' запустит HTTP сервер для просмотра результатов")
}
// printDebugInfo выводит отладочную информацию о параметрах запроса
func printDebugInfo(file, system, commandInput string, timeout int) {
printColored("\n🔍 DEBUG ИНФОРМАЦИЯ:\n", colorCyan)
fmt.Printf("📁 Файл: %s\n", file)
fmt.Printf("🤖 Системный промпт: %s\n", system)
fmt.Printf("💬 Запрос: %s\n", commandInput)
fmt.Printf("⏱️ Таймаут: %d сек\n", timeout)
fmt.Printf("🌐 Провайдер: %s\n", config.AppConfig.ProviderType)
fmt.Printf("🏠 Хост: %s\n", config.AppConfig.Host)
fmt.Printf("🧠 Модель: %s\n", config.AppConfig.Model)
fmt.Printf("📝 История: %t\n", !config.AppConfig.MainFlags.NoHistory)
printColored("────────────────────────────────────────\n", colorCyan)
}

View File

@@ -1,33 +1 @@
package main package main
import (
"testing"
)
func TestHandleCommand(t *testing.T) {
tests := []struct {
command string
expected int
}{
{"", CMD_HELP},
{"--help", CMD_HELP},
{"-h", CMD_HELP},
{"--version", CMD_VERSION},
{"-v", CMD_VERSION},
{"--update-key", CMD_UPDATE},
{"-u", CMD_UPDATE},
{"--delete-key", CMD_DELETE},
{"-d", CMD_DELETE},
{"random strings", CMD_COMPLETION},
{"--test", CMD_COMPLETION},
{"-test", CMD_COMPLETION},
{"how to extract test.tar.gz", CMD_COMPLETION},
}
for _, test := range tests {
result := handleCommand(test.command)
if result != test.expected {
t.Error("Expected", test.expected, "got", result)
}
}
}

59
response.go Normal file
View File

@@ -0,0 +1,59 @@
package main
import (
"fmt"
"os"
"path"
"strings"
"time"
"github.com/direct-dev-ru/linux-command-gpt/config"
)
func nowTimestamp() string {
return time.Now().Format("2006-01-02_15-04-05")
}
func pathJoin(base, name string) string {
return path.Join(base, name)
}
func writeFile(filePath, content string) {
if err := os.WriteFile(filePath, []byte(content), 0644); err != nil {
fmt.Println("Failed to save response:", err)
} else {
fmt.Printf("Saved to %s\n", filePath)
}
}
func saveResponse(response string, gpt3Model string, prompt string, cmd string, explanation ...string) {
timestamp := nowTimestamp()
filename := fmt.Sprintf("gpt_request_%s_%s.md", gpt3Model, timestamp)
filePath := pathJoin(config.AppConfig.ResultFolder, filename)
title := truncateTitle(cmd)
var content string
if len(explanation) > 0 && strings.TrimSpace(explanation[0]) != "" {
// Если есть объяснение, сохраняем полную структуру
content = fmt.Sprintf("# %s\n\n## Prompt\n\n%s\n\n## Response\n\n%s\n\n## Explanation\n\n%s\n",
title, cmd+". "+prompt, response, explanation[0])
} else {
// Если объяснения нет, сохраняем базовую структуру
content = fmt.Sprintf("# %s\n\n## Prompt\n\n%s\n\n## Response\n\n%s\n",
title, cmd+". "+prompt, response)
}
writeFile(filePath, content)
}
func truncateTitle(s string) string {
const maxLen = 120
if runeCount := len([]rune(s)); runeCount <= maxLen {
return s
}
const head = 116
r := []rune(s)
if len(r) <= head {
return s
}
return string(r[:head]) + " ..."
}

View File

@@ -1,92 +1,134 @@
#!/bin/bash #!/bin/bash
# REPO=registry.direct-dev.ru/go-lcg # Включаем строгий режим для лучшей отладки
REPO=kuznetcovay/go-lcg set -euo pipefail
VERSION=$1
if [ -z "$VERSION" ]; then
VERSION=v1.0.1
fi
BRANCH=main
echo ${VERSION} > VERSION.txt # Конфигурация
readonly REPO="kuznetcovay/go-lcg"
readonly BRANCH="main"
readonly BINARY_NAME="lcg"
export GOCACHE="${HOME}/.cache/go-build" # Получаем версию из аргумента или используем значение по умолчанию
VERSION="${1:-v2.0.0}"
# Save the current branch # Цвета для вывода
CURRENT_BRANCH=$(git branch --show-current) readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly NC='\033[0m' # No Color
# Function to restore the original branch # Функции для логирования
function restore_branch { log_info() {
echo "Restoring original branch: ${CURRENT_BRANCH}" echo -e "${GREEN}[INFO]${NC} $1"
git checkout "${CURRENT_BRANCH}"
} }
# Check if the current branch is different from the target branch log_warn() {
if [ "$CURRENT_BRANCH" != "$BRANCH" ]; then echo -e "${YELLOW}[WARN]${NC} $1"
# Set a trap to restore the branch on exit }
trap restore_branch EXIT
echo "Switching to branch: ${BRANCH}"
git checkout ${BRANCH}
fi
# Fetch all tags from the remote repository log_error() {
git fetch --tags echo -e "${RED}[ERROR]${NC} $1"
}
# Check if the specified version tag exists # Функция для обработки ошибок
if git rev-parse "refs/tags/${VERSION}" >/dev/null 2>&1; then handle_error() {
echo "Tag ${VERSION} already exists. Halting script." local exit_code=$?
log_error "Скрипт завершился с ошибкой (код: $exit_code)"
exit $exit_code
}
# Функция для восстановления ветки
restore_branch() {
if [[ -n "${CURRENT_BRANCH:-}" ]]; then
log_info "Восстанавливаем исходную ветку: ${CURRENT_BRANCH}"
git checkout "${CURRENT_BRANCH}" || log_warn "Не удалось переключиться на ${CURRENT_BRANCH}"
fi
}
# Функция для сборки бинарного файла
build_binary() {
local platform=$1
local output_dir="bin-linux-${platform}"
local dockerfile="Dockerfiles/LocalCompile/Dockerfile"
log_info "Собираем для ${platform}..."
if docker build -f "$dockerfile" --target bin-linux --output "$output_dir/" --platform "linux/${platform}" .; then
cp "$output_dir/$BINARY_NAME" "binaries-for-upload/$BINARY_NAME.${platform}.${VERSION}"
log_info "Сборка для ${platform} завершена успешно"
else
log_error "Сборка для ${platform} не удалась"
return 1
fi
}
# Функция для git операций
git_operations() {
log_info "Выполняем git операции..."
git add -A . || { log_error "git add не удался"; return 1; }
git commit -m "release $VERSION" || { log_error "git commit не удался"; return 1; }
git tag -a "$VERSION" -m "release $VERSION" || { log_error "git tag не удался"; return 1; }
git push -u origin main --tags || { log_error "git push не удался"; return 1; }
log_info "Git операции завершены успешно"
}
# Основная функция
main() {
log_info "Начинаем сборку версии: $VERSION"
# Записываем версию в файл
echo "$VERSION" > VERSION.txt
# Настраиваем кэш Go
export GOCACHE="${HOME}/.cache/go-build"
# Сохраняем текущую ветку
CURRENT_BRANCH=$(git branch --show-current)
# Настраиваем обработчик ошибок
trap handle_error ERR
trap restore_branch EXIT
# Переключаемся на нужную ветку если необходимо
if [[ "$CURRENT_BRANCH" != "$BRANCH" ]]; then
log_info "Переключаемся на ветку: $BRANCH"
git checkout "$BRANCH"
fi
# Получаем теги
log_info "Получаем теги из удаленного репозитория..."
git fetch --tags
# Проверяем существование тега
if git rev-parse "refs/tags/${VERSION}" >/dev/null 2>&1; then
log_error "Тег ${VERSION} уже существует. Прерываем выполнение."
exit 1 exit 1
fi fi
# Создаем директорию для бинарных файлов
mkdir -p binaries-for-upload
# Собираем бинарные файлы для обеих платформ
build_binary "amd64"
build_binary "arm64"
# Собираем и пушим Docker образы
log_info "Собираем и пушим multi-platform Docker образы..."
if docker buildx build -f Dockerfiles/ImageBuild/Dockerfile --push --platform linux/amd64,linux/arm64 -t "${REPO}:${VERSION}" .; then
log_info "Docker образы успешно собраны и запушены"
else
log_error "Сборка Docker образов не удалась"
exit 1
fi
# Выполняем git операции
git_operations
log_info "Сборка версии $VERSION завершена успешно!"
}
# Run go tests # Запускаем основную функцию
# if ! go test -v -run=^Test; then main "$@"
# echo "Tests failed. Exiting..."
# exit 1
# fi
mkdir binaries-for-upload
# Build for linux/amd64
docker build -f Dockerfiles/LocalCompile/Dockerfile --target bin-linux --output bin-linux-amd64/ --platform linux/amd64 . ||
{
echo "docker build for amd64 failed. Exiting with code 1."
exit 1
}
cp bin-linux-amd64/lcg "binaries-for-upload/lcg.amd64.${VERSION}"
# Build for linux/arm64
docker build -f Dockerfiles/LocalCompile/Dockerfile --target bin-linux --output bin-linux-arm64/ --platform linux/arm64 . ||
{
echo "docker build for arm64 failed. Exiting with code 1."
exit 1
}
cp bin-linux-arm64/lcg "binaries-for-upload/lcg.arm64.${VERSION}"
# Push multi-platform images
docker buildx build -f Dockerfiles/ImageBuild/Dockerfile --push --platform linux/amd64,linux/arm64 -t ${REPO}:"${VERSION}" . ||
{
echo "docker buildx build --push failed. Exiting with code 1."
exit 1
}
git add -A . ||
{
echo "git add failed. Exiting with code 1."
exit 1
}
git commit -m "release $VERSION" ||
{
echo "git commit failed. Exiting with code 1."
exit 1
}
git tag -a "$VERSION" -m "release $VERSION" ||
{
echo "git tag failed. Exiting with code 1."
exit 1
}
git push -u origin main --tags ||
{
echo "git push failed. Exiting with code 1."
exit 1
}

View File

@@ -4,5 +4,5 @@ docker build -f Dockerfiles/LocalCompile/Dockerfile --target bin-linux --output
docker build -f Dockerfiles/LocalCompile/Dockerfile --target bin-linux --output bin-linux-arm64/ --platform linux/arm64 . docker build -f Dockerfiles/LocalCompile/Dockerfile --target bin-linux --output bin-linux-arm64/ --platform linux/arm64 .
# in linux setuid # in linux setuid
# sudo chown root:root bin-linux/go-ansible-vault # sudo chown root:root bin-linux/lcg
# sudo chmod +s bin-linux/go-ansible-vault # sudo chmod +s bin-linux/lcg

View File

@@ -1,13 +1,13 @@
#!/bin/bash #!/bin/bash
REPO=kuznetcovay/go-ansible-vault REPO=kuznetcovay/go-lcg
VERSION=$1 VERSION=$1
if [ -z "$VERSION" ]; then if [ -z "$VERSION" ]; then
VERSION=v1.0.8 VERSION=v1.0.8
fi fi
BRANCH=main BRANCH=main
echo ${VERSION} > VERSION.txt echo "${VERSION}" > VERSION.txt
export GOCACHE="${HOME}/.cache/go-build" export GOCACHE="${HOME}/.cache/go-build"
# Save the current branch # Save the current branch
@@ -34,7 +34,7 @@ if ! go test -v -run=^Test; then
fi fi
# Push multi-platform images # Push multi-platform images
docker buildx build --push --platform linux/amd64,linux/arm64 -t ${REPO}:${VERSION} . || docker buildx build --push --platform linux/amd64,linux/arm64 -t ${REPO}:"${VERSION}" . ||
{ {
echo "docker buildx build --push failed. Exiting with code 1." echo "docker buildx build --push failed. Exiting with code 1."
exit 1 exit 1

View File

@@ -0,0 +1,5 @@
#!/bin/bash
go-ansible-vault -i shell-code/build.env -a get -m GITHUB_TOKEN > /tmp/source && source /tmp/source
#GITHUB_TOKEN=$GITHUB_TOKEN python3 shell-code/release.py

203
shell-code/release.py Normal file
View File

@@ -0,0 +1,203 @@
#!/usr/bin/env python3
"""
Скрипт для создания релиза на GitHub
Использование: GITHUB_TOKEN=your_token python3 release.py
"""
import os
import sys
import json
import requests
from pathlib import Path
# Цвета для вывода
class Colors:
RED = '\033[0;31m'
GREEN = '\033[0;32m'
YELLOW = '\033[1;33m'
BLUE = '\033[0;34m'
NC = '\033[0m' # No Color
def log(message):
print(f"{Colors.GREEN}[INFO]{Colors.NC} {message}")
def error(message):
print(f"{Colors.RED}[ERROR]{Colors.NC} {message}", file=sys.stderr)
def warn(message):
print(f"{Colors.YELLOW}[WARN]{Colors.NC} {message}")
def debug(message):
print(f"{Colors.BLUE}[DEBUG]{Colors.NC} {message}")
# Конфигурация
REPO = "direct-dev-ru/go-lcg"
VERSION_FILE = "VERSION.txt"
BINARIES_DIR = "binaries-for-upload"
def check_environment():
"""Проверка переменных окружения"""
token = os.getenv('GITHUB_TOKEN')
if not token:
error("GITHUB_TOKEN не установлен")
sys.exit(1)
log(f"GITHUB_TOKEN установлен (длина: {len(token)} символов)")
return token
def get_version():
"""Получение версии из файла"""
version_file = Path(VERSION_FILE)
if not version_file.exists():
error(f"Файл {VERSION_FILE} не найден")
sys.exit(1)
version = version_file.read_text().strip()
tag = f"lcg.{version}"
log(f"Версия: {version}")
log(f"Тег: {tag}")
return tag
def check_files():
"""Проверка файлов для загрузки"""
binaries_path = Path(BINARIES_DIR)
if not binaries_path.exists():
error(f"Директория {BINARIES_DIR} не найдена")
sys.exit(1)
files = list(binaries_path.glob("*"))
files = [f for f in files if f.is_file()]
if not files:
error(f"В директории {BINARIES_DIR} нет файлов")
sys.exit(1)
log(f"Найдено файлов: {len(files)}")
for file in files:
log(f" - {file.name} ({file.stat().st_size} байт)")
return files
def create_github_session(token):
"""Создание сессии для GitHub API"""
session = requests.Session()
session.headers.update({
'Authorization': f'token {token}',
'Accept': 'application/vnd.github.v3+json',
'User-Agent': 'release-script'
})
return session
def check_existing_release(session, tag):
"""Проверка существующего релиза"""
log("Проверяем существующий релиз...")
url = f"https://api.github.com/repos/{REPO}/releases/tags/{tag}"
response = session.get(url)
if response.status_code == 200:
release_data = response.json()
log(f"Реліз {tag} уже существует")
return release_data
elif response.status_code == 404:
log(f"Реліз {tag} не найден, создаем новый")
return None
else:
error(f"Ошибка проверки релиза: {response.status_code}")
debug(f"Ответ: {response.text}")
sys.exit(1)
def create_release(session, tag):
"""Создание нового релиза"""
log(f"Создаем новый релиз {tag}...")
data = {
"tag_name": tag,
"name": tag,
"body": f"Release {tag}",
"draft": False,
"prerelease": False
}
url = f"https://api.github.com/repos/{REPO}/releases"
response = session.post(url, json=data)
if response.status_code == 201:
release_data = response.json()
log("Реліз создан успешно")
return release_data
else:
error(f"Ошибка создания релиза: {response.status_code}")
debug(f"Ответ: {response.text}")
sys.exit(1)
def upload_file(session, upload_url, file_path):
"""Загрузка файла в релиз"""
filename = file_path.name
log(f"Загружаем: {filename}")
# Убираем {?name,label} из URL
upload_url = upload_url.replace("{?name,label}", "")
with open(file_path, 'rb') as f:
headers = {'Content-Type': 'application/octet-stream'}
params = {'name': filename}
response = session.post(
upload_url,
data=f,
headers=headers,
params=params
)
if response.status_code == 201:
log(f"{filename} загружен")
return True
else:
error(f"Ошибка загрузки {filename}: {response.status_code}")
debug(f"Ответ: {response.text}")
return False
def main():
"""Основная функция"""
log("=== НАЧАЛО РАБОТЫ СКРИПТА ===")
# Проверки
token = check_environment()
tag = get_version()
files = check_files()
# Создание сессии
session = create_github_session(token)
# Проверка/создание релиза
release = check_existing_release(session, tag)
if not release:
release = create_release(session, tag)
# Получение URL для загрузки
upload_url = release['upload_url']
log(f"Upload URL: {upload_url}")
# Загрузка файлов
log("=== ЗАГРУЗКА ФАЙЛОВ ===")
uploaded = 0
failed = 0
for file_path in files:
if upload_file(session, upload_url, file_path):
uploaded += 1
else:
failed += 1
# Результат
log("=== РЕЗУЛЬТАТ ===")
log(f"Успешно загружено: {uploaded}")
if failed > 0:
warn(f"Ошибок: {failed}")
else:
log("Все файлы загружены успешно!")
log(f"Реліз доступен: https://github.com/{REPO}/releases/tag/{tag}")
log("=== СКРИПТ ЗАВЕРШЕН ===")
if __name__ == "__main__":
main()

134
shell-code/release.sh Normal file
View File

@@ -0,0 +1,134 @@
#!/bin/bash
# Простой скрипт для создания релиза на GitHub
# Использование: GITHUB_TOKEN=your_token ./release.sh
set -e
# Цвета
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# Функции логирования
log() { echo -e "${GREEN}[INFO]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1" >&2; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
debug() { echo -e "${BLUE}[DEBUG]${NC} $1"; }
# Конфигурация
REPO="direct-dev-ru/go-lcg"
VERSION_FILE="VERSION.txt"
BINARIES_DIR="binaries-for-upload"
# Проверки
if [[ -z "$GITHUB_TOKEN" ]]; then
error "GITHUB_TOKEN не установлен"
exit 1
fi
if [[ ! -f "$VERSION_FILE" ]]; then
error "Файл $VERSION_FILE не найден"
exit 1
fi
if [[ ! -d "$BINARIES_DIR" ]]; then
error "Директория $BINARIES_DIR не найдена"
exit 1
fi
# Получение версии
VERSION=$(cat "$VERSION_FILE" | tr -d ' \t\n\r')
TAG="lcg.$VERSION"
log "Версия: $VERSION"
log "Тег: $TAG"
# Проверяем, существует ли уже релиз
log "Проверяем существующий релиз..."
EXISTING_RELEASE=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/repos/$REPO/releases/tags/$TAG")
if echo "$EXISTING_RELEASE" | grep -q '"id":'; then
log "Реліз $TAG уже существует, получаем upload_url..."
UPLOAD_URL=$(echo "$EXISTING_RELEASE" | grep '"upload_url"' | cut -d'"' -f4 | sed 's/{?name,label}//')
else
log "Создаем новый релиз $TAG..."
# Создаем релиз
RELEASE_DATA="{\"tag_name\":\"$TAG\",\"name\":\"$TAG\",\"body\":\"Release $TAG\"}"
RELEASE_RESPONSE=$(curl -s -X POST \
-H "Authorization: token $GITHUB_TOKEN" \
-H "Content-Type: application/json" \
"https://api.github.com/repos/$REPO/releases" \
-d "$RELEASE_DATA")
if echo "$RELEASE_RESPONSE" | grep -q '"message"'; then
error "Ошибка создания релиза:"
echo "$RELEASE_RESPONSE" | grep '"message"' | cut -d'"' -f4
exit 1
fi
UPLOAD_URL=$(echo "$RELEASE_RESPONSE" | grep '"upload_url"' | cut -d'"' -f4 | sed 's/{?name,label}//')
log "Реліз создан успешно"
fi
if [[ -z "$UPLOAD_URL" ]]; then
error "Не удалось получить upload_url"
exit 1
fi
log "Upload URL: $UPLOAD_URL"
# Проверяем файлы в директории
log "Проверяем файлы в директории $BINARIES_DIR:"
ls -la "$BINARIES_DIR"
# Загружаем файлы
log "Загружаем файлы..."
UPLOADED=0
FAILED=0
# Простой цикл по всем файлам в директории
for file in "$BINARIES_DIR"/*; do
if [[ -f "$file" ]]; then
filename=$(basename "$file")
log "Обрабатываем файл: $file"
debug "Имя файла: $filename"
log "Загружаем: $filename"
response=$(curl -s -X POST \
-H "Authorization: token $GITHUB_TOKEN" \
-H "Content-Type: application/octet-stream" \
"$UPLOAD_URL?name=$filename" \
--data-binary @"$file")
debug "Ответ API: $response"
if echo "$response" | grep -q '"message"'; then
error "Ошибка загрузки $filename:"
echo "$response" | grep '"message"' | cut -d'"' -f4
((FAILED++))
else
log "$filename загружен"
((UPLOADED++))
fi
else
warn "Пропускаем не-файл: $file"
fi
done
# Результат
log "=== РЕЗУЛЬТАТ ==="
log "Успешно загружено: $UPLOADED"
if [[ $FAILED -gt 0 ]]; then
warn "Ошибок: $FAILED"
else
log "Все файлы загружены успешно!"
fi
log "Реліз доступен: https://github.com/$REPO/releases/tag/$TAG"

View File

@@ -0,0 +1,6 @@
#!/usr/bin/bash
# shellcheck disable=SC2034
LCG_PROVIDER=proxy LCG_HOST=http://localhost:8080 LCG_MODEL=GigaChat-2-Max LCG_JWT_TOKEN=$(go-ansible-vault -a -i shell-code/jwt.admin.token get -m 'JWT_TOKEN' -q) go run . $1 $2 $3 $4 $5 $6 $7 $8 $9
LCG_PROVIDER=proxy LCG_HOST=https://direct-dev.ru LCG_MODEL=GigaChat-2-Max LCG_JWT_TOKEN=$(go-ansible-vault --key $(cat ~/.config/gak) -i ~/.config/jwt.direct-dev.ru get -m 'JWT_TOKEN' -q) go run . [your question here]

6
shell-code/run_ollama.sh Normal file
View 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

81
shell-code/test_api.py Normal file
View File

@@ -0,0 +1,81 @@
#!/usr/bin/env python3
"""
Скрипт для тестирования GitHub API
Использование: GITHUB_TOKEN=your_token python3 test_api.py
"""
import os
import sys
import requests
# Цвета
class Colors:
RED = '\033[0;31m'
GREEN = '\033[0;32m'
YELLOW = '\033[1;33m'
NC = '\033[0m'
def log(message):
print(f"{Colors.GREEN}[INFO]{Colors.NC} {message}")
def error(message):
print(f"{Colors.RED}[ERROR]{Colors.NC} {message}")
def main():
REPO = "direct-dev-ru/go-lcg"
token = os.getenv('GITHUB_TOKEN')
if not token:
error("GITHUB_TOKEN не установлен")
sys.exit(1)
session = requests.Session()
session.headers.update({
'Authorization': f'token {token}',
'Accept': 'application/vnd.github.v3+json'
})
print("=== ТЕСТИРОВАНИЕ GITHUB API ===")
# Тест 1: Проверка доступа к репозиторию
print("1. Проверка доступа к репозиторию...")
response = session.get(f"https://api.github.com/repos/{REPO}")
if response.status_code == 200:
repo_data = response.json()
print(f"✅ Доступ к репозиторию есть")
print(f" Репозиторий: {repo_data['full_name']}")
print(f" Описание: {repo_data.get('description', 'Нет описания')}")
else:
print(f"❌ Ошибка доступа: {response.status_code}")
print(f" Ответ: {response.text}")
# Тест 2: Проверка прав
print("\n2. Проверка прав...")
if response.status_code == 200:
permissions = repo_data.get('permissions', {})
if permissions.get('admin'):
print("✅ Есть права администратора")
elif permissions.get('push'):
print("✅ Есть права на запись")
else:
print("❌ Недостаточно прав для создания релизов")
# Тест 3: Последние релизы
print("\n3. Последние релизы:")
releases_response = session.get(f"https://api.github.com/repos/{REPO}/releases")
if releases_response.status_code == 200:
releases = releases_response.json()
if releases:
for release in releases[:5]:
print(f" - {release['tag_name']} ({release['name']})")
else:
print(" Релизов пока нет")
else:
print(f" Ошибка получения релизов: {releases_response.status_code}")
print("\n=== ТЕСТ ЗАВЕРШЕН ===")
if __name__ == "__main__":
main()

View File

@@ -1,63 +0,0 @@
#!/bin/bash
# Variables
VERSION_FILE="VERSION.txt"
GITHUB_TOKEN="${GITHUB_TOKEN}" # Replace with your GitHub token
REPO="Direct-Dev-Ru/binaries.git" # Replace with your GitHub username/repo
TAG=go-ansible-vault.$(cat "$VERSION_FILE")
echo $TAG
RELEASE_NAME="Binaries ${TAG}" # Replace with your release title
RELEASE_DIR="/home/su/projects/golang/ansible-vault/binaries-for-upload"
# Create a new release
# response=$(curl -s -X POST \
# -H "Authorization: Bearer ${GITHUB_TOKEN}" \
# -H "Accept: application/vnd.github+json" \
# -H "X-GitHub-Api-Version: 2022-11-28" \
# https://api.github.com/repos/$REPO/releases \
# -d "{\"tag_name\": \"$TAG\", \"name\": \"$RELEASE_NAME\"}")
body="{\"tag_name\":\"${TAG}\", \"target_commitish\":\"main\", \"name\":\"${TAG}\", \
\"body\":\"${TAG}\", \"draft\":false, \"prerelease\":false, \"generate_release_notes\":false}"
echo $body
response=$(curl -L -X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
https://api.github.com/repos/direct-dev-ru/binaries/releases \
-d $body)
echo $response
# Extract the upload URL from the response
upload_url=$(echo "$response" | jq -r '.upload_url' | sed "s/{?name,label}//")
# Check if the release was created successfully
if [[ "$response" == *"Not Found"* ]]; then
echo "Error: Repository not found or invalid token."
exit 1
fi
# Upload each binary file
for file in "$RELEASE_DIR"/*; do
if [[ -f "$file" ]]; then
filename=$(basename "$file")
echo "Uploading $filename..."
response=$(curl -s -X POST -H "Authorization: token $GITHUB_TOKEN" \
-H "Content-Type: application/octet-stream" \
"$upload_url?name=$filename" \
--data-binary @"$file")
echo $response
fi
done
echo "All binaries uploaded successfully."