mirror of
https://github.com/Direct-Dev-Ru/go-lcg.git
synced 2025-11-16 09:39:56 +00:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3e1cb1e078 | |||
| e37599d3ef | |||
| 344f763bb4 | |||
| d7a53c218e | |||
| 58c2934924 | |||
| 47671eb566 | |||
| 04d785db77 | |||
| c57d981804 | |||
| 01f8adc979 | |||
| e99fe76bef | |||
| cc242e1192 | |||
| 3e1c4594b1 | |||
| ec2486ce3d | |||
| 46a0d9e45a | |||
| 12cd3fe6db | |||
| 7136fe4607 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -13,3 +13,7 @@ shell-code/build.env
|
||||
bin-linux-amd64/*
|
||||
bin-linux-arm64/*
|
||||
binaries-for-upload/*
|
||||
gpt_results
|
||||
shell-code/jwt.admin.token
|
||||
run.sh
|
||||
lcg_history.json
|
||||
|
||||
@@ -1,33 +1,53 @@
|
||||
archives:
|
||||
- format: tar.gz
|
||||
|
||||
builds:
|
||||
- binary: lcg
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
- arm
|
||||
goos:
|
||||
- linux
|
||||
- darwin
|
||||
|
||||
changelog:
|
||||
filters:
|
||||
exclude:
|
||||
- '^docs:'
|
||||
- '^test:'
|
||||
sort: asc
|
||||
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
|
||||
release:
|
||||
draft: true
|
||||
|
||||
snapshot:
|
||||
name_template: "{{ incpatch .Version }}-next"
|
||||
# This is an example .goreleaser.yml file with some sensible defaults.
|
||||
# Make sure to check the documentation at https://goreleaser.com
|
||||
|
||||
# The lines below are called `modelines`. See `:help modeline`
|
||||
# Feel free to remove those if you don't want/need to use them.
|
||||
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
|
||||
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
|
||||
|
||||
version: 2
|
||||
|
||||
before:
|
||||
hooks:
|
||||
# You may remove this if you don't use go modules.
|
||||
- go mod tidy
|
||||
# you may remove this if you don't need go generate
|
||||
- go generate ./...
|
||||
|
||||
builds:
|
||||
- env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- linux
|
||||
- windows
|
||||
- darwin
|
||||
|
||||
archives:
|
||||
- formats: [tar.gz]
|
||||
# this name template makes the OS and Arch compatible with the results of `uname`.
|
||||
name_template: >-
|
||||
{{ .ProjectName }}_
|
||||
{{- title .Os }}_
|
||||
{{- if eq .Arch "amd64" }}x86_64
|
||||
{{- else if eq .Arch "386" }}i386
|
||||
{{- else }}{{ .Arch }}{{ end }}
|
||||
{{- if .Arm }}v{{ .Arm }}{{ end }}
|
||||
# use zip for windows archives
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
formats: [zip]
|
||||
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- "^docs:"
|
||||
- "^test:"
|
||||
|
||||
release:
|
||||
footer: >-
|
||||
|
||||
---
|
||||
|
||||
Released by [GoReleaser](https://github.com/goreleaser/goreleaser).
|
||||
|
||||
33
.goreleaser.yaml.old
Normal file
33
.goreleaser.yaml.old
Normal file
@@ -0,0 +1,33 @@
|
||||
archives:
|
||||
- format: tar.gz
|
||||
|
||||
builds:
|
||||
- binary: lcg
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
- arm
|
||||
goos:
|
||||
- linux
|
||||
- darwin
|
||||
|
||||
changelog:
|
||||
filters:
|
||||
exclude:
|
||||
- '^docs:'
|
||||
- '^test:'
|
||||
sort: asc
|
||||
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
|
||||
release:
|
||||
draft: true
|
||||
|
||||
snapshot:
|
||||
name_template: "{{ incpatch .Version }}-next"
|
||||
|
||||
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
|
||||
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
|
||||
187
API_CONTRACT.md
Normal file
187
API_CONTRACT.md
Normal file
@@ -0,0 +1,187 @@
|
||||
# Контракт 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) нарушит совместимость.
|
||||
|
||||
---
|
||||
|
||||
Дополнительно: для HTTP API веб‑сервера (эндпоинт `POST /execute`, только `curl`) см. `API_GUIDE.md` с примерами и подробной схемой запроса/ответа.
|
||||
259
API_GUIDE.md
Normal file
259
API_GUIDE.md
Normal file
@@ -0,0 +1,259 @@
|
||||
# API Guide - Linux Command GPT
|
||||
|
||||
## Обзор
|
||||
|
||||
API позволяет выполнять запросы к Linux Command GPT через HTTP POST запросы с помощью curl. API принимает только запросы от curl (проверка User-Agent).
|
||||
|
||||
## Endpoint
|
||||
|
||||
``` curl
|
||||
POST /execute
|
||||
```
|
||||
|
||||
## Запуск сервера
|
||||
|
||||
```bash
|
||||
# Запуск сервера
|
||||
lcg serve
|
||||
|
||||
# Запуск на другом порту
|
||||
lcg serve --port 9000
|
||||
|
||||
# Запуск с автоматическим открытием браузера
|
||||
lcg serve --browser
|
||||
```
|
||||
|
||||
## Структура запроса
|
||||
|
||||
### JSON Payload
|
||||
|
||||
```json
|
||||
{
|
||||
"prompt": "создать директорию test",
|
||||
"system_id": 1,
|
||||
"system": "альтернативный системный промпт",
|
||||
"verbose": "vv",
|
||||
"timeout": 120
|
||||
}
|
||||
```
|
||||
|
||||
### Поля запроса
|
||||
|
||||
| Поле | Тип | Обязательное | Описание |
|
||||
|------|-----|--------------|----------|
|
||||
| `prompt` | string | ✅ | Пользовательский запрос |
|
||||
| `system_id` | int | ❌ | ID системного промпта (1-5) |
|
||||
| `system` | string | ❌ | Текст системного промпта (альтернатива system_id) |
|
||||
| `verbose` | string | ❌ | Степень подробности: "v", "vv", "vvv" |
|
||||
| `timeout` | int | ❌ | Таймаут в секундах (по умолчанию: 120) |
|
||||
|
||||
### Структура ответа
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"command": "mkdir test",
|
||||
"explanation": "Команда mkdir создает новую директорию...",
|
||||
"model": "hf.co/yandex/YandexGPT-5-Lite-8B-instruct-GGUF:Q4_K_M",
|
||||
"elapsed": 2.34
|
||||
}
|
||||
```
|
||||
|
||||
## Примеры использования
|
||||
|
||||
### 1. Базовый запрос
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/execute \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"prompt": "создать директорию test"
|
||||
}'
|
||||
```
|
||||
|
||||
**Ответ:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"command": "mkdir test",
|
||||
"model": "hf.co/yandex/YandexGPT-5-Lite-8B-instruct-GGUF:Q4_K_M",
|
||||
"elapsed": 1.23
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Запрос с системным промптом по ID
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/execute \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"prompt": "найти все файлы .txt",
|
||||
"system_id": 2
|
||||
}'
|
||||
```
|
||||
|
||||
### 3. Запрос с кастомным системным промптом
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/execute \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"prompt": "показать использование памяти",
|
||||
"system": "Ты эксперт по Linux. Отвечай только командами без объяснений."
|
||||
}'
|
||||
```
|
||||
|
||||
### 4. Запрос с подробным объяснением
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/execute \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"prompt": "архивировать папку documents",
|
||||
"verbose": "vv",
|
||||
"timeout": 180
|
||||
}'
|
||||
```
|
||||
|
||||
**Ответ:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"command": "tar -czf documents.tar.gz documents/",
|
||||
"explanation": "Команда tar создает архив в формате gzip...",
|
||||
"model": "hf.co/yandex/YandexGPT-5-Lite-8B-instruct-GGUF:Q4_K_M",
|
||||
"elapsed": 3.45
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Запрос с максимальной подробностью
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/execute \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"prompt": "настроить SSH сервер",
|
||||
"system_id": 3,
|
||||
"verbose": "vvv",
|
||||
"timeout": 300
|
||||
}'
|
||||
```
|
||||
|
||||
## Системные промпты
|
||||
|
||||
| ID | Название | Описание |
|
||||
|----|----------|----------|
|
||||
| 1 | basic | Базовые команды Linux |
|
||||
| 2 | advanced | Продвинутые команды |
|
||||
| 3 | system | Системное администрирование |
|
||||
| 4 | network | Сетевые команды |
|
||||
| 5 | security | Безопасность |
|
||||
|
||||
## Степени подробности
|
||||
|
||||
| Уровень | Описание |
|
||||
|---------|----------|
|
||||
| `v` | Краткое объяснение |
|
||||
| `vv` | Подробное объяснение с альтернативами |
|
||||
| `vvv` | Максимально подробное объяснение с примерами |
|
||||
|
||||
## Обработка ошибок
|
||||
|
||||
### Ошибка валидации
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "Prompt is required"
|
||||
}
|
||||
```
|
||||
|
||||
### Ошибка AI
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "Failed to get response from AI"
|
||||
}
|
||||
```
|
||||
|
||||
### Ошибка доступа
|
||||
|
||||
``` text
|
||||
HTTP 403 Forbidden
|
||||
Only curl requests are allowed
|
||||
```
|
||||
|
||||
## Переменные окружения
|
||||
|
||||
Убедитесь, что настроены необходимые переменные:
|
||||
|
||||
```bash
|
||||
# Основные настройки
|
||||
export LCG_PROVIDER="ollama"
|
||||
export LCG_HOST="http://localhost:11434"
|
||||
export LCG_MODEL="hf.co/yandex/YandexGPT-5-Lite-8B-instruct-GGUF:Q4_K_M"
|
||||
|
||||
# Для proxy провайдера
|
||||
export LCG_PROVIDER="proxy"
|
||||
export LCG_HOST="https://your-proxy-server.com"
|
||||
export LCG_JWT_TOKEN="your-jwt-token"
|
||||
```
|
||||
|
||||
## Безопасность
|
||||
|
||||
- ✅ **Только curl**: API принимает только запросы от curl
|
||||
- ✅ **POST только**: Только POST запросы к `/execute`
|
||||
- ✅ **JSON валидация**: Строгая проверка входных данных
|
||||
- ✅ **Таймауты**: Ограничение времени выполнения запросов
|
||||
|
||||
## Примеры скриптов
|
||||
|
||||
### Bash скрипт для автоматизации
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
API_URL="http://localhost:8080/execute"
|
||||
|
||||
# Функция для выполнения запроса
|
||||
execute_command() {
|
||||
local prompt="$1"
|
||||
local verbose="${2:-}"
|
||||
|
||||
curl -s -X POST "$API_URL" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"prompt\": \"$prompt\", \"verbose\": \"$verbose\"}" | \
|
||||
jq -r '.command'
|
||||
}
|
||||
|
||||
# Использование
|
||||
echo "Команда: $(execute_command "создать директорию backup")"
|
||||
```
|
||||
|
||||
### Python скрипт
|
||||
|
||||
```python
|
||||
import requests
|
||||
import json
|
||||
|
||||
def execute_command(prompt, system_id=None, verbose=None):
|
||||
url = "http://localhost:8080/execute"
|
||||
payload = {"prompt": prompt}
|
||||
|
||||
if system_id:
|
||||
payload["system_id"] = system_id
|
||||
if verbose:
|
||||
payload["verbose"] = verbose
|
||||
|
||||
response = requests.post(url, json=payload)
|
||||
return response.json()
|
||||
|
||||
# Использование
|
||||
result = execute_command("показать использование диска", verbose="vv")
|
||||
print(f"Команда: {result['command']}")
|
||||
if 'explanation' in result:
|
||||
print(f"Объяснение: {result['explanation']}")
|
||||
```
|
||||
152
CHANGELOG.txt
Normal file
152
CHANGELOG.txt
Normal file
@@ -0,0 +1,152 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
Версия 2.0.1 (2025-10-22)
|
||||
=========================
|
||||
|
||||
## ✨ ИЗМЕНЕНИЯ И НОВОЕ
|
||||
|
||||
- 📱 Веб‑интерфейс: улучшена мобильная адаптация (кнопки, шрифты, отступы, верстка)
|
||||
- 🧭 Учет `prefers-reduced-motion` для снижения анимаций при необходимости
|
||||
- 🔌 Добавлен публичный REST эндпоинт `POST /execute` для программного доступа
|
||||
- ⚠️ Ограничение безопасности: принимаются только запросы от `curl` (User-Agent)
|
||||
- 📖 Подробности и примеры: см. `API_GUIDE.md`
|
||||
|
||||
## 🐛 ИСПРАВЛЕНИЯ И УЛУЧШЕНИЯ
|
||||
|
||||
- 🔧 Рефакторинг HTTP‑сервера: вынесен в пакет `serve/*` для лучшей поддерживаемости
|
||||
- 🗑️ Удалены устаревшие файлы: `cmd/serve.go`, `_main.go`
|
||||
- 🛠️ Скрипты релиза: небольшие правки в `shell-code/pre-release.sh` и `shell-code/release.py`
|
||||
|
||||
## 📚 ДОКУМЕНТАЦИЯ
|
||||
|
||||
- ➕ Добавлен `API_GUIDE.md` (описание `/execute`, примеры `curl`/Python)
|
||||
- 📝 Обновлены `README.md` и `USAGE_GUIDE.md` (актуализация про мобильную версию и API)
|
||||
- 🔗 В `API_CONTRACT.md` добавлена ссылка на `API_GUIDE.md` (HTTP API веб‑сервера)
|
||||
|
||||
## ♻️ СОВМЕСТИМОСТЬ
|
||||
|
||||
- ✅ Обратная совместимость сохранена; CLI и провайдеры (proxy/ollama) без изменений
|
||||
- 🚫 Миграция не требуется
|
||||
|
||||
Версия 2.0.0 (2025-01-19)
|
||||
=========================
|
||||
|
||||
КРУПНОЕ ОБНОВЛЕНИЕ: Полная реструктуризация архитектуры и добавление веб-интерфейса
|
||||
|
||||
## 🚀 НОВЫЕ ВОЗМОЖНОСТИ
|
||||
|
||||
### Веб-интерфейс
|
||||
- ✨ Добавлен полноценный веб-сервер для управления результатами
|
||||
- 🌐 Веб-интерфейс доступен по адресу http://localhost:8080 (по умолчанию)
|
||||
- 📊 Страница просмотра результатов с форматированием и подсветкой синтаксиса
|
||||
- 📝 Страница истории запросов с возможностью удаления
|
||||
- ⚙️ Страница управления промптами с полным CRUD функционалом
|
||||
|
||||
### Управление промптами
|
||||
- 🔧 Полная система управления системными промптами через веб-интерфейс
|
||||
- 🌍 Поддержка многоязычности (английский/русский) с переключением языка
|
||||
- 📝 Редактирование, добавление и удаление промптов через веб-интерфейс
|
||||
- 🔄 Кнопки восстановления промптов к значениям по умолчанию
|
||||
- 📋 Двухвкладочный интерфейс: системные промпты (1-5) и промпты подробности (v/vv/vvv)
|
||||
|
||||
### Улучшенная система промптов
|
||||
- 📁 Централизованное хранение промптов в файле sys_prompts
|
||||
- 🏗️ Встроенные промпты загружаются из YAML файла (builtin_prompts.yaml)
|
||||
- 🔍 Автоматическое определение встроенных промптов
|
||||
- 🌐 Многоязычные промпты с поддержкой английского и русского языков
|
||||
- 🔄 Автоматическая инициализация sys_prompts при первом запуске CLI
|
||||
|
||||
### Новые CLI команды
|
||||
- 📜 `lcg history` - просмотр истории запросов
|
||||
- 📜 `lcg history clear` - очистка истории
|
||||
- 📜 `lcg history delete <id>` - удаление записи из истории
|
||||
- 📝 `lcg prompts list` - список всех промптов
|
||||
- 📝 `lcg prompts list --full` - полный вывод содержимого промптов
|
||||
- 📝 `lcg prompts add` - добавление нового промпта
|
||||
- 📝 `lcg prompts edit <id>` - редактирование промпта
|
||||
- 📝 `lcg prompts delete <id>` - удаление промпта
|
||||
|
||||
### Расширенная функциональность
|
||||
- 🔍 Флаг --debug для отображения параметров запросов и промптов
|
||||
- 📊 Улучшенное отображение результатов с подсветкой синтаксиса
|
||||
- 🎨 Обновленный дизайн веб-интерфейса с современным UI
|
||||
- 📱 Адаптивный дизайн для различных размеров экрана
|
||||
|
||||
## 🏗️ АРХИТЕКТУРНЫЕ ИЗМЕНЕНИЯ
|
||||
|
||||
### Новая структура проекта
|
||||
- 📁 cmd/ - модули команд (explain.go, history.go, serve.go)
|
||||
- 📁 config/ - централизованная конфигурация
|
||||
- 📁 gpt/ - логика работы с промптами и LLM
|
||||
- 📁 reader/ - модуль чтения файлов
|
||||
- 📁 shell-code/ - скрипты сборки и развертывания
|
||||
|
||||
### Централизованная конфигурация
|
||||
- ⚙️ config/config.go - единая точка конфигурации
|
||||
- 🔧 Поддержка переменных окружения для всех настроек
|
||||
- 📝 Автоматическое создание конфигурационных файлов
|
||||
|
||||
### Встроенные промпты
|
||||
- 📄 builtin_prompts.yaml - YAML файл с встроенными промптами
|
||||
- 🔧 builtin_prompts.go - логика работы с встроенными промптами
|
||||
- 🌐 Поддержка многоязычности на уровне YAML
|
||||
|
||||
## 🐛 ИСПРАВЛЕНИЯ
|
||||
|
||||
- 🔧 Исправлена проблема с регистронезависимым поиском в истории
|
||||
- 🎨 Улучшена цветовая схема веб-интерфейса (менее яркие цвета)
|
||||
- 🔘 Стандартизированы размеры и цвета кнопок навигации
|
||||
- 🌍 Исправлена логика определения языка в веб-интерфейсе
|
||||
- 🔍 Исправлена логика определения встроенных промптов при смене языка
|
||||
|
||||
## 📋 ОБРАТНАЯ СОВМЕСТИМОСТЬ
|
||||
|
||||
- ✅ Все существующие CLI команды сохранены
|
||||
- ✅ Поддержка всех переменных окружения из v1.x.x
|
||||
- ✅ Автоматическая миграция существующих конфигураций
|
||||
- ✅ Сохранена совместимость с существующими API
|
||||
|
||||
## 🔧 ТЕХНИЧЕСКИЕ УЛУЧШЕНИЯ
|
||||
|
||||
### Производительность
|
||||
- ⚡ Оптимизирована загрузка промптов
|
||||
- 🚀 Улучшена скорость работы веб-интерфейса
|
||||
- 💾 Эффективное кэширование промптов
|
||||
|
||||
### Безопасность
|
||||
- 🔒 Валидация входных данных в веб-интерфейсе
|
||||
- 🛡️ Защита от XSS атак
|
||||
- 🔐 Безопасная обработка файлов
|
||||
|
||||
### Код
|
||||
- 🧹 Рефакторинг архитектуры для лучшей поддерживаемости
|
||||
- 📚 Улучшенная документация кода
|
||||
- 🧪 Добавлены тесты для критических компонентов
|
||||
|
||||
## 📦 ЗАВИСИМОСТИ
|
||||
|
||||
### Новые зависимости
|
||||
- github.com/urfave/cli/v2 - CLI фреймворк
|
||||
- gopkg.in/yaml.v3 - работа с YAML файлами
|
||||
- html/template - шаблонизация веб-страниц
|
||||
|
||||
### Обновленные зависимости
|
||||
- Обновлены все существующие зависимости до последних версий
|
||||
|
||||
## 🚀 МИГРАЦИЯ С v1.x.x
|
||||
|
||||
1. **Автоматическая миграция**: При первом запуске v2.0.0 автоматически создастся файл sys_prompts с встроенными промптами
|
||||
2. **Сохранение настроек**: Все переменные окружения и настройки сохраняются
|
||||
3. **Новые возможности**: Доступ к веб-интерфейсу через `lcg serve-result`
|
||||
4. **Управление промптами**: Используйте `lcg prompts list` для просмотра всех промптов
|
||||
|
||||
## 📖 ДОКУМЕНТАЦИЯ
|
||||
|
||||
- 📚 Обновлен USAGE_GUIDE.md с описанием новых команд
|
||||
- 📋 Добавлен API_CONTRACT.md для веб-интерфейса
|
||||
- 🔧 Обновлен README.md с инструкциями по использованию
|
||||
|
||||
---
|
||||
|
||||
**Примечание**: Версия 2.0.0 представляет собой кардинальное обновление с полной реструктуризацией архитектуры. Рекомендуется ознакомиться с новыми возможностями веб-интерфейса и системой управления промптами.
|
||||
@@ -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
|
||||
|
||||
RUN apk add git && go install mvdan.cc/garble@latest
|
||||
# RUN apk add git
|
||||
#&& go install mvdan.cc/garble@latest
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -11,8 +12,8 @@ COPY . .
|
||||
RUN echo $BUILDPLATFORM > buildplatform
|
||||
RUN echo $TARGETARCH > targetarch
|
||||
|
||||
# RUN GOOS=linux GOARCH=$TARGETARCH go build -o /app/go-lcg .
|
||||
RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} garble -literals -tiny build -ldflags="-w -s" -o /app/go-lcg .
|
||||
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 .
|
||||
|
||||
FROM alpine:latest
|
||||
|
||||
|
||||
@@ -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 TARGETARCH
|
||||
RUN apk add git && go install mvdan.cc/garble@latest
|
||||
# RUN apk add git
|
||||
#&& go install mvdan.cc/garble@latest
|
||||
WORKDIR /src
|
||||
ENV CGO_ENABLED=0
|
||||
COPY go.* .
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
|
||||
# RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -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} 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 .
|
||||
|
||||
FROM scratch AS bin-unix
|
||||
COPY --from=build /out/go-lcg /lcg
|
||||
|
||||
1
LICENSE
1
LICENSE
@@ -1,6 +1,7 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 asrul10
|
||||
Copyright (c) 2025 direct-dev.ru
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
197
README.md
197
README.md
@@ -1,72 +1,167 @@
|
||||
## Linux Command GPT (lcg)
|
||||
Get Linux commands in natural language with the power of ChatGPT.
|
||||
# Linux Command GPT (lcg)
|
||||
|
||||
### Installation
|
||||
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
|
||||
```
|
||||
This repo is forked from <https://github.com/asrul10/linux-command-gpt.git>
|
||||
|
||||
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
|
||||
> lcg I want to extract linux-command-gpt.tar.gz file
|
||||
Completed in 0.92 seconds
|
||||
git clone --depth 1 https://github.com/Direct-Dev-Ru/linux-command-gpt.git ~/.linux-command-gpt
|
||||
cd ~/.linux-command-gpt
|
||||
go build -o lcg
|
||||
|
||||
tar -xvzf linux-command-gpt.tar.gz
|
||||
|
||||
Do you want to (c)opy, (r)egenerate, or take (N)o action on the command? (c/r/N):
|
||||
# Add to your PATH
|
||||
ln -s ~/.linux-command-gpt/lcg ~/.local/bin
|
||||
```
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
> LCG_PROMPT='Provide full response' LCG_MODEL=codellama:13b lcg 'i need bash script
|
||||
to execute some command by ssh on some array of hosts'
|
||||
Completed in 181.16 seconds
|
||||
lcg "I want to extract linux-command-gpt.tar.gz file"
|
||||
```
|
||||
|
||||
After generation you will see a CAPS warning that the answer is from AI and must be verified, the command, and the action menu:
|
||||
|
||||
```text
|
||||
ACTIONS: (c)opy, (s)ave, (r)egenerate, (e)xecute, (v|vv|vvv)explain, (n)othing
|
||||
```
|
||||
|
||||
Explanations:
|
||||
|
||||
- `v` — short; `vv` — medium; `vvv` — detailed with alternatives.
|
||||
|
||||
Clipboard support requires `xclip` or `xsel`.
|
||||
|
||||
## What's new in 2.0.1
|
||||
|
||||
- Mobile UI improvements: better responsiveness (buttons, fonts, spacing) and reduced motion support
|
||||
- Public REST endpoint: `POST /execute` (curl-only) for programmatic access — see `API_GUIDE.md`
|
||||
|
||||
## Environment
|
||||
|
||||
- `LCG_PROVIDER` (default `ollama`) — provider type: `ollama` or `proxy`
|
||||
- `LCG_HOST` (default `http://192.168.87.108:11434/`) — base API URL
|
||||
- `LCG_MODEL` (default `hf.co/yandex/YandexGPT-5-Lite-8B-instruct-GGUF:Q4_K_M`)
|
||||
- `LCG_PROMPT` — default system prompt content
|
||||
- `LCG_PROXY_URL` (default `/api/v1/protected/sberchat/chat`) — proxy chat endpoint
|
||||
- `LCG_COMPLETIONS_PATH` (default `api/chat`) — Ollama chat endpoint (relative)
|
||||
- `LCG_TIMEOUT` (default `300`) — request timeout in seconds
|
||||
- `LCG_RESULT_FOLDER` (default `~/.config/lcg/gpt_results`) — folder for saved results
|
||||
- `LCG_RESULT_HISTORY` (default `$(LCG_RESULT_FOLDER)/lcg_history.json`) — JSON history path
|
||||
- `LCG_PROMPT_FOLDER` (default `~/.config/lcg/gpt_sys_prompts`) — folder for system prompts
|
||||
- `LCG_PROMPT_ID` (default `1`) — default system prompt ID
|
||||
- `LCG_BROWSER_PATH` — custom browser executable path for `--browser` flag
|
||||
- `LCG_JWT_TOKEN` — JWT token for proxy provider
|
||||
- `LCG_NO_HISTORY` — if `1`/`true`, disables history writes for the process
|
||||
- `LCG_ALLOW_EXECUTION` — if `1`/`true`, enables command execution via `(e)` action menu
|
||||
- `LCG_SERVER_PORT` (default `8080`), `LCG_SERVER_HOST` (default `localhost`) — HTTP server settings
|
||||
|
||||
## Flags
|
||||
|
||||
- `--file, -f` read part of prompt from file
|
||||
- `--sys, -s` system prompt content or ID
|
||||
- `--prompt-id, --pid` choose built-in prompt (1–5)
|
||||
- `--timeout, -t` request timeout (sec)
|
||||
- `--no-history, --nh` disable writing/updating JSON history for this run
|
||||
- `--debug, -d` show debug information (request parameters and prompts)
|
||||
- `--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)
|
||||
- `serve` — start HTTP server to browse saved results (`--port`, `--host`, `--browser`)
|
||||
- `/run` — web interface for executing requests
|
||||
- `/execute` — API endpoint for programmatic access via curl
|
||||
|
||||
## Saving results
|
||||
|
||||
Files are saved to `LCG_RESULT_FOLDER` (default `~/.config/lcg/gpt_results`).
|
||||
|
||||
- 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.
|
||||
|
||||
## Browser Integration
|
||||
|
||||
The `serve` command supports automatic browser opening:
|
||||
|
||||
Here is a sample Bash script that demonstrates how to execute commands over SSH on an array of hosts:
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Start server and open browser automatically
|
||||
lcg serve --browser
|
||||
|
||||
hosts=(host1 host2 host3)
|
||||
# Use custom browser
|
||||
export LCG_BROWSER_PATH="/usr/bin/firefox"
|
||||
lcg serve --browser
|
||||
|
||||
for host in "${hosts[@]}"; do
|
||||
ssh $host "echo 'Hello, world!' > /tmp/hello.txt"
|
||||
done
|
||||
# Start on custom host/port with browser
|
||||
lcg serve --host 0.0.0.0 --port 9000 --browser
|
||||
```
|
||||
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:
|
||||
Supported browsers (in priority order):
|
||||
|
||||
- Yandex Browser (`yandex-browser`, `yandex-browser-stable`)
|
||||
- Mozilla Firefox (`firefox`, `firefox-esr`)
|
||||
- Google Chrome (`google-chrome`, `google-chrome-stable`)
|
||||
- Chromium (`chromium`, `chromium-browser`)
|
||||
|
||||
## API Access
|
||||
|
||||
The `serve` command provides both a web interface and REST API:
|
||||
|
||||
**Web Interface:**
|
||||
|
||||
- Browse results at `http://localhost:8080/`
|
||||
- Execute requests at `http://localhost:8080/run`
|
||||
- Manage prompts at `http://localhost:8080/prompts`
|
||||
- View history at `http://localhost:8080/history`
|
||||
|
||||
**REST API:**
|
||||
|
||||
```bash
|
||||
ssh $host "python /path/to/script.py"
|
||||
```
|
||||
This will execute the Python script located at `/path/to/script.py` on the remote host.
|
||||
# Start server
|
||||
lcg serve
|
||||
|
||||
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):
|
||||
# Make API request
|
||||
curl -X POST http://localhost:8080/execute \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"prompt": "create directory test", "verbose": "vv"}'
|
||||
```
|
||||
|
||||
To use the "copy to clipboard" feature, you need to install either the `xclip` or `xsel` package.
|
||||
**Response:**
|
||||
|
||||
### Options
|
||||
```bash
|
||||
> 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
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"command": "mkdir test",
|
||||
"explanation": "The mkdir command creates a new directory...",
|
||||
"model": "hf.co/yandex/YandexGPT-5-Lite-8B-instruct-GGUF:Q4_K_M",
|
||||
"elapsed": 1.23
|
||||
}
|
||||
```
|
||||
|
||||
For complete API documentation, see `API_GUIDE.md`.
|
||||
|
||||
For full guide in Russian, see `USAGE_GUIDE.md`.
|
||||
|
||||
424
USAGE_GUIDE.md
Normal file
424
USAGE_GUIDE.md
Normal file
@@ -0,0 +1,424 @@
|
||||
# Руководство по использованию (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/Direct-Dev-Ru/go-lcg.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)ничего:
|
||||
```
|
||||
|
||||
### Что нового в 2.0.1
|
||||
|
||||
- Улучшена мобильная версия веб‑интерфейса: корректные размеры кнопок, шрифтов и отступов; адаптивная верстка
|
||||
- Учитывается `prefers-reduced-motion` для снижения анимаций, если это задано в системе
|
||||
- Добавлен REST эндпоинт `POST /execute` (только через curl) — см. подробности и примеры в `API_GUIDE.md`
|
||||
|
||||
## Переменные окружения
|
||||
|
||||
Можно настроить поведение без изменения командной строки.
|
||||
|
||||
| Переменная | Значение по умолчанию | Назначение |
|
||||
| --- | --- | --- |
|
||||
| `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_BROWSER_PATH` | пусто | Путь к браузеру для автооткрытия (`--browser`). |
|
||||
| `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_ALLOW_EXECUTION` | пусто | Если `1`/`true` — включает возможность выполнения команд через опцию `(e)` в меню действий. |
|
||||
| `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 системного промпта (1–5 для стандартных, либо ваш кастомный ID).
|
||||
- `--timeout, -t int` — таймаут запроса в секундах (по умолчанию 120; через `LCG_TIMEOUT` — 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`: запустить HTTP сервер для просмотра сохраненных результатов:
|
||||
- `--port, -p` — порт сервера (по умолчанию из `LCG_SERVER_PORT`)
|
||||
- `--host, -H` — хост сервера (по умолчанию из `LCG_SERVER_HOST`)
|
||||
- `--browser, -b` — открыть браузер автоматически после старта
|
||||
|
||||
### Подробные объяснения (v/vv/vvv)
|
||||
|
||||
- `v` — кратко: что делает команда и ключевые опции, без альтернатив.
|
||||
- `vv` — средне: назначение, основные ключи, 1–2 примера, кратко об альтернативах.
|
||||
- `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), английские — в популярных open‑source (Ollama).
|
||||
- Балансируйте: скорость (малые модели) vs качество (крупные модели). Тестируйте `lcg models` и подбирайте `LCG_MODEL`.
|
||||
|
||||
### Таймауты
|
||||
|
||||
- Стартовые значения: локально с Ollama — **120–300 сек**, удалённый proxy — **300–600 сек**.
|
||||
- Увеличьте таймаут для больших моделей/длинных запросов. Флаг `--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 1–5) и промптами подробности (ID 6–8)
|
||||
- Если файл `sys_prompts` **существует** — загружаются все промпты из файла
|
||||
- **Промпты подробности** (v/vv/vvv) сохраняются в том же файле с ID 6, 7, 8
|
||||
|
||||
### Встроенные промпты (ID 1–5)
|
||||
|
||||
| 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 6–8)
|
||||
|
||||
| ID | Name | Описание |
|
||||
| --- | --- | --- |
|
||||
| 6 | verbose-v | Подробный режим (v) - детальное объяснение команды |
|
||||
| 7 | verbose-vv | Очень подробный режим (vv) - исчерпывающее объяснение с альтернативами |
|
||||
| 8 | verbose-vvv | Максимально подробный режим (vvv) - полное руководство с примерами |
|
||||
|
||||
### Веб-интерфейс управления
|
||||
|
||||
Через HTTP сервер (`lcg serve`) доступно полное управление промптами:
|
||||
|
||||
- **Просмотр всех промптов** (встроенных и пользовательских)
|
||||
- **Редактирование любых промптов** (включая встроенные)
|
||||
- **Добавление новых промптов**
|
||||
- **Удаление промптов**
|
||||
- **Автоматическое сохранение** в файл `sys_prompts`
|
||||
|
||||
## Сохранение результатов
|
||||
|
||||
При выборе действия `s` ответ сохраняется в `LCG_RESULT_FOLDER` (по умолчанию: `~/.config/lcg/gpt_results`) в файл вида:
|
||||
|
||||
```text
|
||||
gpt_request_<MODEL>_YYYY-MM-DD_HH-MM-SS.md
|
||||
```
|
||||
|
||||
## HTTP сервер для просмотра результатов
|
||||
|
||||
Команда `lcg serve` запускает веб-сервер для удобного просмотра всех сохраненных результатов:
|
||||
|
||||
```bash
|
||||
# Запуск с настройками по умолчанию
|
||||
lcg serve
|
||||
|
||||
# Запуск на другом порту
|
||||
lcg serve --port 9090
|
||||
|
||||
# Запуск на другом хосте
|
||||
lcg serve --host 0.0.0.0 --port 8080
|
||||
|
||||
# Автооткрытие браузера (опционально)
|
||||
lcg serve --browser
|
||||
|
||||
# Использование переменных окружения
|
||||
export LCG_SERVER_PORT=3000
|
||||
export LCG_SERVER_HOST=0.0.0.0
|
||||
lcg serve
|
||||
```
|
||||
|
||||
### Возможности веб-интерфейса
|
||||
|
||||
- **Главная страница** (`/`) — отображает все сохраненные файлы с превью
|
||||
- **Статистика** — количество файлов, файлы за последние 7 дней
|
||||
- **Просмотр файлов** (`/file/{filename}`) — отображение содержимого конкретного файла
|
||||
- **Современный дизайн** — адаптивный интерфейс с карточками файлов
|
||||
- **Сортировка** — файлы отсортированы по дате изменения (новые сверху)
|
||||
- **Превью содержимого** — первые 200 символов каждого файла
|
||||
|
||||
Структура файла (команда):
|
||||
|
||||
- `# <заголовок>` — H1, это ваш запрос, при длине >120 символов обрезается до 116 + `...`.
|
||||
- `## Prompt` — запрос (включая системный промпт).
|
||||
- `## Response` — сгенерированная команда.
|
||||
|
||||
Структура файла (подробное объяснение):
|
||||
|
||||
- `# <заголовок>` — H1, ваш исходный запрос (с тем же правилом обрезки).
|
||||
- `## Prompt` — исходный запрос.
|
||||
- `## Command` — первая сгенерированная команда.
|
||||
- `## Explanation and Alternatives (model: <MODEL>)` — подробное объяснение и альтернативы.
|
||||
|
||||
### Браузер
|
||||
|
||||
- По умолчанию UI не открывается автоматически. Для автооткрытия используйте `--browser`.
|
||||
- Путь к конкретному браузеру можно задать переменной `LCG_BROWSER_PATH`.
|
||||
|
||||
## Выполнение сгенерированной команды
|
||||
|
||||
Действие `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
|
||||
|
||||
# Запуск на другом порту
|
||||
lcg serve --port 9090
|
||||
|
||||
# Запуск на всех интерфейсах
|
||||
lcg serve --host 0.0.0.0 --port 8080
|
||||
```
|
||||
|
||||
## История
|
||||
|
||||
`lcg history` выводит историю из JSON‑файла (`LCG_RESULT_HISTORY`), сохраняется между запусками:
|
||||
|
||||
```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.
|
||||
|
||||
## Доступ к локальному API
|
||||
|
||||
Эндпоинт: `POST /execute` (только через curl).
|
||||
|
||||
```bash
|
||||
# Запустить сервер
|
||||
lcg serve
|
||||
|
||||
# Выполнить запрос
|
||||
curl -X POST http://localhost:8080/execute \
|
||||
-H "Content-Type: application/json" \
|
||||
-A curl \
|
||||
-d '{"prompt": "create directory test", "verbose": "vv"}'
|
||||
```
|
||||
|
||||
Подробности и примеры: `API_GUIDE.md`.
|
||||
@@ -1 +1 @@
|
||||
v1.0.2
|
||||
v2.0.1
|
||||
|
||||
189
cmd/explain.go
Normal file
189
cmd/explain.go
Normal 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")
|
||||
}
|
||||
186
cmd/history.go
Normal file
186
cmd/history.go
Normal file
@@ -0,0 +1,186 @@
|
||||
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)
|
||||
fmt.Printf("%s\n", "========================================================================================")
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
110
config/config.go
Normal file
110
config/config.go
Normal file
@@ -0,0 +1,110 @@
|
||||
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
|
||||
AllowExecution bool
|
||||
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", ""),
|
||||
AllowExecution: isAllowExecutionEnabled(),
|
||||
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"
|
||||
}
|
||||
|
||||
func isAllowExecutionEnabled() bool {
|
||||
v := strings.TrimSpace(getEnv("LCG_ALLOW_EXECUTION", ""))
|
||||
if v == "" {
|
||||
return false
|
||||
}
|
||||
vLower := strings.ToLower(v)
|
||||
return vLower == "1" || vLower == "true"
|
||||
}
|
||||
|
||||
var AppConfig Config
|
||||
|
||||
func init() {
|
||||
AppConfig = Load()
|
||||
}
|
||||
9
go.mod
9
go.mod
@@ -3,3 +3,12 @@ module github.com/direct-dev-ru/linux-command-gpt
|
||||
go 1.18
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
12
go.sum
12
go.sum
@@ -1,2 +1,14 @@
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
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 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
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
124
gpt/builtin_prompts.go
Normal 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
262
gpt/builtin_prompts.yaml
Normal 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. **Исторический контекст**: Эволюция и история разработки
|
||||
|
||||
Предоставьте максимальную техническую глубину с системными инсайтами, примерами кода и архитектурным пониманием.
|
||||
111
gpt/gpt.go
111
gpt/gpt.go
@@ -1,24 +1,40 @@
|
||||
package gpt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ProxySimpleChatRequest структура для простого запроса
|
||||
type ProxySimpleChatRequest struct {
|
||||
Message string `json:"message"`
|
||||
Model string `json:"model,omitempty"`
|
||||
}
|
||||
|
||||
// ProxySimpleChatResponse структура ответа для простого запроса
|
||||
type ProxySimpleChatResponse struct {
|
||||
Response string `json:"response"`
|
||||
Usage struct {
|
||||
PromptTokens int `json:"prompt_tokens"`
|
||||
CompletionTokens int `json:"completion_tokens"`
|
||||
TotalTokens int `json:"total_tokens"`
|
||||
} `json:"usage,omitempty"`
|
||||
Model string `json:"model,omitempty"`
|
||||
Timeout int `json:"timeout_seconds,omitempty"`
|
||||
}
|
||||
|
||||
// Gpt3 обновленная структура с поддержкой разных провайдеров
|
||||
type Gpt3 struct {
|
||||
CompletionUrl string
|
||||
Provider Provider
|
||||
Prompt string
|
||||
Model string
|
||||
HomeDir string
|
||||
ApiKeyFile string
|
||||
ApiKey string
|
||||
Temperature float64
|
||||
ProviderType string // "ollama", "proxy"
|
||||
}
|
||||
|
||||
type Chat struct {
|
||||
@@ -135,6 +151,11 @@ func (gpt3 *Gpt3) DeleteKey() {
|
||||
}
|
||||
|
||||
func (gpt3 *Gpt3) InitKey() {
|
||||
// Для ollama и proxy провайдеров не нужен API ключ
|
||||
if gpt3.ProviderType == "ollama" || gpt3.ProviderType == "proxy" {
|
||||
return
|
||||
}
|
||||
|
||||
load := gpt3.loadApiKey()
|
||||
if load {
|
||||
return
|
||||
@@ -145,55 +166,51 @@ func (gpt3 *Gpt3) InitKey() {
|
||||
gpt3.storeApiKey(apiKey)
|
||||
}
|
||||
|
||||
func (gpt3 *Gpt3) Completions(ask string) string {
|
||||
req, err := http.NewRequest("POST", gpt3.CompletionUrl, nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
// req.Header.Set("Authorization", "Bearer "+strings.TrimSpace(gpt3.ApiKey))
|
||||
// NewGpt3 создает новый экземпляр GPT с выбранным провайдером
|
||||
func NewGpt3(providerType, host, apiKey, model, prompt string, temperature float64, timeout int) *Gpt3 {
|
||||
var provider Provider
|
||||
|
||||
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{
|
||||
{"system", gpt3.Prompt},
|
||||
{"user", ask + "." + gpt3.Prompt},
|
||||
}
|
||||
payload := Gpt3Request{
|
||||
Model: gpt3.Model,
|
||||
Messages: messages,
|
||||
Stream: false,
|
||||
Options: Gpt3Options{gpt3.Temperature},
|
||||
{"user", ask + ". " + gpt3.Prompt},
|
||||
}
|
||||
|
||||
payloadJson, err := json.Marshal(payload)
|
||||
response, err := gpt3.Provider.Chat(messages)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
req.Body = io.NopCloser(bytes.NewBuffer(payloadJson))
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
fmt.Println(string(body))
|
||||
fmt.Printf("Ошибка при выполнении запроса: %v\n", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
// var res Gpt3Response
|
||||
var res OllamaResponse
|
||||
err = json.Unmarshal(body, &res)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// return strings.TrimSpace(res.Choices[0].Message.Content)
|
||||
return strings.TrimSpace(res.Message.Content)
|
||||
return response
|
||||
}
|
||||
|
||||
// Health проверяет состояние провайдера
|
||||
func (gpt3 *Gpt3) Health() error {
|
||||
return gpt3.Provider.Health()
|
||||
}
|
||||
|
||||
// GetAvailableModels возвращает список доступных моделей
|
||||
func (gpt3 *Gpt3) GetAvailableModels() ([]string, error) {
|
||||
return gpt3.Provider.GetAvailableModels()
|
||||
}
|
||||
|
||||
403
gpt/prompts.go
Normal file
403
gpt/prompts.go
Normal file
@@ -0,0 +1,403 @@
|
||||
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() {
|
||||
// Устанавливаем язык по умолчанию как русский
|
||||
pm.Language = "ru"
|
||||
|
||||
// Загружаем все встроенные промпты из YAML на русском языке
|
||||
pm.Prompts = GetBuiltinPromptsByLanguage("ru")
|
||||
|
||||
// Сохраняем все промпты в файл
|
||||
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] + "..."
|
||||
}
|
||||
|
||||
// GetVerbosePromptByLevel возвращает промпт для подробного объяснения по уровню
|
||||
func GetVerbosePromptByLevel(level int) string {
|
||||
// Создаем PromptManager для получения текущего языка из sys_prompts (без принудительной загрузки дефолтов)
|
||||
pm := NewPromptManager("")
|
||||
currentLang := pm.GetCurrentLanguage()
|
||||
|
||||
var prompt *SystemPrompt
|
||||
switch level {
|
||||
case 1:
|
||||
prompt = GetBuiltinPromptByIDAndLanguage(6, currentLang) // v
|
||||
case 2:
|
||||
prompt = GetBuiltinPromptByIDAndLanguage(7, currentLang) // vv
|
||||
case 3:
|
||||
prompt = GetBuiltinPromptByIDAndLanguage(8, currentLang) // vvv
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
|
||||
if prompt != nil {
|
||||
return prompt.Content
|
||||
}
|
||||
return ""
|
||||
}
|
||||
296
gpt/providers.go
Normal file
296
gpt/providers.go
Normal 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
|
||||
}
|
||||
32
main_test.go
32
main_test.go
@@ -1,33 +1 @@
|
||||
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
59
response.go
Normal 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]) + " ..."
|
||||
}
|
||||
110
serve/README.md
Normal file
110
serve/README.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# Пакет serve
|
||||
|
||||
Этот пакет содержит HTTP сервер для веб-интерфейса LCG (Linux Command GPT).
|
||||
|
||||
## Структура файлов
|
||||
|
||||
### serve.go
|
||||
|
||||
Основной файл пакета. Содержит:
|
||||
|
||||
- `StartResultServer()` - функция запуска HTTP сервера
|
||||
- `registerRoutes()` - регистрация всех маршрутов
|
||||
|
||||
### results.go
|
||||
|
||||
Обработчики для результатов и файлов:
|
||||
|
||||
- `handleResultsPage()` - главная страница со списком файлов результатов
|
||||
- `handleFileView()` - просмотр конкретного файла
|
||||
- `handleDeleteFile()` - удаление файла результата
|
||||
- `getResultFiles()` - получение списка файлов
|
||||
- `formatFileSize()` - форматирование размера файла
|
||||
|
||||
### history.go
|
||||
|
||||
Обработчики для работы с историей запросов:
|
||||
|
||||
- `handleHistoryPage()` - страница истории запросов
|
||||
- `handleDeleteHistoryEntry()` - удаление записи из истории
|
||||
- `handleClearHistory()` - очистка всей истории
|
||||
- `readHistoryEntries()` - чтение записей истории
|
||||
|
||||
### history_utils.go
|
||||
|
||||
Утилиты для работы с историей:
|
||||
|
||||
- `HistoryEntry` - структура записи истории
|
||||
- `read()` - чтение истории из файла
|
||||
- `write()` - запись истории в файл
|
||||
- `DeleteHistoryEntry()` - удаление записи по индексу
|
||||
|
||||
### prompts.go
|
||||
|
||||
Обработчики для управления промптами:
|
||||
|
||||
- `handlePromptsPage()` - страница управления промптами
|
||||
- `handleAddPrompt()` - добавление нового промпта
|
||||
- `handleEditPrompt()` - редактирование промпта
|
||||
- `handleDeletePrompt()` - удаление промпта
|
||||
- `handleRestorePrompt()` - восстановление системного промпта к значению по умолчанию
|
||||
- `handleRestoreVerbosePrompt()` - восстановление verbose промпта
|
||||
- `handleSaveLang()` - сохранение промптов при переключении языка
|
||||
|
||||
### prompts_helpers.go
|
||||
|
||||
Вспомогательные функции для работы с промптами:
|
||||
|
||||
- `getVerbosePromptsFromFile()` - получение verbose промптов из файла
|
||||
- `translateVerbosePrompt()` - перевод verbose промпта
|
||||
- `getVerbosePrompts()` - получение встроенных verbose промптов (fallback)
|
||||
- `getSystemPromptsWithLang()` - получение системных промптов с учетом языка
|
||||
- `translateSystemPrompt()` - перевод системного промпта
|
||||
|
||||
## Использование
|
||||
|
||||
```go
|
||||
import "github.com/direct-dev-ru/linux-command-gpt/serve"
|
||||
|
||||
// Запуск сервера на localhost:8080
|
||||
err := serve.StartResultServer("localhost", "8080")
|
||||
```
|
||||
|
||||
## Маршруты
|
||||
|
||||
### Результаты
|
||||
|
||||
- `GET /` - главная страница со списком файлов
|
||||
- `GET /file/{filename}` - просмотр файла результата
|
||||
- `DELETE /delete/{filename}` - удаление файла
|
||||
|
||||
### История
|
||||
|
||||
- `GET /history` - страница истории запросов
|
||||
- `GET /history/view/{id}` - просмотр записи истории в развернутом виде
|
||||
- `DELETE /history/delete/{id}` - удаление записи
|
||||
- `DELETE /history/clear` - очистка всей истории
|
||||
|
||||
### Промпты
|
||||
|
||||
- `GET /prompts` - страница управления промптами
|
||||
- `POST /prompts/add` - добавление промпта
|
||||
- `PUT /prompts/edit/{id}` - редактирование промпта
|
||||
- `DELETE /prompts/delete/{id}` - удаление промпта
|
||||
- `POST /prompts/restore/{id}` - восстановление системного промпта
|
||||
- `POST /prompts/restore-verbose/{mode}` - восстановление verbose промпта (v/vv/vvv)
|
||||
- `POST /prompts/save-lang` - сохранение языка промптов
|
||||
|
||||
### Выполнение запросов
|
||||
|
||||
- `GET /run` - веб-страница для выполнения запросов
|
||||
- `POST /run` - обработка выполнения запроса
|
||||
- `POST /execute` - API для программного доступа (только curl)
|
||||
|
||||
## Особенности
|
||||
|
||||
- **Многоязычность**: Поддержка английского и русского языков для промптов
|
||||
- **Responsive дизайн**: Адаптивный интерфейс для различных устройств
|
||||
- **Markdown**: Автоматическая конвертация Markdown файлов в HTML
|
||||
- **История**: Поиск дубликатов с учетом регистра
|
||||
- **Промпты**: Управление встроенными и пользовательскими промптами
|
||||
205
serve/api.go
Normal file
205
serve/api.go
Normal file
@@ -0,0 +1,205 @@
|
||||
package serve
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||
)
|
||||
|
||||
// SaveResultRequest представляет запрос на сохранение результата
|
||||
type SaveResultRequest struct {
|
||||
Prompt string `json:"prompt"`
|
||||
Command string `json:"command"`
|
||||
Explanation string `json:"explanation,omitempty"`
|
||||
Model string `json:"model"`
|
||||
}
|
||||
|
||||
// SaveResultResponse представляет ответ на сохранение результата
|
||||
type SaveResultResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
File string `json:"file,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// AddToHistoryRequest представляет запрос на добавление в историю
|
||||
type AddToHistoryRequest struct {
|
||||
Prompt string `json:"prompt"`
|
||||
Command string `json:"command"`
|
||||
Response string `json:"response"`
|
||||
Explanation string `json:"explanation,omitempty"`
|
||||
System string `json:"system"`
|
||||
}
|
||||
|
||||
// AddToHistoryResponse представляет ответ на добавление в историю
|
||||
type AddToHistoryResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// handleSaveResult обрабатывает сохранение результата
|
||||
func handleSaveResult(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var req SaveResultRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Prompt == "" || req.Command == "" {
|
||||
http.Error(w, "Prompt and command are required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Создаем папку результатов если не существует
|
||||
if err := os.MkdirAll(config.AppConfig.ResultFolder, 0755); err != nil {
|
||||
apiJsonResponse(w, SaveResultResponse{
|
||||
Success: false,
|
||||
Error: "Failed to create result folder",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Генерируем имя файла
|
||||
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
||||
filename := fmt.Sprintf("gpt_request_%s_%s.md", req.Model, timestamp)
|
||||
filePath := path.Join(config.AppConfig.ResultFolder, filename)
|
||||
title := truncateTitle(req.Prompt)
|
||||
|
||||
// Формируем содержимое
|
||||
var content string
|
||||
if strings.TrimSpace(req.Explanation) != "" {
|
||||
content = fmt.Sprintf("# %s\n\n## Prompt\n\n%s\n\n## Response\n\n%s\n\n## Explanation\n\n%s\n",
|
||||
title, req.Prompt, req.Command, req.Explanation)
|
||||
} else {
|
||||
content = fmt.Sprintf("# %s\n\n## Prompt\n\n%s\n\n## Response\n\n%s\n",
|
||||
title, req.Prompt, req.Command)
|
||||
}
|
||||
|
||||
// Сохраняем файл
|
||||
if err := os.WriteFile(filePath, []byte(content), 0644); err != nil {
|
||||
apiJsonResponse(w, SaveResultResponse{
|
||||
Success: false,
|
||||
Error: "Failed to save file",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Debug вывод для сохранения результата
|
||||
PrintWebSaveDebugInfo("SAVE_RESULT", req.Prompt, req.Command, req.Explanation, req.Model, filename)
|
||||
|
||||
apiJsonResponse(w, SaveResultResponse{
|
||||
Success: true,
|
||||
Message: "Result saved successfully",
|
||||
File: filename,
|
||||
})
|
||||
}
|
||||
|
||||
// handleAddToHistory обрабатывает добавление в историю
|
||||
func handleAddToHistory(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var req AddToHistoryRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Prompt == "" || req.Command == "" || req.Response == "" {
|
||||
http.Error(w, "Prompt, command and response are required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Проверяем, есть ли уже такой запрос в истории
|
||||
entries, err := Read(config.AppConfig.ResultHistory)
|
||||
if err != nil {
|
||||
// Если файл не существует, создаем пустой массив
|
||||
entries = []HistoryEntry{}
|
||||
}
|
||||
|
||||
// Ищем дубликат
|
||||
duplicateIndex := -1
|
||||
for i, entry := range entries {
|
||||
if strings.EqualFold(strings.TrimSpace(entry.Command), strings.TrimSpace(req.Prompt)) {
|
||||
duplicateIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Создаем новую запись
|
||||
newEntry := HistoryEntry{
|
||||
Index: len(entries) + 1,
|
||||
Command: req.Prompt,
|
||||
Response: req.Response,
|
||||
Explanation: req.Explanation,
|
||||
System: req.System,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
if duplicateIndex == -1 {
|
||||
// Добавляем новую запись
|
||||
entries = append(entries, newEntry)
|
||||
} else {
|
||||
// Перезаписываем существующую
|
||||
newEntry.Index = entries[duplicateIndex].Index
|
||||
entries[duplicateIndex] = newEntry
|
||||
}
|
||||
|
||||
// Сохраняем историю
|
||||
if err := Write(config.AppConfig.ResultHistory, entries); err != nil {
|
||||
apiJsonResponse(w, AddToHistoryResponse{
|
||||
Success: false,
|
||||
Error: "Failed to save to history",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
message := "Added to history successfully"
|
||||
if duplicateIndex != -1 {
|
||||
message = "Updated existing history entry"
|
||||
}
|
||||
|
||||
// Debug вывод для добавления в историю
|
||||
PrintWebSaveDebugInfo("ADD_TO_HISTORY", req.Prompt, req.Command, req.Explanation, req.System, "")
|
||||
|
||||
apiJsonResponse(w, AddToHistoryResponse{
|
||||
Success: true,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
// apiJsonResponse отправляет JSON ответ
|
||||
func apiJsonResponse(w http.ResponseWriter, data interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(data); err != nil {
|
||||
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// 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]) + " ..."
|
||||
}
|
||||
56
serve/debug.go
Normal file
56
serve/debug.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package serve
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||
)
|
||||
|
||||
// PrintWebDebugInfo выводит отладочную информацию для веб-запросов
|
||||
func PrintWebDebugInfo(operation, prompt, systemPrompt, model string, timeout int) {
|
||||
if !config.AppConfig.MainFlags.Debug {
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("\n🔍 DEBUG WEB %s:\n", operation)
|
||||
fmt.Printf("💬 Запрос: %s\n", prompt)
|
||||
fmt.Printf("🤖 Системный промпт: %s\n", systemPrompt)
|
||||
fmt.Printf("⏱️ Таймаут: %d сек\n", timeout)
|
||||
fmt.Printf("🌐 Провайдер: %s\n", config.AppConfig.ProviderType)
|
||||
fmt.Printf("🏠 Хост: %s\n", config.AppConfig.Host)
|
||||
fmt.Printf("🧠 Модель: %s\n", model)
|
||||
fmt.Printf("📝 История: %t\n", !config.AppConfig.MainFlags.NoHistory)
|
||||
fmt.Printf("────────────────────────────────────────\n")
|
||||
}
|
||||
|
||||
// PrintWebVerboseDebugInfo выводит отладочную информацию для verbose запросов
|
||||
func PrintWebVerboseDebugInfo(operation, prompt, verbosePrompt, model string, level int, timeout int) {
|
||||
if !config.AppConfig.MainFlags.Debug {
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("\n🔍 DEBUG WEB %s (v%d):\n", operation, level)
|
||||
fmt.Printf("💬 Запрос: %s\n", prompt)
|
||||
fmt.Printf("📝 Системный промпт подробности:\n%s\n", verbosePrompt)
|
||||
fmt.Printf("⏱️ Таймаут: %d сек\n", timeout)
|
||||
fmt.Printf("🌐 Провайдер: %s\n", config.AppConfig.ProviderType)
|
||||
fmt.Printf("🏠 Хост: %s\n", config.AppConfig.Host)
|
||||
fmt.Printf("🧠 Модель: %s\n", model)
|
||||
fmt.Printf("🎯 Уровень подробности: %d\n", level)
|
||||
fmt.Printf("────────────────────────────────────────\n")
|
||||
}
|
||||
|
||||
// PrintWebSaveDebugInfo выводит отладочную информацию для сохранения
|
||||
func PrintWebSaveDebugInfo(operation, prompt, command, explanation, model, file string) {
|
||||
if !config.AppConfig.MainFlags.Debug {
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("\n🔍 DEBUG WEB %s:\n", operation)
|
||||
fmt.Printf("💬 Запрос: %s\n", prompt)
|
||||
fmt.Printf("⚡ Команда: %s\n", command)
|
||||
fmt.Printf("📖 Объяснение: %s\n", explanation)
|
||||
fmt.Printf("🧠 Модель: %s\n", model)
|
||||
fmt.Printf("📁 Файл: %s\n", file)
|
||||
fmt.Printf("────────────────────────────────────────\n")
|
||||
}
|
||||
178
serve/execute.go
Normal file
178
serve/execute.go
Normal file
@@ -0,0 +1,178 @@
|
||||
package serve
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||
"github.com/direct-dev-ru/linux-command-gpt/gpt"
|
||||
)
|
||||
|
||||
// ExecuteRequest представляет запрос на выполнение
|
||||
type ExecuteRequest struct {
|
||||
Prompt string `json:"prompt"` // Пользовательский промпт
|
||||
SystemID int `json:"system_id"` // ID системного промпта (1-5)
|
||||
SystemText string `json:"system"` // Текст системного промпта (альтернатива system_id)
|
||||
Verbose string `json:"verbose"` // Степень подробности: "v", "vv", "vvv" или пустая строка
|
||||
Timeout int `json:"timeout"` // Таймаут в секундах (опционально)
|
||||
}
|
||||
|
||||
// ExecuteResponse представляет ответ
|
||||
type ExecuteResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Command string `json:"command,omitempty"`
|
||||
Explanation string `json:"explanation,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Model string `json:"model,omitempty"`
|
||||
Elapsed float64 `json:"elapsed,omitempty"`
|
||||
}
|
||||
|
||||
// handleExecute обрабатывает POST запросы на выполнение
|
||||
func handleExecute(w http.ResponseWriter, r *http.Request) {
|
||||
// Проверяем User-Agent - только curl
|
||||
userAgent := r.Header.Get("User-Agent")
|
||||
if !strings.Contains(strings.ToLower(userAgent), "curl") {
|
||||
http.Error(w, "Only curl requests are allowed", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Проверяем метод
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Парсим JSON
|
||||
var req ExecuteRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Валидация обязательных полей
|
||||
if req.Prompt == "" {
|
||||
http.Error(w, "Prompt is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Определяем системный промпт
|
||||
systemPrompt := ""
|
||||
if req.SystemText != "" {
|
||||
systemPrompt = req.SystemText
|
||||
} else if req.SystemID > 0 && req.SystemID <= 5 {
|
||||
// Получаем системный промпт по ID
|
||||
pm := gpt.NewPromptManager(config.AppConfig.PromptFolder)
|
||||
prompt, err := pm.GetPromptByID(req.SystemID)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to get system prompt", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
systemPrompt = prompt.Content
|
||||
} else {
|
||||
// Используем промпт по умолчанию
|
||||
systemPrompt = config.AppConfig.Prompt
|
||||
}
|
||||
|
||||
// Устанавливаем таймаут
|
||||
timeout := req.Timeout
|
||||
if timeout <= 0 {
|
||||
timeout = 120 // По умолчанию 2 минуты
|
||||
}
|
||||
|
||||
// Создаем GPT клиент
|
||||
gpt3 := gpt.NewGpt3(
|
||||
config.AppConfig.ProviderType,
|
||||
config.AppConfig.Host,
|
||||
config.AppConfig.JwtToken,
|
||||
config.AppConfig.Model,
|
||||
systemPrompt,
|
||||
0.01,
|
||||
timeout,
|
||||
)
|
||||
|
||||
// Выполняем запрос
|
||||
response, elapsed := getCommand(*gpt3, req.Prompt)
|
||||
if response == "" {
|
||||
jsonResponse(w, ExecuteResponse{
|
||||
Success: false,
|
||||
Error: "Failed to get response from AI",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Если запрошено подробное объяснение
|
||||
if req.Verbose != "" {
|
||||
explanation, err := getDetailedExplanation(req.Prompt, req.Verbose, timeout)
|
||||
if err != nil {
|
||||
jsonResponse(w, ExecuteResponse{
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("Failed to get explanation: %v", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
jsonResponse(w, ExecuteResponse{
|
||||
Success: true,
|
||||
Command: response,
|
||||
Explanation: explanation,
|
||||
Model: config.AppConfig.Model,
|
||||
Elapsed: elapsed,
|
||||
})
|
||||
} else {
|
||||
jsonResponse(w, ExecuteResponse{
|
||||
Success: true,
|
||||
Command: response,
|
||||
Model: config.AppConfig.Model,
|
||||
Elapsed: elapsed,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// getCommand выполняет запрос к AI
|
||||
func getCommand(gpt3 gpt.Gpt3, prompt string) (string, float64) {
|
||||
gpt3.InitKey()
|
||||
start := time.Now()
|
||||
response := gpt3.Completions(prompt)
|
||||
elapsed := time.Since(start).Seconds()
|
||||
return response, elapsed
|
||||
}
|
||||
|
||||
// getDetailedExplanation получает подробное объяснение
|
||||
func getDetailedExplanation(prompt, verbose string, timeout int) (string, error) {
|
||||
level := len(verbose) // 1, 2, 3
|
||||
|
||||
// Получаем системный промпт для подробного объяснения
|
||||
detailedSystem := gpt.GetVerbosePromptByLevel(level)
|
||||
if detailedSystem == "" {
|
||||
return "", fmt.Errorf("invalid verbose level: %s", verbose)
|
||||
}
|
||||
|
||||
// Создаем GPT клиент для объяснения
|
||||
explanationGpt := gpt.NewGpt3(
|
||||
config.AppConfig.ProviderType,
|
||||
config.AppConfig.Host,
|
||||
config.AppConfig.JwtToken,
|
||||
config.AppConfig.Model,
|
||||
detailedSystem,
|
||||
0.2,
|
||||
timeout,
|
||||
)
|
||||
|
||||
explanationGpt.InitKey()
|
||||
explanation := explanationGpt.Completions(prompt)
|
||||
|
||||
if explanation == "" {
|
||||
return "", fmt.Errorf("failed to get explanation")
|
||||
}
|
||||
|
||||
return explanation, nil
|
||||
}
|
||||
|
||||
// jsonResponse отправляет JSON ответ
|
||||
func jsonResponse(w http.ResponseWriter, response ExecuteResponse) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
293
serve/execute_page.go
Normal file
293
serve/execute_page.go
Normal file
@@ -0,0 +1,293 @@
|
||||
package serve
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"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/serve/templates"
|
||||
"github.com/russross/blackfriday/v2"
|
||||
)
|
||||
|
||||
// ExecutePageData содержит данные для страницы выполнения
|
||||
type ExecutePageData struct {
|
||||
Title string
|
||||
Header string
|
||||
CurrentPrompt string
|
||||
SystemOptions []SystemPromptOption
|
||||
ResultSection template.HTML
|
||||
VerboseButtons template.HTML
|
||||
ActionButtons template.HTML
|
||||
}
|
||||
|
||||
// SystemPromptOption представляет опцию системного промпта
|
||||
type SystemPromptOption struct {
|
||||
ID int
|
||||
Name string
|
||||
Description string
|
||||
}
|
||||
|
||||
// ExecuteResultData содержит результат выполнения
|
||||
type ExecuteResultData struct {
|
||||
Success bool
|
||||
Command string
|
||||
Explanation string
|
||||
Error string
|
||||
Model string
|
||||
Elapsed float64
|
||||
Verbose string
|
||||
}
|
||||
|
||||
// handleExecutePage обрабатывает страницу выполнения
|
||||
func handleExecutePage(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
// Показываем форму
|
||||
showExecuteForm(w)
|
||||
case http.MethodPost:
|
||||
// Обрабатываем выполнение
|
||||
handleExecuteRequest(w, r)
|
||||
default:
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
// showExecuteForm показывает форму выполнения
|
||||
func showExecuteForm(w http.ResponseWriter) {
|
||||
// Получаем системные промпты
|
||||
pm := gpt.NewPromptManager(config.AppConfig.PromptFolder)
|
||||
|
||||
var systemOptions []SystemPromptOption
|
||||
for i := 1; i <= 5; i++ {
|
||||
prompt, err := pm.GetPromptByID(i)
|
||||
if err == nil {
|
||||
systemOptions = append(systemOptions, SystemPromptOption{
|
||||
ID: prompt.ID,
|
||||
Name: prompt.Name,
|
||||
Description: prompt.Description,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
data := ExecutePageData{
|
||||
Title: "Выполнение запроса",
|
||||
Header: "Выполнение запроса",
|
||||
CurrentPrompt: "",
|
||||
SystemOptions: systemOptions,
|
||||
ResultSection: template.HTML(""),
|
||||
VerboseButtons: template.HTML(""),
|
||||
ActionButtons: template.HTML(""),
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
templates.ExecutePageTemplate.Execute(w, data)
|
||||
}
|
||||
|
||||
// handleExecuteRequest обрабатывает запрос на выполнение
|
||||
func handleExecuteRequest(w http.ResponseWriter, r *http.Request) {
|
||||
// Парсим форму
|
||||
prompt := r.FormValue("prompt")
|
||||
systemIDStr := r.FormValue("system_id")
|
||||
verbose := r.FormValue("verbose")
|
||||
|
||||
// Получаем системные промпты
|
||||
pm := gpt.NewPromptManager(config.AppConfig.PromptFolder)
|
||||
|
||||
if prompt == "" {
|
||||
http.Error(w, "Prompt is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
systemID := 1
|
||||
if systemIDStr != "" {
|
||||
if id, err := strconv.Atoi(systemIDStr); err == nil && id >= 1 && id <= 5 {
|
||||
systemID = id
|
||||
}
|
||||
}
|
||||
|
||||
// Получаем системный промпт
|
||||
systemPrompt, err := pm.GetPromptByID(systemID)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to get system prompt", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Создаем GPT клиент
|
||||
gpt3 := gpt.NewGpt3(
|
||||
config.AppConfig.ProviderType,
|
||||
config.AppConfig.Host,
|
||||
config.AppConfig.JwtToken,
|
||||
config.AppConfig.Model,
|
||||
systemPrompt.Content,
|
||||
0.01,
|
||||
120,
|
||||
)
|
||||
|
||||
// Debug вывод для основного запроса
|
||||
PrintWebDebugInfo("EXECUTE", prompt, systemPrompt.Content, config.AppConfig.Model, 120)
|
||||
|
||||
// Выполняем запрос
|
||||
response, elapsed := getCommand(*gpt3, prompt)
|
||||
|
||||
var result ExecuteResultData
|
||||
if response == "" {
|
||||
result = ExecuteResultData{
|
||||
Success: false,
|
||||
Error: "Failed to get response from AI",
|
||||
}
|
||||
} else {
|
||||
result = ExecuteResultData{
|
||||
Success: true,
|
||||
Command: response,
|
||||
Model: config.AppConfig.Model,
|
||||
Elapsed: elapsed,
|
||||
}
|
||||
}
|
||||
|
||||
// Если запрошено подробное объяснение
|
||||
if verbose != "" {
|
||||
level := len(verbose)
|
||||
verbosePrompt := gpt.GetVerbosePromptByLevel(level)
|
||||
|
||||
// Debug вывод для verbose запроса
|
||||
PrintWebVerboseDebugInfo("VERBOSE", prompt, verbosePrompt, config.AppConfig.Model, level, 120)
|
||||
|
||||
explanation, err := getDetailedExplanation(prompt, verbose, 120)
|
||||
if err == nil {
|
||||
// Конвертируем Markdown в HTML
|
||||
explanationHTML := blackfriday.Run([]byte(explanation))
|
||||
result.Explanation = string(explanationHTML)
|
||||
result.Verbose = verbose
|
||||
}
|
||||
}
|
||||
|
||||
// Получаем системные промпты для dropdown
|
||||
var systemOptions []SystemPromptOption
|
||||
for i := 1; i <= 5; i++ {
|
||||
prompt, err := pm.GetPromptByID(i)
|
||||
if err == nil {
|
||||
systemOptions = append(systemOptions, SystemPromptOption{
|
||||
ID: prompt.ID,
|
||||
Name: prompt.Name,
|
||||
Description: prompt.Description,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
data := ExecutePageData{
|
||||
Title: "Результат выполнения",
|
||||
Header: "Результат выполнения",
|
||||
CurrentPrompt: prompt,
|
||||
SystemOptions: systemOptions,
|
||||
ResultSection: template.HTML(formatResultSection(result)),
|
||||
VerboseButtons: template.HTML(formatVerboseButtons(result)),
|
||||
ActionButtons: template.HTML(formatActionButtons(result)),
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
templates.ExecutePageTemplate.Execute(w, data)
|
||||
}
|
||||
|
||||
// formatResultSection форматирует секцию результата
|
||||
func formatResultSection(result ExecuteResultData) string {
|
||||
if !result.Success {
|
||||
return fmt.Sprintf(`
|
||||
<div class="result-section">
|
||||
<div class="error-message">
|
||||
<h3>❌ Ошибка</h3>
|
||||
<p>%s</p>
|
||||
</div>
|
||||
</div>`, result.Error)
|
||||
}
|
||||
|
||||
explanationSection := ""
|
||||
if result.Explanation != "" {
|
||||
explanationSection = fmt.Sprintf(`
|
||||
<div class="explanation-section">
|
||||
<h3>📖 Подробное объяснение (%s):</h3>
|
||||
<div class="explanation-content">%s</div>
|
||||
</div>
|
||||
<script>
|
||||
// Показываем кнопку "Наверх" после загрузки объяснения
|
||||
showScrollToTopButton();
|
||||
</script>`, result.Verbose, result.Explanation)
|
||||
}
|
||||
|
||||
// Определяем, содержит ли результат Markdown/многострочный текст
|
||||
useMarkdown := false
|
||||
if strings.Contains(result.Command, "```") || strings.Contains(result.Command, "\n") || strings.Contains(result.Command, "#") || strings.Contains(result.Command, "*") || strings.Contains(result.Command, "_") {
|
||||
useMarkdown = true
|
||||
}
|
||||
|
||||
commandBlock := ""
|
||||
if useMarkdown {
|
||||
// Рендерим Markdown в HTML
|
||||
cmdHTML := blackfriday.Run([]byte(result.Command))
|
||||
commandBlock = fmt.Sprintf(`<div class="command-md">%s</div>`, string(cmdHTML))
|
||||
} else {
|
||||
// Оставляем как простой однострочный вывод команды
|
||||
commandBlock = fmt.Sprintf(`<div class="command-code">%s</div>`, result.Command)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`
|
||||
<div class="result-section">
|
||||
<div class="command-result">
|
||||
<h3>✅ Команда:</h3>
|
||||
%s
|
||||
<div class="result-meta">
|
||||
<span>Модель: %s</span>
|
||||
<span>Время: %.2f сек</span>
|
||||
</div>
|
||||
</div>
|
||||
%s
|
||||
</div>
|
||||
<script>
|
||||
// Сохраняем результаты в скрытое поле
|
||||
(function() {
|
||||
const resultData = {
|
||||
command: %s,
|
||||
explanation: %s,
|
||||
model: %s
|
||||
};
|
||||
const resultDataField = document.getElementById('resultData');
|
||||
if (resultDataField) {
|
||||
resultDataField.value = JSON.stringify(resultData);
|
||||
}
|
||||
})();
|
||||
</script>`,
|
||||
commandBlock, result.Model, result.Elapsed, explanationSection,
|
||||
fmt.Sprintf(`"%s"`, strings.ReplaceAll(result.Command, `"`, `\"`)),
|
||||
fmt.Sprintf(`"%s"`, strings.ReplaceAll(result.Explanation, `"`, `\"`)),
|
||||
fmt.Sprintf(`"%s"`, result.Model))
|
||||
}
|
||||
|
||||
// formatVerboseButtons форматирует кнопки подробности
|
||||
func formatVerboseButtons(result ExecuteResultData) string {
|
||||
if !result.Success || result.Explanation != "" {
|
||||
return "" // Скрываем кнопки если есть ошибка или уже есть объяснение
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="verbose-buttons">
|
||||
<button onclick="requestExplanation('v')" class="verbose-btn v-btn">v - Краткое объяснение</button>
|
||||
<button onclick="requestExplanation('vv')" class="verbose-btn vv-btn">vv - Подробное объяснение</button>
|
||||
<button onclick="requestExplanation('vvv')" class="verbose-btn vvv-btn">vvv - Максимально подробное</button>
|
||||
</div>`
|
||||
}
|
||||
|
||||
// formatActionButtons форматирует кнопки действий
|
||||
func formatActionButtons(result ExecuteResultData) string {
|
||||
if !result.Success {
|
||||
return "" // Скрываем кнопки если есть ошибка
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="action-buttons">
|
||||
<button onclick="saveResult()" class="action-btn">💾 Сохранить результат</button>
|
||||
<button onclick="addToHistory()" class="action-btn">📝 Добавить в историю</button>
|
||||
</div>`
|
||||
}
|
||||
168
serve/history.go
Normal file
168
serve/history.go
Normal file
@@ -0,0 +1,168 @@
|
||||
package serve
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||
"github.com/direct-dev-ru/linux-command-gpt/serve/templates"
|
||||
"github.com/russross/blackfriday/v2"
|
||||
)
|
||||
|
||||
// HistoryEntryInfo содержит информацию о записи истории для отображения
|
||||
type HistoryEntryInfo struct {
|
||||
Index int
|
||||
Command string
|
||||
Response string
|
||||
Timestamp string
|
||||
}
|
||||
|
||||
// handleHistoryPage обрабатывает страницу истории запросов
|
||||
func handleHistoryPage(w http.ResponseWriter, r *http.Request) {
|
||||
historyEntries, err := readHistoryEntries()
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Ошибка чтения истории: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
tmpl := templates.HistoryPageTemplate
|
||||
|
||||
t, err := template.New("history").Parse(tmpl)
|
||||
if err != nil {
|
||||
http.Error(w, "Ошибка шаблона", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
data := struct {
|
||||
Entries []HistoryEntryInfo
|
||||
}{
|
||||
Entries: historyEntries,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
t.Execute(w, data)
|
||||
}
|
||||
|
||||
// readHistoryEntries читает записи истории
|
||||
func readHistoryEntries() ([]HistoryEntryInfo, error) {
|
||||
entries, err := Read(config.AppConfig.ResultHistory)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result []HistoryEntryInfo
|
||||
for _, entry := range entries {
|
||||
result = append(result, HistoryEntryInfo{
|
||||
Index: entry.Index,
|
||||
Command: entry.Command,
|
||||
Response: entry.Response,
|
||||
Timestamp: entry.Timestamp.Format("02.01.2006 15:04:05"),
|
||||
})
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// handleDeleteHistoryEntry обрабатывает удаление записи истории
|
||||
func handleDeleteHistoryEntry(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "DELETE" {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
indexStr := strings.TrimPrefix(r.URL.Path, "/history/delete/")
|
||||
index, err := strconv.Atoi(indexStr)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid index", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err = DeleteHistoryEntry(config.AppConfig.ResultHistory, index)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Ошибка удаления: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("Запись успешно удалена"))
|
||||
}
|
||||
|
||||
// handleClearHistory обрабатывает очистку всей истории
|
||||
func handleClearHistory(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "DELETE" {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
err := os.WriteFile(config.AppConfig.ResultHistory, []byte("[]"), 0644)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Ошибка очистки: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("История успешно очищена"))
|
||||
}
|
||||
|
||||
// handleHistoryView обрабатывает просмотр записи истории
|
||||
func handleHistoryView(w http.ResponseWriter, r *http.Request) {
|
||||
// Получаем индекс из URL
|
||||
indexStr := strings.TrimPrefix(r.URL.Path, "/history/view/")
|
||||
index, err := strconv.Atoi(indexStr)
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Читаем записи истории
|
||||
entries, err := Read(config.AppConfig.ResultHistory)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Ошибка чтения истории: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Ищем запись с нужным индексом
|
||||
var targetEntry *HistoryEntry
|
||||
for _, entry := range entries {
|
||||
if entry.Index == index {
|
||||
targetEntry = &entry
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if targetEntry == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Формируем объяснение, если оно есть
|
||||
explanationSection := ""
|
||||
if strings.TrimSpace(targetEntry.Explanation) != "" {
|
||||
// Конвертируем Markdown в HTML
|
||||
explanationHTML := blackfriday.Run([]byte(targetEntry.Explanation))
|
||||
explanationSection = fmt.Sprintf(`
|
||||
<div class="history-explanation">
|
||||
<h3>📖 Подробное объяснение:</h3>
|
||||
<div class="history-explanation-content">%s</div>
|
||||
</div>`, string(explanationHTML))
|
||||
}
|
||||
|
||||
// Создаем HTML страницу
|
||||
htmlPage := fmt.Sprintf(templates.HistoryViewTemplate,
|
||||
index, // title
|
||||
index, // header
|
||||
targetEntry.Timestamp.Format("02.01.2006 15:04:05"), // timestamp
|
||||
index, // meta index
|
||||
targetEntry.Command, // command
|
||||
targetEntry.Response, // response
|
||||
explanationSection, // explanation (if exists)
|
||||
index, // delete button index
|
||||
)
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.Write([]byte(htmlPage))
|
||||
}
|
||||
67
serve/history_utils.go
Normal file
67
serve/history_utils.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package serve
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
// HistoryEntry представляет запись в истории
|
||||
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"`
|
||||
}
|
||||
|
||||
// read читает записи истории из файла
|
||||
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
|
||||
}
|
||||
|
||||
// write записывает записи истории в файл
|
||||
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)
|
||||
}
|
||||
|
||||
// DeleteHistoryEntry удаляет запись из истории по индексу
|
||||
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)
|
||||
}
|
||||
421
serve/prompts.go
Normal file
421
serve/prompts.go
Normal file
@@ -0,0 +1,421 @@
|
||||
package serve
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/direct-dev-ru/linux-command-gpt/gpt"
|
||||
"github.com/direct-dev-ru/linux-command-gpt/serve/templates"
|
||||
)
|
||||
|
||||
// VerbosePrompt структура для промптов подробности
|
||||
type VerbosePrompt struct {
|
||||
Mode string
|
||||
Name string
|
||||
Description string
|
||||
Content string
|
||||
IsDefault bool
|
||||
}
|
||||
|
||||
// handlePromptsPage обрабатывает страницу управления промптами
|
||||
func handlePromptsPage(w http.ResponseWriter, r *http.Request) {
|
||||
// Получаем домашнюю директорию пользователя
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
http.Error(w, "Ошибка получения домашней директории", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Создаем менеджер промптов (использует конфигурацию из config.AppConfig.PromptFolder)
|
||||
pm := gpt.NewPromptManager(homeDir)
|
||||
|
||||
// Получаем язык из параметра запроса, если не указан - берем из файла
|
||||
lang := r.URL.Query().Get("lang")
|
||||
if lang == "" {
|
||||
lang = pm.GetCurrentLanguage()
|
||||
}
|
||||
|
||||
tmpl := templates.PromptsPageTemplate
|
||||
|
||||
t, err := template.New("prompts").Parse(tmpl)
|
||||
if err != nil {
|
||||
http.Error(w, "Ошибка шаблона", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Создаем структуру с дополнительным полем IsDefault
|
||||
type PromptWithDefault struct {
|
||||
gpt.SystemPrompt
|
||||
IsDefault bool
|
||||
}
|
||||
|
||||
// Получаем текущий язык из файла
|
||||
currentLang := pm.GetCurrentLanguage()
|
||||
|
||||
// Если язык не указан в URL, используем язык из файла
|
||||
if lang == "" {
|
||||
lang = currentLang
|
||||
}
|
||||
|
||||
// Получаем системные промпты с учетом языка
|
||||
systemPrompts := getSystemPromptsWithLang(pm.Prompts, lang)
|
||||
|
||||
var promptsWithDefault []PromptWithDefault
|
||||
for _, prompt := range systemPrompts {
|
||||
// Показываем только системные промпты (ID 1-5) на первой вкладке
|
||||
if prompt.ID >= 1 && prompt.ID <= 5 {
|
||||
// Проверяем, является ли промпт встроенным и неизмененным
|
||||
isDefault := gpt.IsBuiltinPrompt(prompt)
|
||||
promptsWithDefault = append(promptsWithDefault, PromptWithDefault{
|
||||
SystemPrompt: prompt,
|
||||
IsDefault: isDefault,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Получаем промпты подробности из файла sys_prompts
|
||||
verbosePrompts := getVerbosePromptsFromFile(pm.Prompts, lang)
|
||||
|
||||
data := struct {
|
||||
Prompts []PromptWithDefault
|
||||
VerbosePrompts []VerbosePrompt
|
||||
Lang string
|
||||
}{
|
||||
Prompts: promptsWithDefault,
|
||||
VerbosePrompts: verbosePrompts,
|
||||
Lang: lang,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
t.Execute(w, data)
|
||||
}
|
||||
|
||||
// handleAddPrompt обрабатывает добавление нового промпта
|
||||
func handleAddPrompt(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем домашнюю директорию пользователя
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
http.Error(w, "Ошибка получения домашней директории", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Создаем менеджер промптов (использует конфигурацию из config.AppConfig.PromptFolder)
|
||||
pm := gpt.NewPromptManager(homeDir)
|
||||
|
||||
// Парсим JSON данные
|
||||
var promptData struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&promptData); err != nil {
|
||||
http.Error(w, "Ошибка парсинга JSON", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Добавляем промпт
|
||||
if err := pm.AddPrompt(promptData.Name, promptData.Description, promptData.Content); err != nil {
|
||||
http.Error(w, fmt.Sprintf("Ошибка добавления промпта: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("Промпт успешно добавлен"))
|
||||
}
|
||||
|
||||
// handleEditPrompt обрабатывает редактирование промпта
|
||||
func handleEditPrompt(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "PUT" {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем ID из URL
|
||||
idStr := strings.TrimPrefix(r.URL.Path, "/prompts/edit/")
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
http.Error(w, "Неверный ID промпта", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем домашнюю директорию пользователя
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
http.Error(w, "Ошибка получения домашней директории", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Создаем менеджер промптов (использует конфигурацию из config.AppConfig.PromptFolder)
|
||||
pm := gpt.NewPromptManager(homeDir)
|
||||
|
||||
// Парсим JSON данные
|
||||
var promptData struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&promptData); err != nil {
|
||||
http.Error(w, "Ошибка парсинга JSON", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Обновляем промпт
|
||||
if err := pm.UpdatePrompt(id, promptData.Name, promptData.Description, promptData.Content); err != nil {
|
||||
http.Error(w, fmt.Sprintf("Ошибка обновления промпта: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("Промпт успешно обновлен"))
|
||||
}
|
||||
|
||||
// handleDeletePrompt обрабатывает удаление промпта
|
||||
func handleDeletePrompt(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "DELETE" {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем ID из URL
|
||||
idStr := strings.TrimPrefix(r.URL.Path, "/prompts/delete/")
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
http.Error(w, "Неверный ID промпта", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем домашнюю директорию пользователя
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
http.Error(w, "Ошибка получения домашней директории", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Создаем менеджер промптов (использует конфигурацию из config.AppConfig.PromptFolder)
|
||||
pm := gpt.NewPromptManager(homeDir)
|
||||
|
||||
// Удаляем промпт
|
||||
if err := pm.DeletePrompt(id); err != nil {
|
||||
http.Error(w, fmt.Sprintf("Ошибка удаления промпта: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("Промпт успешно удален"))
|
||||
}
|
||||
|
||||
// handleRestorePrompt восстанавливает системный промпт к значению по умолчанию
|
||||
func handleRestorePrompt(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем ID из URL
|
||||
idStr := strings.TrimPrefix(r.URL.Path, "/prompts/restore/")
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid prompt ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем домашнюю директорию пользователя
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
http.Error(w, "Ошибка получения домашней директории", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Создаем менеджер промптов
|
||||
pm := gpt.NewPromptManager(homeDir)
|
||||
|
||||
// Получаем текущий язык
|
||||
currentLang := pm.GetCurrentLanguage()
|
||||
|
||||
// Получаем встроенный промпт для текущего языка
|
||||
builtinPrompt := gpt.GetBuiltinPromptByIDAndLanguage(id, currentLang)
|
||||
if builtinPrompt == nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": false,
|
||||
"error": "Промпт не найден в встроенных",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Обновляем промпт в списке
|
||||
for i, prompt := range pm.Prompts {
|
||||
if prompt.ID == id {
|
||||
pm.Prompts[i] = *builtinPrompt
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Сохраняем изменения
|
||||
if err := pm.SaveAllPrompts(); err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": false,
|
||||
"error": "Ошибка сохранения: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": true,
|
||||
})
|
||||
}
|
||||
|
||||
// handleRestoreVerbosePrompt восстанавливает verbose промпт к значению по умолчанию
|
||||
func handleRestoreVerbosePrompt(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем режим из URL
|
||||
mode := strings.TrimPrefix(r.URL.Path, "/prompts/restore-verbose/")
|
||||
|
||||
// Получаем домашнюю директорию пользователя
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
http.Error(w, "Ошибка получения домашней директории", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Создаем менеджер промптов
|
||||
pm := gpt.NewPromptManager(homeDir)
|
||||
|
||||
// Получаем текущий язык
|
||||
currentLang := pm.GetCurrentLanguage()
|
||||
|
||||
// Определяем ID по режиму
|
||||
var id int
|
||||
switch mode {
|
||||
case "v":
|
||||
id = 6
|
||||
case "vv":
|
||||
id = 7
|
||||
case "vvv":
|
||||
id = 8
|
||||
default:
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": false,
|
||||
"error": "Неверный режим промпта",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем встроенный промпт для текущего языка
|
||||
builtinPrompt := gpt.GetBuiltinPromptByIDAndLanguage(id, currentLang)
|
||||
if builtinPrompt == nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": false,
|
||||
"error": "Промпт не найден в встроенных",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Обновляем промпт в списке
|
||||
for i, prompt := range pm.Prompts {
|
||||
if prompt.ID == id {
|
||||
pm.Prompts[i] = *builtinPrompt
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Сохраняем изменения
|
||||
if err := pm.SaveAllPrompts(); err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": false,
|
||||
"error": "Ошибка сохранения: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": true,
|
||||
})
|
||||
}
|
||||
|
||||
// handleSaveLang обрабатывает сохранение промптов при переключении языка
|
||||
func handleSaveLang(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем домашнюю директорию пользователя
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
http.Error(w, "Ошибка получения домашней директории", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Создаем менеджер промптов
|
||||
pm := gpt.NewPromptManager(homeDir)
|
||||
|
||||
// Парсим JSON данные
|
||||
var langData struct {
|
||||
Lang string `json:"lang"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&langData); err != nil {
|
||||
http.Error(w, "Ошибка парсинга JSON", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Устанавливаем язык файла
|
||||
pm.SetLanguage(langData.Lang)
|
||||
|
||||
// Переводим только встроенные промпты (по ID), а пользовательские оставляем как есть
|
||||
var translatedPrompts []gpt.SystemPrompt
|
||||
for _, p := range pm.Prompts {
|
||||
// Проверяем, является ли промпт встроенным по ID (1-8)
|
||||
if pm.IsDefaultPromptByID(p) {
|
||||
// System (1-5) и Verbose (6-8)
|
||||
if p.ID >= 1 && p.ID <= 5 {
|
||||
translatedPrompts = append(translatedPrompts, translateSystemPrompt(p, langData.Lang))
|
||||
} else if p.ID >= 6 && p.ID <= 8 {
|
||||
translatedPrompts = append(translatedPrompts, translateVerbosePrompt(p, langData.Lang))
|
||||
} else {
|
||||
translatedPrompts = append(translatedPrompts, p)
|
||||
}
|
||||
} else {
|
||||
// Пользовательские промпты (ID > 8) не трогаем
|
||||
translatedPrompts = append(translatedPrompts, p)
|
||||
}
|
||||
}
|
||||
|
||||
// Обновляем в pm и сохраняем
|
||||
pm.Prompts = translatedPrompts
|
||||
if err := pm.SaveAllPrompts(); err != nil {
|
||||
http.Error(w, fmt.Sprintf("Ошибка сохранения: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("Промпты сохранены"))
|
||||
}
|
||||
147
serve/prompts_helpers.go
Normal file
147
serve/prompts_helpers.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package serve
|
||||
|
||||
import (
|
||||
"github.com/direct-dev-ru/linux-command-gpt/gpt"
|
||||
)
|
||||
|
||||
// getVerbosePromptsFromFile возвращает промпты подробности из файла sys_prompts
|
||||
func getVerbosePromptsFromFile(prompts []gpt.SystemPrompt, lang string) []VerbosePrompt {
|
||||
var verbosePrompts []VerbosePrompt
|
||||
|
||||
// Ищем промпты подробности в загруженных промптах (ID 6, 7, 8)
|
||||
for _, prompt := range prompts {
|
||||
if prompt.ID >= 6 && prompt.ID <= 8 {
|
||||
// Определяем режим по ID
|
||||
var mode string
|
||||
switch prompt.ID {
|
||||
case 6:
|
||||
mode = "v"
|
||||
case 7:
|
||||
mode = "vv"
|
||||
case 8:
|
||||
mode = "vvv"
|
||||
}
|
||||
|
||||
// Переводим на нужный язык если необходимо
|
||||
translatedPrompt := translateVerbosePrompt(prompt, lang)
|
||||
|
||||
verbosePrompts = append(verbosePrompts, VerbosePrompt{
|
||||
Mode: mode,
|
||||
Name: translatedPrompt.Name,
|
||||
Description: translatedPrompt.Description,
|
||||
Content: translatedPrompt.Content,
|
||||
IsDefault: gpt.IsBuiltinPrompt(translatedPrompt), // Проверяем, является ли промпт встроенным
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Если промпты подробности не найдены в файле, используем встроенные
|
||||
if len(verbosePrompts) == 0 {
|
||||
return getVerbosePrompts(lang)
|
||||
}
|
||||
|
||||
return verbosePrompts
|
||||
}
|
||||
|
||||
// translateVerbosePrompt переводит промпт подробности на указанный язык
|
||||
func translateVerbosePrompt(prompt gpt.SystemPrompt, lang string) gpt.SystemPrompt {
|
||||
// Получаем встроенный промпт для указанного языка из YAML
|
||||
if builtinPrompt := gpt.GetBuiltinPromptByIDAndLanguage(prompt.ID, lang); builtinPrompt != nil {
|
||||
return *builtinPrompt
|
||||
}
|
||||
|
||||
// Если перевод не найден, возвращаем оригинал
|
||||
return prompt
|
||||
}
|
||||
|
||||
// getVerbosePrompts возвращает промпты для режимов v/vv/vvv (fallback)
|
||||
func getVerbosePrompts(lang string) []VerbosePrompt {
|
||||
// Английские версии (по умолчанию)
|
||||
enPrompts := []VerbosePrompt{
|
||||
{
|
||||
Mode: "v",
|
||||
Name: "Verbose Mode",
|
||||
Description: "Detailed explanation of the command",
|
||||
Content: "Provide a brief explanation of what this Linux command does, including what each flag and option means, and give examples of usage.",
|
||||
IsDefault: true,
|
||||
},
|
||||
{
|
||||
Mode: "vv",
|
||||
Name: "Very Verbose Mode",
|
||||
Description: "Comprehensive explanation with alternatives",
|
||||
Content: "Provide a comprehensive explanation of this Linux command, including detailed descriptions of all flags and options, alternative approaches, common use cases, and potential pitfalls to avoid.",
|
||||
IsDefault: true,
|
||||
},
|
||||
{
|
||||
Mode: "vvv",
|
||||
Name: "Maximum Verbose Mode",
|
||||
Description: "Complete guide with examples and best practices",
|
||||
Content: "Provide a complete guide for this Linux command, including detailed explanations of all options, multiple examples with different scenarios, alternative commands that achieve similar results, best practices, troubleshooting tips, and related commands that work well together.",
|
||||
IsDefault: true,
|
||||
},
|
||||
}
|
||||
|
||||
// Русские версии
|
||||
ruPrompts := []VerbosePrompt{
|
||||
{
|
||||
Mode: "v",
|
||||
Name: "Подробный режим",
|
||||
Description: "Подробное объяснение команды",
|
||||
Content: "Предоставь краткое объяснение того, что делает эта Linux команда, включая значение каждого флага и опции, и приведи примеры использования.",
|
||||
IsDefault: true,
|
||||
},
|
||||
{
|
||||
Mode: "vv",
|
||||
Name: "Очень подробный режим",
|
||||
Description: "Исчерпывающее объяснение с альтернативами",
|
||||
Content: "Предоставь исчерпывающее объяснение этой Linux команды, включая подробные описания всех флагов и опций, альтернативные подходы, распространенные случаи использования и потенциальные подводные камни, которых следует избегать.",
|
||||
IsDefault: true,
|
||||
},
|
||||
{
|
||||
Mode: "vvv",
|
||||
Name: "Максимально подробный режим",
|
||||
Description: "Полное руководство с примерами и лучшими практиками",
|
||||
Content: "Предоставь полное руководство по этой Linux команде, включая подробные объяснения всех опций, множественные примеры с различными сценариями, альтернативные команды, которые дают аналогичные результаты, лучшие практики, советы по устранению неполадок и связанные команды, которые хорошо работают вместе.",
|
||||
IsDefault: true,
|
||||
},
|
||||
}
|
||||
|
||||
if lang == "ru" {
|
||||
return ruPrompts
|
||||
}
|
||||
return enPrompts
|
||||
}
|
||||
|
||||
// getSystemPromptsWithLang возвращает системные промпты с учетом языка
|
||||
func getSystemPromptsWithLang(prompts []gpt.SystemPrompt, lang string) []gpt.SystemPrompt {
|
||||
// Если язык английский, возвращаем оригинальные промпты
|
||||
if lang == "en" {
|
||||
return prompts
|
||||
}
|
||||
|
||||
// Для русского языка переводим только встроенные промпты
|
||||
var translatedPrompts []gpt.SystemPrompt
|
||||
for _, prompt := range prompts {
|
||||
// Проверяем, является ли это встроенным промптом
|
||||
if gpt.IsBuiltinPrompt(prompt) {
|
||||
// Переводим встроенные промпты на русский
|
||||
translated := translateSystemPrompt(prompt, lang)
|
||||
translatedPrompts = append(translatedPrompts, translated)
|
||||
} else {
|
||||
translatedPrompts = append(translatedPrompts, prompt)
|
||||
}
|
||||
}
|
||||
|
||||
return translatedPrompts
|
||||
}
|
||||
|
||||
// translateSystemPrompt переводит системный промпт на указанный язык
|
||||
func translateSystemPrompt(prompt gpt.SystemPrompt, lang string) gpt.SystemPrompt {
|
||||
// Получаем встроенный промпт для указанного языка из YAML
|
||||
if builtinPrompt := gpt.GetBuiltinPromptByIDAndLanguage(prompt.ID, lang); builtinPrompt != nil {
|
||||
return *builtinPrompt
|
||||
}
|
||||
|
||||
// Если перевод не найден, возвращаем оригинал
|
||||
return prompt
|
||||
}
|
||||
239
serve/results.go
Normal file
239
serve/results.go
Normal file
@@ -0,0 +1,239 @@
|
||||
package serve
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||
"github.com/direct-dev-ru/linux-command-gpt/serve/templates"
|
||||
"github.com/russross/blackfriday/v2"
|
||||
)
|
||||
|
||||
// FileInfo содержит информацию о файле
|
||||
type FileInfo struct {
|
||||
Name string
|
||||
Size string
|
||||
ModTime string
|
||||
Preview string
|
||||
Content string // Полное содержимое для поиска
|
||||
}
|
||||
|
||||
// handleResultsPage обрабатывает главную страницу со списком файлов
|
||||
func handleResultsPage(w http.ResponseWriter, r *http.Request) {
|
||||
files, err := getResultFiles()
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Ошибка чтения папки: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
tmpl := templates.ResultsPageTemplate
|
||||
|
||||
t, err := template.New("results").Parse(tmpl)
|
||||
if err != nil {
|
||||
http.Error(w, "Ошибка шаблона", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Подсчитываем статистику
|
||||
recentCount := 0
|
||||
weekAgo := time.Now().AddDate(0, 0, -7)
|
||||
for _, file := range files {
|
||||
// Парсим время из строки для сравнения
|
||||
if modTime, err := time.Parse("02.01.2006 15:04", file.ModTime); err == nil {
|
||||
if modTime.After(weekAgo) {
|
||||
recentCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data := struct {
|
||||
Files []FileInfo
|
||||
TotalFiles int
|
||||
RecentFiles int
|
||||
}{
|
||||
Files: files,
|
||||
TotalFiles: len(files),
|
||||
RecentFiles: recentCount,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
t.Execute(w, data)
|
||||
}
|
||||
|
||||
// getResultFiles возвращает список файлов из папки результатов
|
||||
func getResultFiles() ([]FileInfo, error) {
|
||||
entries, err := os.ReadDir(config.AppConfig.ResultFolder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var files []FileInfo
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".md") {
|
||||
continue
|
||||
}
|
||||
|
||||
info, err := entry.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Читаем превью файла (первые 200 символов) и конвертируем Markdown
|
||||
preview := ""
|
||||
fullContent := ""
|
||||
if content, err := os.ReadFile(filepath.Join(config.AppConfig.ResultFolder, entry.Name())); err == nil {
|
||||
// Сохраняем полное содержимое для поиска
|
||||
fullContent = string(content)
|
||||
// Конвертируем Markdown в HTML для превью
|
||||
htmlContent := blackfriday.Run(content)
|
||||
preview = strings.TrimSpace(string(htmlContent))
|
||||
// Удаляем HTML теги для превью
|
||||
preview = strings.ReplaceAll(preview, "<h1>", "")
|
||||
preview = strings.ReplaceAll(preview, "</h1>", "")
|
||||
preview = strings.ReplaceAll(preview, "<h2>", "")
|
||||
preview = strings.ReplaceAll(preview, "</h2>", "")
|
||||
preview = strings.ReplaceAll(preview, "<h3>", "")
|
||||
preview = strings.ReplaceAll(preview, "</h3>", "")
|
||||
preview = strings.ReplaceAll(preview, "<p>", "")
|
||||
preview = strings.ReplaceAll(preview, "</p>", "")
|
||||
preview = strings.ReplaceAll(preview, "<code>", "")
|
||||
preview = strings.ReplaceAll(preview, "</code>", "")
|
||||
preview = strings.ReplaceAll(preview, "<pre>", "")
|
||||
preview = strings.ReplaceAll(preview, "</pre>", "")
|
||||
preview = strings.ReplaceAll(preview, "<strong>", "")
|
||||
preview = strings.ReplaceAll(preview, "</strong>", "")
|
||||
preview = strings.ReplaceAll(preview, "<em>", "")
|
||||
preview = strings.ReplaceAll(preview, "</em>", "")
|
||||
preview = strings.ReplaceAll(preview, "<ul>", "")
|
||||
preview = strings.ReplaceAll(preview, "</ul>", "")
|
||||
preview = strings.ReplaceAll(preview, "<li>", "• ")
|
||||
preview = strings.ReplaceAll(preview, "</li>", "")
|
||||
preview = strings.ReplaceAll(preview, "<ol>", "")
|
||||
preview = strings.ReplaceAll(preview, "</ol>", "")
|
||||
preview = strings.ReplaceAll(preview, "<blockquote>", "")
|
||||
preview = strings.ReplaceAll(preview, "</blockquote>", "")
|
||||
preview = strings.ReplaceAll(preview, "<br>", "")
|
||||
preview = strings.ReplaceAll(preview, "<br/>", "")
|
||||
preview = strings.ReplaceAll(preview, "<br />", "")
|
||||
|
||||
// Очищаем от лишних пробелов и переносов
|
||||
preview = strings.ReplaceAll(preview, "\n", " ")
|
||||
preview = strings.ReplaceAll(preview, "\r", "")
|
||||
preview = strings.ReplaceAll(preview, " ", " ")
|
||||
preview = strings.TrimSpace(preview)
|
||||
|
||||
if len(preview) > 200 {
|
||||
preview = preview[:200] + "..."
|
||||
}
|
||||
}
|
||||
|
||||
files = append(files, FileInfo{
|
||||
Name: entry.Name(),
|
||||
Size: formatFileSize(info.Size()),
|
||||
ModTime: info.ModTime().Format("02.01.2006 15:04"),
|
||||
Preview: preview,
|
||||
Content: fullContent,
|
||||
})
|
||||
}
|
||||
|
||||
// Сортируем по времени изменения (новые сверху)
|
||||
for i := 0; i < len(files)-1; i++ {
|
||||
for j := i + 1; j < len(files); j++ {
|
||||
if files[i].ModTime < files[j].ModTime {
|
||||
files[i], files[j] = files[j], files[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
||||
// formatFileSize форматирует размер файла в читаемый вид
|
||||
func formatFileSize(size int64) string {
|
||||
const unit = 1024
|
||||
if size < unit {
|
||||
return fmt.Sprintf("%d B", size)
|
||||
}
|
||||
div, exp := int64(unit), 0
|
||||
for n := size / unit; n >= unit; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
return fmt.Sprintf("%.1f %cB", float64(size)/float64(div), "KMGTPE"[exp])
|
||||
}
|
||||
|
||||
// handleFileView обрабатывает просмотр конкретного файла
|
||||
func handleFileView(w http.ResponseWriter, r *http.Request) {
|
||||
filename := strings.TrimPrefix(r.URL.Path, "/file/")
|
||||
if filename == "" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Проверяем, что файл существует и находится в папке результатов
|
||||
filePath := filepath.Join(config.AppConfig.ResultFolder, filename)
|
||||
if !strings.HasPrefix(filePath, config.AppConfig.ResultFolder) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Конвертируем Markdown в HTML
|
||||
htmlContent := blackfriday.Run(content)
|
||||
|
||||
// Создаем HTML страницу с красивым отображением
|
||||
htmlPage := fmt.Sprintf(templates.FileViewTemplate, filename, filename, string(htmlContent))
|
||||
|
||||
// Устанавливаем заголовки для отображения HTML
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.Write([]byte(htmlPage))
|
||||
}
|
||||
|
||||
// handleDeleteFile обрабатывает удаление файла
|
||||
func handleDeleteFile(w http.ResponseWriter, r *http.Request) {
|
||||
// Проверяем метод запроса
|
||||
if r.Method != "DELETE" {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
filename := strings.TrimPrefix(r.URL.Path, "/delete/")
|
||||
if filename == "" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Проверяем, что файл существует и находится в папке результатов
|
||||
filePath := filepath.Join(config.AppConfig.ResultFolder, filename)
|
||||
if !strings.HasPrefix(filePath, config.AppConfig.ResultFolder) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Проверяем, что файл существует
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Удаляем файл
|
||||
err := os.Remove(filePath)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Ошибка удаления файла: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Возвращаем успешный ответ
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("Файл успешно удален"))
|
||||
}
|
||||
59
serve/serve.go
Normal file
59
serve/serve.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package serve
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||
)
|
||||
|
||||
// StartResultServer запускает HTTP сервер для просмотра сохраненных результатов
|
||||
func StartResultServer(host, port string) error {
|
||||
// Регистрируем все маршруты
|
||||
registerRoutes()
|
||||
|
||||
addr := fmt.Sprintf("%s:%s", host, port)
|
||||
fmt.Printf("Сервер запущен на http://%s\n", addr)
|
||||
fmt.Println("Нажмите Ctrl+C для остановки")
|
||||
|
||||
// Тестовое логирование для проверки debug флага
|
||||
if config.AppConfig.MainFlags.Debug {
|
||||
fmt.Printf("🔍 DEBUG РЕЖИМ ВКЛЮЧЕН - веб-операции будут логироваться\n")
|
||||
} else {
|
||||
fmt.Printf("🔍 DEBUG РЕЖИМ ОТКЛЮЧЕН - веб-операции не будут логироваться\n")
|
||||
}
|
||||
|
||||
return http.ListenAndServe(addr, nil)
|
||||
}
|
||||
|
||||
// registerRoutes регистрирует все маршруты сервера
|
||||
func registerRoutes() {
|
||||
// Главная страница и файлы
|
||||
http.HandleFunc("/", handleResultsPage)
|
||||
http.HandleFunc("/file/", handleFileView)
|
||||
http.HandleFunc("/delete/", handleDeleteFile)
|
||||
|
||||
// История запросов
|
||||
http.HandleFunc("/history", handleHistoryPage)
|
||||
http.HandleFunc("/history/view/", handleHistoryView)
|
||||
http.HandleFunc("/history/delete/", handleDeleteHistoryEntry)
|
||||
http.HandleFunc("/history/clear", handleClearHistory)
|
||||
|
||||
// Управление промптами
|
||||
http.HandleFunc("/prompts", handlePromptsPage)
|
||||
http.HandleFunc("/prompts/add", handleAddPrompt)
|
||||
http.HandleFunc("/prompts/edit/", handleEditPrompt)
|
||||
http.HandleFunc("/prompts/delete/", handleDeletePrompt)
|
||||
http.HandleFunc("/prompts/restore/", handleRestorePrompt)
|
||||
http.HandleFunc("/prompts/restore-verbose/", handleRestoreVerbosePrompt)
|
||||
http.HandleFunc("/prompts/save-lang", handleSaveLang)
|
||||
|
||||
// Веб-страница для выполнения запросов
|
||||
http.HandleFunc("/run", handleExecutePage)
|
||||
|
||||
// API для выполнения запросов
|
||||
http.HandleFunc("/api/execute", handleExecute)
|
||||
// API для сохранения результатов и истории
|
||||
http.HandleFunc("/api/save-result", handleSaveResult)
|
||||
http.HandleFunc("/api/add-to-history", handleAddToHistory)
|
||||
}
|
||||
600
serve/templates/execute.css.go
Normal file
600
serve/templates/execute.css.go
Normal file
@@ -0,0 +1,600 @@
|
||||
package templates
|
||||
|
||||
import "html/template"
|
||||
|
||||
// ExecutePageCSSTemplate - CSS стили для страницы выполнения запросов
|
||||
var ExecutePageCSSTemplate = template.Must(template.New("execute_css").Parse(`
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
/* Динамичный плавный градиент (современный стиль) */
|
||||
background: linear-gradient(135deg, #5b86e5, #36d1dc, #4a7c59, #764ba2);
|
||||
background-size: 400% 400%;
|
||||
animation: gradientShift 18s ease infinite;
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* Анимация плавного перелива фона */
|
||||
@keyframes gradientShift {
|
||||
0% { background-position: 0% 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
100% { background-position: 0% 50%; }
|
||||
}
|
||||
|
||||
/* Учитываем системные настройки доступности */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
body { animation: none; }
|
||||
}
|
||||
|
||||
/* Улучшения для touch-устройств */
|
||||
.nav-btn, .submit-btn, .reset-btn, .verbose-btn, .action-btn {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
/* Оптимизация производительности */
|
||||
.container {
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
/* Улучшение читаемости на мобильных */
|
||||
@media (max-width: 768px) {
|
||||
.command-result code {
|
||||
font-size: 0.9em;
|
||||
padding: 1px 4px;
|
||||
}
|
||||
.command-result pre {
|
||||
font-size: 14px;
|
||||
padding: 12px;
|
||||
}
|
||||
.explanation-content {
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
.header {
|
||||
background: linear-gradient(135deg, #2d5016 0%, #4a7c59 100%);
|
||||
color: white;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
.header h1 {
|
||||
font-size: 2.5em;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.header p {
|
||||
opacity: 0.9;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
.content {
|
||||
padding: 30px;
|
||||
}
|
||||
.nav-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 30px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.nav-btn {
|
||||
background: #3498db;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 1em;
|
||||
text-decoration: none;
|
||||
transition: background 0.3s ease;
|
||||
display: inline-block;
|
||||
}
|
||||
.nav-btn:hover {
|
||||
background: #2980b9;
|
||||
}
|
||||
.form-section {
|
||||
background: #f8f9fa;
|
||||
padding: 25px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
color: #2d5016;
|
||||
}
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
.form-group select:focus,
|
||||
.form-group textarea:focus {
|
||||
outline: none;
|
||||
border-color: #2d5016;
|
||||
box-shadow: 0 0 0 3px rgba(45, 80, 22, 0.1);
|
||||
}
|
||||
.form-group textarea {
|
||||
resize: vertical;
|
||||
min-height: 120px;
|
||||
}
|
||||
.submit-btn {
|
||||
background: linear-gradient(135deg, #2d5016 0%, #4a7c59 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 15px 30px;
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
width: 100%;
|
||||
}
|
||||
.submit-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 20px rgba(45, 80, 22, 0.3);
|
||||
}
|
||||
.submit-btn:disabled {
|
||||
background: #6c757d;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
.form-buttons {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.reset-btn {
|
||||
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 15px 30px;
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
flex: 1;
|
||||
}
|
||||
.reset-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 20px rgba(231, 76, 60, 0.3);
|
||||
}
|
||||
.reset-btn:disabled {
|
||||
background: #6c757d;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
.result-section {
|
||||
margin-top: 30px;
|
||||
}
|
||||
.command-result {
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #2d5016;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.command-result h3 {
|
||||
color: #2d5016;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
/* Заголовки внутри результата команды */
|
||||
.command-result h1,
|
||||
.command-result h2,
|
||||
.command-result h3,
|
||||
.command-result h4,
|
||||
.command-result h5,
|
||||
.command-result h6 {
|
||||
margin-top: 18px; /* отделяем сверху */
|
||||
margin-bottom: 10px;/* и немного снизу */
|
||||
line-height: 1.25;
|
||||
}
|
||||
/* Ритм текста внутри markdown-блока команды */
|
||||
.command-result .command-md { line-height: 1.7; }
|
||||
.command-result p { margin: 10px 0 14px; line-height: 1.7; }
|
||||
.command-result ul,
|
||||
.command-result ol { margin: 10px 0 14px 24px; line-height: 1.7; }
|
||||
.command-result li { margin: 6px 0; }
|
||||
.command-result hr { margin: 18px 0; border: 0; border-top: 1px solid #e1e5e9; }
|
||||
/* Подсветка code внутри результата команды */
|
||||
.command-result code {
|
||||
background: #e6f4ea; /* светло-зеленый фон */
|
||||
color: #2e7d32; /* зеленый текст */
|
||||
border: 1px solid #b7dfb9;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.98em; /* немного крупнее базового */
|
||||
}
|
||||
.command-result pre {
|
||||
background: #eaf7ee; /* мягкий зеленоватый фон */
|
||||
border-left: 4px solid #2e7d32; /* зеленая полоса слева */
|
||||
padding: 14px;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
margin: 12px 0 16px; /* вертикальные отступы вокруг кода */
|
||||
}
|
||||
.command-result pre code {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #2e7d32;
|
||||
font-size: 16px; /* увеличить размер шрифта в блоках */
|
||||
}
|
||||
.command-code {
|
||||
background: #2d5016;
|
||||
color: white;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: 16px;
|
||||
margin-bottom: 15px;
|
||||
word-break: break-all;
|
||||
}
|
||||
.result-meta {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
color: #6c757d;
|
||||
font-size: 14px;
|
||||
}
|
||||
.explanation-section {
|
||||
background: #f0f8f0;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #4a7c59;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.explanation-section h3 {
|
||||
color: #2d5016;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.explanation-content {
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.explanation-content h1,
|
||||
.explanation-content h2,
|
||||
.explanation-content h3,
|
||||
.explanation-content h4,
|
||||
.explanation-content h5,
|
||||
.explanation-content h6 {
|
||||
color: #2d5016;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.explanation-content h1 {
|
||||
border-bottom: 2px solid #2d5016;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
.explanation-content h2 {
|
||||
border-bottom: 1px solid #4a7c59;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
.explanation-content code {
|
||||
background: #f0f8f0;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
color: #2d5016;
|
||||
border: 1px solid #a8e6cf;
|
||||
}
|
||||
.explanation-content pre {
|
||||
background: #f0f8f0;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #2d5016;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.explanation-content pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
border: none;
|
||||
color: #2d5016;
|
||||
}
|
||||
.explanation-content blockquote {
|
||||
border-left: 4px solid #4a7c59;
|
||||
margin: 15px 0;
|
||||
padding: 10px 20px;
|
||||
background: #f0f8f0;
|
||||
border-radius: 0 8px 8px 0;
|
||||
}
|
||||
.explanation-content ul,
|
||||
.explanation-content ol {
|
||||
padding-left: 20px;
|
||||
}
|
||||
.explanation-content li {
|
||||
margin: 5px 0;
|
||||
}
|
||||
.explanation-content strong {
|
||||
color: #2d5016;
|
||||
}
|
||||
.explanation-content em {
|
||||
color: #4a7c59;
|
||||
}
|
||||
.verbose-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
.verbose-btn {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
color: #495057;
|
||||
padding: 10px 15px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 14px;
|
||||
}
|
||||
.v-btn {
|
||||
background: #e3f2fd;
|
||||
border: 1px solid #bbdefb;
|
||||
color: #1976d2;
|
||||
}
|
||||
.v-btn:hover {
|
||||
background: #bbdefb;
|
||||
border-color: #90caf9;
|
||||
}
|
||||
.v-btn:disabled {
|
||||
background: #f5f5f5;
|
||||
border-color: #e0e0e0;
|
||||
color: #9e9e9e;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.vv-btn {
|
||||
background: #e1f5fe;
|
||||
border: 1px solid #b3e5fc;
|
||||
color: #0277bd;
|
||||
}
|
||||
.vv-btn:hover {
|
||||
background: #b3e5fc;
|
||||
border-color: #81d4fa;
|
||||
}
|
||||
.vv-btn:disabled {
|
||||
background: #f5f5f5;
|
||||
border-color: #e0e0e0;
|
||||
color: #9e9e9e;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.vvv-btn {
|
||||
background: #e8eaf6;
|
||||
border: 1px solid #c5cae9;
|
||||
color: #3f51b5;
|
||||
}
|
||||
.vvv-btn:hover {
|
||||
background: #c5cae9;
|
||||
border-color: #9fa8da;
|
||||
}
|
||||
.vvv-btn:disabled {
|
||||
background: #f5f5f5;
|
||||
border-color: #e0e0e0;
|
||||
color: #9e9e9e;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.action-btn {
|
||||
background: #e8f5e8;
|
||||
border: 1px solid #c8e6c9;
|
||||
color: #2e7d32;
|
||||
padding: 10px 20px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
.action-btn:hover {
|
||||
background: #c8e6c9;
|
||||
border-color: #a5d6a7;
|
||||
color: #1b5e20;
|
||||
}
|
||||
.error-message {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #dc3545;
|
||||
}
|
||||
.error-message h3 {
|
||||
color: #721c24;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.loading {
|
||||
display: none;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
.loading.show {
|
||||
display: block;
|
||||
}
|
||||
.spinner {
|
||||
border: 3px solid #f3f3f3;
|
||||
border-top: 3px solid #2d5016;
|
||||
border-radius: 50%;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 10px;
|
||||
}
|
||||
.verbose-loading {
|
||||
display: none;
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.verbose-loading.show {
|
||||
display: block;
|
||||
}
|
||||
.verbose-spinner {
|
||||
border: 2px solid #f3f3f3;
|
||||
border-top: 2px solid #1976d2;
|
||||
border-radius: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 5px;
|
||||
}
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
.scroll-to-top {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
background: #3498db;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
||||
z-index: 1000;
|
||||
}
|
||||
.scroll-to-top:hover {
|
||||
background: #2980b9;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 12px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
/* Мобильная оптимизация */
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
padding: 10px;
|
||||
}
|
||||
.container {
|
||||
margin: 0;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
|
||||
}
|
||||
.header {
|
||||
padding: 20px;
|
||||
}
|
||||
.header h1 {
|
||||
font-size: 2em;
|
||||
}
|
||||
.content {
|
||||
padding: 20px;
|
||||
}
|
||||
.nav-buttons {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.nav-btn {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
.form-buttons {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.submit-btn, .reset-btn {
|
||||
width: 100%;
|
||||
padding: 16px 20px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.verbose-buttons {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.verbose-btn {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.action-buttons {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.action-btn {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.command-result {
|
||||
padding: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.command-code {
|
||||
font-size: 14px;
|
||||
padding: 12px;
|
||||
word-break: break-word;
|
||||
}
|
||||
.explanation-section {
|
||||
padding: 15px;
|
||||
}
|
||||
.result-meta {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.scroll-to-top {
|
||||
bottom: 15px;
|
||||
right: 15px;
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Очень маленькие экраны */
|
||||
@media (max-width: 480px) {
|
||||
.header h1 {
|
||||
font-size: 1.8em;
|
||||
}
|
||||
.header p {
|
||||
font-size: 1em;
|
||||
}
|
||||
.content {
|
||||
padding: 15px;
|
||||
}
|
||||
.form-group textarea {
|
||||
min-height: 100px;
|
||||
font-size: 16px; /* Предотвращает зум на iOS */
|
||||
}
|
||||
.form-group select {
|
||||
font-size: 16px; /* Предотвращает зум на iOS */
|
||||
}
|
||||
.command-result h3 {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
.explanation-content h1,
|
||||
.explanation-content h2,
|
||||
.explanation-content h3 {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
}
|
||||
`))
|
||||
88
serve/templates/execute.go
Normal file
88
serve/templates/execute.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package templates
|
||||
|
||||
import "html/template"
|
||||
|
||||
// ExecutePageTemplate - шаблон страницы выполнения запросов
|
||||
var ExecutePageTemplate = template.Must(template.New("execute").Parse(`<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{.Title}} - Linux Command GPT</title>
|
||||
<style>
|
||||
{{template "execute_css" .}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>{{.Header}}</h1>
|
||||
<p>Выполнение запросов к Linux Command GPT через веб-интерфейс</p>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="nav-buttons">
|
||||
<a href="/" class="nav-btn">🏠 Главная</a>
|
||||
<a href="/history" class="nav-btn">📝 История</a>
|
||||
<a href="/prompts" class="nav-btn">⚙️ Промпты</a>
|
||||
</div>
|
||||
|
||||
<form method="POST" id="executeForm">
|
||||
<div class="form-section">
|
||||
<div class="form-group">
|
||||
<label for="system_id">🤖 Системный промпт:</label>
|
||||
<select name="system_id" id="system_id" required>
|
||||
{{range .SystemOptions}}
|
||||
<option value="{{.ID}}">{{.ID}}. {{.Name}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="prompt">💬 Ваш запрос:</label>
|
||||
<textarea name="prompt" id="prompt" placeholder="Опишите, что вы хотите сделать..." required>{{.CurrentPrompt}}</textarea>
|
||||
</div>
|
||||
|
||||
<!-- Скрытое поле для хранения результатов -->
|
||||
<input type="hidden" id="resultData" name="resultData" value="">
|
||||
|
||||
<div class="form-buttons">
|
||||
<button type="submit" class="submit-btn" id="submitBtn">
|
||||
🚀 Выполнить запрос
|
||||
</button>
|
||||
<button type="button" class="reset-btn" id="resetBtn" onclick="resetForm()">
|
||||
🔄 Сброс
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="loading" id="loading">
|
||||
<div class="spinner"></div>
|
||||
<p>Обрабатываю запрос...</p>
|
||||
</div>
|
||||
|
||||
{{.ResultSection}}
|
||||
|
||||
{{.VerboseButtons}}
|
||||
|
||||
<div class="verbose-loading" id="verboseLoading">
|
||||
<div class="verbose-spinner"></div>
|
||||
<p>Получаю подробное объяснение...</p>
|
||||
</div>
|
||||
|
||||
{{.ActionButtons}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Кнопка "Наверх" -->
|
||||
<button class="scroll-to-top" id="scrollToTop" onclick="scrollToTop()" style="display: none;">↑</button>
|
||||
|
||||
{{template "execute_scripts" .}}
|
||||
</body>
|
||||
</html>`))
|
||||
|
||||
// Объединяем шаблоны
|
||||
func init() {
|
||||
template.Must(ExecutePageTemplate.AddParseTree("execute_css", ExecutePageCSSTemplate.Tree))
|
||||
template.Must(ExecutePageTemplate.AddParseTree("execute_scripts", ExecutePageScriptsTemplate.Tree))
|
||||
}
|
||||
284
serve/templates/execute.js.go
Normal file
284
serve/templates/execute.js.go
Normal file
@@ -0,0 +1,284 @@
|
||||
package templates
|
||||
|
||||
import "html/template"
|
||||
|
||||
var ExecutePageScriptsTemplate = template.Must(template.New("execute_scripts").Parse(`
|
||||
<script>
|
||||
// Обработка отправки формы (блокируем все кнопки кроме навигации)
|
||||
document.getElementById('executeForm').addEventListener('submit', function(e) {
|
||||
// Предотвращаем множественные отправки на мобильных устройствах
|
||||
if (this.dataset.submitting === 'true') {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
this.dataset.submitting = 'true';
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
const loading = document.getElementById('loading');
|
||||
const actionButtons = document.querySelectorAll('.action-btn');
|
||||
const verboseButtons = document.querySelectorAll('.verbose-btn');
|
||||
const scrollBtn = document.getElementById('scrollToTop');
|
||||
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = '⏳ Выполняется...';
|
||||
loading.classList.add('show');
|
||||
|
||||
// Блокируем кнопки действий (сохранение/история)
|
||||
actionButtons.forEach(btn => {
|
||||
btn.disabled = true;
|
||||
btn.style.opacity = '0.5';
|
||||
});
|
||||
|
||||
// Блокируем кнопки подробностей (v/vv/vvv)
|
||||
verboseButtons.forEach(btn => {
|
||||
btn.disabled = true;
|
||||
btn.style.opacity = '0.5';
|
||||
});
|
||||
|
||||
// Прячем кнопку "Наверх" до получения нового ответа
|
||||
if (scrollBtn) {
|
||||
scrollBtn.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Запрос подробного объяснения
|
||||
function requestExplanation(verbose) {
|
||||
const form = document.getElementById('executeForm');
|
||||
const prompt = document.getElementById('prompt').value;
|
||||
const systemId = document.getElementById('system_id').value;
|
||||
const verboseLoading = document.getElementById('verboseLoading');
|
||||
const verboseButtons = document.querySelectorAll('.verbose-btn');
|
||||
const actionButtons = document.querySelectorAll('.action-btn');
|
||||
|
||||
if (!prompt.trim()) {
|
||||
alert('Сначала выполните основной запрос');
|
||||
return;
|
||||
}
|
||||
|
||||
// Показываем лоадер и блокируем ВСЕ кнопки кроме навигации
|
||||
verboseLoading.classList.add('show');
|
||||
verboseButtons.forEach(btn => {
|
||||
btn.disabled = true;
|
||||
btn.style.opacity = '0.5';
|
||||
});
|
||||
actionButtons.forEach(btn => {
|
||||
btn.disabled = true;
|
||||
btn.style.opacity = '0.5';
|
||||
});
|
||||
|
||||
// Создаем скрытое поле для verbose
|
||||
const verboseInput = document.createElement('input');
|
||||
verboseInput.type = 'hidden';
|
||||
verboseInput.name = 'verbose';
|
||||
verboseInput.value = verbose;
|
||||
form.appendChild(verboseInput);
|
||||
|
||||
// Отправляем форму
|
||||
form.submit();
|
||||
}
|
||||
|
||||
// Сохранение результата
|
||||
function saveResult() {
|
||||
const resultDataField = document.getElementById('resultData');
|
||||
const prompt = document.getElementById('prompt').value;
|
||||
|
||||
if (!resultDataField.value || !prompt.trim()) {
|
||||
alert('Нет данных для сохранения');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const resultData = JSON.parse(resultDataField.value);
|
||||
const requestData = {
|
||||
prompt: prompt,
|
||||
command: resultData.command,
|
||||
explanation: resultData.explanation || '',
|
||||
model: resultData.model || 'Unknown'
|
||||
};
|
||||
|
||||
fetch('/api/save-result', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(requestData)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert('✅ Результат сохранен: ' + data.file);
|
||||
} else {
|
||||
alert('❌ Ошибка сохранения: ' + data.error);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('❌ Ошибка при сохранении результата');
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error parsing result data:', error);
|
||||
alert('❌ Ошибка при чтении данных результата');
|
||||
}
|
||||
}
|
||||
|
||||
// Добавление в историю
|
||||
function addToHistory() {
|
||||
const resultDataField = document.getElementById('resultData');
|
||||
const prompt = document.getElementById('prompt').value;
|
||||
const systemId = document.getElementById('system_id').value;
|
||||
|
||||
if (!resultDataField.value || !prompt.trim()) {
|
||||
alert('Нет данных для сохранения в историю');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const resultData = JSON.parse(resultDataField.value);
|
||||
const systemName = document.querySelector('option[value="' + systemId + '"]')?.textContent || 'Unknown';
|
||||
|
||||
const requestData = {
|
||||
prompt: prompt,
|
||||
command: resultData.command,
|
||||
response: resultData.command,
|
||||
explanation: resultData.explanation || '',
|
||||
system: systemName
|
||||
};
|
||||
|
||||
fetch('/api/add-to-history', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(requestData)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert('✅ ' + data.message);
|
||||
} else {
|
||||
alert('❌ Ошибка: ' + data.error);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('❌ Ошибка при добавлении в историю');
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error parsing result data:', error);
|
||||
alert('❌ Ошибка при чтении данных результата');
|
||||
}
|
||||
}
|
||||
|
||||
// Функция прокрутки наверх
|
||||
function scrollToTop() {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
|
||||
// Показываем кнопку "Наверх" при появлении объяснения
|
||||
function showScrollToTopButton() {
|
||||
const scrollBtn = document.getElementById('scrollToTop');
|
||||
if (scrollBtn) {
|
||||
scrollBtn.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
// Скрываем кнопку "Наверх" при загрузке страницы
|
||||
function hideScrollToTopButton() {
|
||||
const scrollBtn = document.getElementById('scrollToTop');
|
||||
if (scrollBtn) {
|
||||
scrollBtn.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Сброс формы к начальному состоянию
|
||||
function resetForm() {
|
||||
// Очищаем поля формы
|
||||
document.getElementById('prompt').value = '';
|
||||
document.getElementById('resultData').value = '';
|
||||
|
||||
// Сбрасываем флаг отправки формы
|
||||
const form = document.getElementById('executeForm');
|
||||
if (form) {
|
||||
form.dataset.submitting = 'false';
|
||||
}
|
||||
|
||||
// Скрываем все секции результатов
|
||||
const resultSection = document.querySelector('.result-section');
|
||||
const verboseButtons = document.querySelector('.verbose-buttons');
|
||||
const actionButtons = document.querySelector('.action-buttons');
|
||||
const explanationSection = document.querySelector('.explanation-section');
|
||||
const loading = document.getElementById('loading');
|
||||
const verboseLoading = document.getElementById('verboseLoading');
|
||||
const scrollBtn = document.getElementById('scrollToTop');
|
||||
|
||||
if (resultSection) resultSection.style.display = 'none';
|
||||
if (verboseButtons) verboseButtons.style.display = 'none';
|
||||
if (actionButtons) actionButtons.style.display = 'none';
|
||||
if (explanationSection) explanationSection.style.display = 'none';
|
||||
if (loading) loading.classList.remove('show');
|
||||
if (verboseLoading) verboseLoading.classList.remove('show');
|
||||
if (scrollBtn) scrollBtn.style.display = 'none';
|
||||
|
||||
// Разблокируем кнопки
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
const resetBtn = document.getElementById('resetBtn');
|
||||
const allButtons = document.querySelectorAll('.action-btn, .verbose-btn');
|
||||
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = '🚀 Выполнить запрос';
|
||||
}
|
||||
if (resetBtn) resetBtn.disabled = false;
|
||||
|
||||
allButtons.forEach(btn => {
|
||||
btn.disabled = false;
|
||||
btn.style.opacity = '1';
|
||||
});
|
||||
|
||||
// Прокручиваем к началу формы (с проверкой поддержки smooth)
|
||||
const formSection = document.querySelector('.form-section');
|
||||
if (formSection) {
|
||||
if ('scrollBehavior' in document.documentElement.style) {
|
||||
formSection.scrollIntoView({ behavior: 'smooth' });
|
||||
} else {
|
||||
formSection.scrollIntoView();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Сохранение результатов в скрытое поле
|
||||
function saveResultToHiddenField() {
|
||||
const resultDataField = document.getElementById('resultData');
|
||||
const commandElement = document.querySelector('.command-code, .command-md');
|
||||
const explanationElement = document.querySelector('.explanation-content');
|
||||
const modelElement = document.querySelector('.result-meta span:first-child');
|
||||
|
||||
if (commandElement) {
|
||||
const command = commandElement.textContent.trim();
|
||||
const explanation = explanationElement ? explanationElement.innerHTML.trim() : '';
|
||||
const model = modelElement ? modelElement.textContent.replace('Модель: ', '') : 'Unknown';
|
||||
|
||||
const resultData = {
|
||||
command: command,
|
||||
explanation: explanation,
|
||||
model: model
|
||||
};
|
||||
|
||||
resultDataField.value = JSON.stringify(resultData);
|
||||
}
|
||||
}
|
||||
|
||||
// Показываем кнопку при появлении объяснения и сохраняем результаты
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const explanationSection = document.querySelector('.explanation-section');
|
||||
if (explanationSection) {
|
||||
showScrollToTopButton();
|
||||
}
|
||||
|
||||
// Сохраняем результаты в скрытое поле при загрузке страницы
|
||||
saveResultToHiddenField();
|
||||
});
|
||||
</script>
|
||||
`))
|
||||
136
serve/templates/file.go
Normal file
136
serve/templates/file.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package templates
|
||||
|
||||
// FileViewTemplate шаблон для просмотра файла результата
|
||||
const FileViewTemplate = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>%s - LCG Results</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #56ab2f 0%%, #a8e6cf 100%%);
|
||||
min-height: 100vh;
|
||||
}
|
||||
.container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
.header {
|
||||
background: linear-gradient(135deg, #2d5016 0%%, #4a7c59 100%%);
|
||||
color: white;
|
||||
padding: 20px 30px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.header h1 {
|
||||
margin: 0;
|
||||
font-size: 1.5em;
|
||||
font-weight: 300;
|
||||
}
|
||||
.back-btn {
|
||||
background: rgba(255,255,255,0.2);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
.back-btn:hover {
|
||||
background: rgba(255,255,255,0.3);
|
||||
}
|
||||
.content {
|
||||
padding: 30px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.content h1 {
|
||||
color: #2d5016;
|
||||
border-bottom: 2px solid #2d5016;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.content h2 {
|
||||
color: #4a7c59;
|
||||
margin-top: 30px;
|
||||
}
|
||||
.content h3 {
|
||||
color: #2d5016;
|
||||
}
|
||||
.content code {
|
||||
background: #f0f8f0;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
color: #2d5016;
|
||||
border: 1px solid #a8e6cf;
|
||||
}
|
||||
.content pre {
|
||||
background: #f0f8f0;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #2d5016;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.content pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
border: none;
|
||||
color: #2d5016;
|
||||
}
|
||||
.content blockquote {
|
||||
border-left: 4px solid #4a7c59;
|
||||
margin: 20px 0;
|
||||
padding: 10px 20px;
|
||||
background: #f0f8f0;
|
||||
border-radius: 0 8px 8px 0;
|
||||
}
|
||||
.content ul, .content ol {
|
||||
padding-left: 20px;
|
||||
}
|
||||
.content li {
|
||||
margin: 5px 0;
|
||||
}
|
||||
.content strong {
|
||||
color: #2d5016;
|
||||
}
|
||||
.content em {
|
||||
color: #4a7c59;
|
||||
}
|
||||
|
||||
/* Мобильная адаптация */
|
||||
@media (max-width: 768px) {
|
||||
body { padding: 10px; }
|
||||
.container { margin: 0; border-radius: 8px; box-shadow: 0 10px 20px rgba(0,0,0,0.1); }
|
||||
.header { padding: 16px; }
|
||||
.header h1 { font-size: 1.2em; }
|
||||
.back-btn { padding: 6px 12px; font-size: 0.9em; }
|
||||
.content { padding: 20px; }
|
||||
.content pre { font-size: 14px; }
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.header h1 { font-size: 1em; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>📄 %s</h1>
|
||||
<a href="/" class="back-btn">← Назад к списку</a>
|
||||
</div>
|
||||
<div class="content">
|
||||
%s
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`
|
||||
309
serve/templates/history.go
Normal file
309
serve/templates/history.go
Normal file
@@ -0,0 +1,309 @@
|
||||
package templates
|
||||
|
||||
// HistoryPageTemplate шаблон страницы истории запросов
|
||||
const HistoryPageTemplate = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>История запросов - LCG Results</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #56ab2f 0%, #a8e6cf 100%);
|
||||
min-height: 100vh;
|
||||
}
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
.header {
|
||||
background: linear-gradient(135deg, #2d5016 0%, #4a7c59 100%);
|
||||
color: white;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
.header h1 {
|
||||
margin: 0;
|
||||
font-size: 2.5em;
|
||||
font-weight: 300;
|
||||
}
|
||||
.content {
|
||||
padding: 30px;
|
||||
}
|
||||
.nav-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.nav-btn {
|
||||
background: #3498db;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 1em;
|
||||
text-decoration: none;
|
||||
transition: background 0.3s ease;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
}
|
||||
.nav-btn:hover {
|
||||
background: #2980b9;
|
||||
}
|
||||
.clear-btn {
|
||||
background: #e74c3c;
|
||||
}
|
||||
.clear-btn:hover {
|
||||
background: #c0392b;
|
||||
}
|
||||
.history-item {
|
||||
background: #f0f8f0;
|
||||
border: 1px solid #a8e6cf;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 15px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.history-item:hover {
|
||||
border-color: #2d5016;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(45,80,22,0.2);
|
||||
}
|
||||
.history-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.history-index {
|
||||
background: #2d5016;
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.history-timestamp {
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.history-command {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.history-response {
|
||||
background: #f8f9fa;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: 0.9em;
|
||||
color: #2d5016;
|
||||
border-left: 3px solid #2d5016;
|
||||
}
|
||||
.delete-btn {
|
||||
background: #e74c3c;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.8em;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
.delete-btn:hover {
|
||||
background: #c0392b;
|
||||
}
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* Мобильная адаптация */
|
||||
@media (max-width: 768px) {
|
||||
body { padding: 10px; }
|
||||
.container { margin: 0; border-radius: 8px; box-shadow: 0 10px 20px rgba(0,0,0,0.1); }
|
||||
.header { padding: 20px; }
|
||||
.header h1 { font-size: 2em; }
|
||||
.content { padding: 20px; }
|
||||
.nav-buttons { flex-direction: column; gap: 8px; }
|
||||
.nav-btn { text-align: center; padding: 12px 16px; font-size: 14px; }
|
||||
.history-header { flex-direction: column; align-items: flex-start; gap: 8px; }
|
||||
.history-item { padding: 15px; }
|
||||
.history-response { font-size: 0.85em; }
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.header h1 { font-size: 1.8em; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>📝 История запросов</h1>
|
||||
<p>Управление историей запросов Linux Command GPT</p>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="nav-buttons">
|
||||
<a href="/" class="nav-btn">🏠 Главная</a>
|
||||
<a href="/run" class="nav-btn">🚀 Выполнение</a>
|
||||
<a href="/prompts" class="nav-btn">⚙️ Промпты</a>
|
||||
<button class="nav-btn clear-btn" onclick="clearHistory()">🗑️ Очистить всю историю</button>
|
||||
</div>
|
||||
|
||||
<!-- Поиск -->
|
||||
<div class="search-container" style="margin: 20px 0;">
|
||||
<input type="text" id="searchInput" placeholder="🔍 Поиск по командам, ответам и объяснениям..."
|
||||
style="width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 16px;">
|
||||
<div id="searchResults" style="margin-top: 10px; color: #666; font-size: 14px;"></div>
|
||||
</div>
|
||||
|
||||
{{if .Entries}}
|
||||
{{range .Entries}}
|
||||
<div class="history-item" onclick="viewHistoryEntry({{.Index}})">
|
||||
<div class="history-header">
|
||||
<div>
|
||||
<span class="history-index">#{{.Index}}</span>
|
||||
<span class="history-timestamp">{{.Timestamp}}</span>
|
||||
</div>
|
||||
<button class="delete-btn" onclick="event.stopPropagation(); deleteHistoryEntry({{.Index}})">🗑️ Удалить</button>
|
||||
</div>
|
||||
<div class="history-command">{{.Command}}</div>
|
||||
<div class="history-response">{{.Response}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<div class="empty-state">
|
||||
<h3>📝 История пуста</h3>
|
||||
<p>Здесь будут отображаться запросы после использования команды lcg</p>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function viewHistoryEntry(index) {
|
||||
window.location.href = '/history/view/' + index;
|
||||
}
|
||||
|
||||
function deleteHistoryEntry(index) {
|
||||
if (confirm('Вы уверены, что хотите удалить запись #' + index + '?')) {
|
||||
fetch('/history/delete/' + index, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Ошибка при удалении записи');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Ошибка при удалении записи');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function clearHistory() {
|
||||
if (confirm('Вы уверены, что хотите очистить всю историю?\\n\\nЭто действие нельзя отменить.')) {
|
||||
fetch('/history/clear', {
|
||||
method: 'DELETE'
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Ошибка при очистке истории');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Ошибка при очистке истории');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Поиск по истории
|
||||
function performSearch() {
|
||||
const searchTerm = document.getElementById('searchInput').value.trim();
|
||||
const searchResults = document.getElementById('searchResults');
|
||||
const historyItems = document.querySelectorAll('.history-item');
|
||||
|
||||
if (searchTerm === '') {
|
||||
// Показать все записи
|
||||
historyItems.forEach(item => {
|
||||
item.style.display = 'block';
|
||||
});
|
||||
searchResults.textContent = '';
|
||||
return;
|
||||
}
|
||||
|
||||
let visibleCount = 0;
|
||||
let totalCount = historyItems.length;
|
||||
|
||||
historyItems.forEach(item => {
|
||||
const command = item.querySelector('.history-command').textContent.toLowerCase();
|
||||
const response = item.querySelector('.history-response').textContent.toLowerCase();
|
||||
|
||||
// Объединяем команду и ответ для поиска
|
||||
const searchContent = command + ' ' + response;
|
||||
|
||||
let matches = false;
|
||||
|
||||
// Проверяем, есть ли фраза в кавычках
|
||||
if (searchTerm.startsWith("'") && searchTerm.endsWith("'")) {
|
||||
// Поиск точной фразы
|
||||
const phrase = searchTerm.slice(1, -1).toLowerCase();
|
||||
matches = searchContent.includes(phrase);
|
||||
} else {
|
||||
// Поиск по отдельным словам
|
||||
const words = searchTerm.toLowerCase().split(/\s+/);
|
||||
matches = words.every(word => searchContent.includes(word));
|
||||
}
|
||||
|
||||
if (matches) {
|
||||
item.style.display = 'block';
|
||||
visibleCount++;
|
||||
} else {
|
||||
item.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Обновляем информацию о результатах
|
||||
if (visibleCount === 0) {
|
||||
searchResults.textContent = '🔍 Ничего не найдено';
|
||||
searchResults.style.color = '#e74c3c';
|
||||
} else if (visibleCount === totalCount) {
|
||||
searchResults.textContent = '';
|
||||
} else {
|
||||
searchResults.textContent = '🔍 Найдено: ' + visibleCount + ' из ' + totalCount + ' записей';
|
||||
searchResults.style.color = '#27ae60';
|
||||
}
|
||||
}
|
||||
|
||||
// Обработчик ввода в поле поиска
|
||||
document.getElementById('searchInput').addEventListener('input', performSearch);
|
||||
|
||||
// Обработчик Enter в поле поиска
|
||||
document.getElementById('searchInput').addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
performSearch();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
||||
279
serve/templates/history_view.go
Normal file
279
serve/templates/history_view.go
Normal file
@@ -0,0 +1,279 @@
|
||||
package templates
|
||||
|
||||
// HistoryViewTemplate шаблон для просмотра записи истории
|
||||
const HistoryViewTemplate = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Запись #%d - LCG History</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #56ab2f 0%%, #a8e6cf 100%%);
|
||||
min-height: 100vh;
|
||||
}
|
||||
.container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
.header {
|
||||
background: linear-gradient(135deg, #2d5016 0%%, #4a7c59 100%%);
|
||||
color: white;
|
||||
padding: 20px 30px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.header h1 {
|
||||
margin: 0;
|
||||
font-size: 1.5em;
|
||||
font-weight: 300;
|
||||
}
|
||||
.back-btn {
|
||||
background: rgba(255,255,255,0.2);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
.back-btn:hover {
|
||||
background: rgba(255,255,255,0.3);
|
||||
}
|
||||
.content {
|
||||
padding: 30px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.history-meta {
|
||||
background: #f0f8f0;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
border-left: 4px solid #2d5016;
|
||||
}
|
||||
.history-meta-item {
|
||||
margin: 5px 0;
|
||||
color: #666;
|
||||
}
|
||||
.history-meta-label {
|
||||
font-weight: 600;
|
||||
color: #2d5016;
|
||||
}
|
||||
.history-command {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
border-left: 4px solid #4a7c59;
|
||||
}
|
||||
.history-command h3 {
|
||||
margin: 0 0 10px 0;
|
||||
color: #2d5016;
|
||||
}
|
||||
.history-command-text {
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: 1.1em;
|
||||
color: #333;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.history-response {
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #2d5016;
|
||||
}
|
||||
.history-response h3 {
|
||||
margin: 0 0 15px 0;
|
||||
color: #2d5016;
|
||||
}
|
||||
.history-response-content {
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: 0.95em;
|
||||
color: #333;
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.history-explanation {
|
||||
background: #f0f8f0;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-top: 20px;
|
||||
border-left: 4px solid #4a7c59;
|
||||
}
|
||||
.history-explanation h3 {
|
||||
margin: 0 0 15px 0;
|
||||
color: #2d5016;
|
||||
}
|
||||
.history-explanation-content {
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.history-explanation-content h1,
|
||||
.history-explanation-content h2,
|
||||
.history-explanation-content h3,
|
||||
.history-explanation-content h4,
|
||||
.history-explanation-content h5,
|
||||
.history-explanation-content h6 {
|
||||
color: #2d5016;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.history-explanation-content h1 {
|
||||
border-bottom: 2px solid #2d5016;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
.history-explanation-content h2 {
|
||||
border-bottom: 1px solid #4a7c59;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
.history-explanation-content code {
|
||||
background: #f0f8f0;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
color: #2d5016;
|
||||
border: 1px solid #a8e6cf;
|
||||
}
|
||||
.history-explanation-content pre {
|
||||
background: #f0f8f0;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #2d5016;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.history-explanation-content pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
border: none;
|
||||
color: #2d5016;
|
||||
}
|
||||
.history-explanation-content blockquote {
|
||||
border-left: 4px solid #4a7c59;
|
||||
margin: 15px 0;
|
||||
padding: 10px 20px;
|
||||
background: #f0f8f0;
|
||||
border-radius: 0 8px 8px 0;
|
||||
}
|
||||
.history-explanation-content ul,
|
||||
.history-explanation-content ol {
|
||||
padding-left: 20px;
|
||||
}
|
||||
.history-explanation-content li {
|
||||
margin: 5px 0;
|
||||
}
|
||||
.history-explanation-content strong {
|
||||
color: #2d5016;
|
||||
}
|
||||
.history-explanation-content em {
|
||||
color: #4a7c59;
|
||||
}
|
||||
.actions {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
.action-btn {
|
||||
background: #3498db;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: background 0.3s ease;
|
||||
display: inline-block;
|
||||
}
|
||||
.action-btn:hover {
|
||||
background: #2980b9;
|
||||
}
|
||||
.delete-btn {
|
||||
background: #e74c3c;
|
||||
}
|
||||
.delete-btn:hover {
|
||||
background: #c0392b;
|
||||
}
|
||||
|
||||
/* Мобильная адаптация */
|
||||
@media (max-width: 768px) {
|
||||
body { padding: 10px; }
|
||||
.container { margin: 0; border-radius: 8px; box-shadow: 0 10px 20px rgba(0,0,0,0.1); }
|
||||
.header { padding: 16px; }
|
||||
.header h1 { font-size: 1.2em; }
|
||||
.back-btn { padding: 6px 12px; font-size: 0.9em; }
|
||||
.content { padding: 20px; }
|
||||
.actions { flex-direction: column; }
|
||||
.action-btn { width: 100%; text-align: center; }
|
||||
.history-response-content { font-size: 0.9em; }
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.header h1 { font-size: 1em; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>📝 Запись #%d</h1>
|
||||
<a href="/history" class="back-btn">← Назад к истории</a>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="history-meta">
|
||||
<div class="history-meta-item">
|
||||
<span class="history-meta-label">📅 Время:</span> %s
|
||||
</div>
|
||||
<div class="history-meta-item">
|
||||
<span class="history-meta-label">🔢 Индекс:</span> #%d
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="history-command">
|
||||
<h3>💬 Запрос пользователя:</h3>
|
||||
<div class="history-command-text">%s</div>
|
||||
</div>
|
||||
|
||||
<div class="history-response">
|
||||
<h3>🤖 Ответ Модели:</h3>
|
||||
<div class="history-response-content">%s</div>
|
||||
</div>
|
||||
|
||||
%s
|
||||
|
||||
<div class="actions">
|
||||
<a href="/history" class="action-btn">📝 К истории</a>
|
||||
<button class="action-btn delete-btn" onclick="deleteHistoryEntry(%d)">🗑️ Удалить запись</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function deleteHistoryEntry(index) {
|
||||
if (confirm('Вы уверены, что хотите удалить запись #' + index + '?')) {
|
||||
fetch('/history/delete/' + index, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
window.location.href = '/history';
|
||||
} else {
|
||||
alert('Ошибка при удалении записи');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Ошибка при удалении записи');
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
||||
515
serve/templates/prompts.go
Normal file
515
serve/templates/prompts.go
Normal file
@@ -0,0 +1,515 @@
|
||||
package templates
|
||||
|
||||
// PromptsPageTemplate шаблон страницы управления промптами
|
||||
const PromptsPageTemplate = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Системные промпты - LCG Results</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #56ab2f 0%, #a8e6cf 100%);
|
||||
min-height: 100vh;
|
||||
}
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
.header {
|
||||
background: linear-gradient(135deg, #2d5016 0%, #4a7c59 100%);
|
||||
color: white;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
.header h1 {
|
||||
margin: 0;
|
||||
font-size: 2.5em;
|
||||
font-weight: 300;
|
||||
}
|
||||
.content {
|
||||
padding: 30px;
|
||||
}
|
||||
.nav-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.nav-btn {
|
||||
background: #3498db;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 1em;
|
||||
text-decoration: none;
|
||||
transition: background 0.3s ease;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
}
|
||||
.nav-btn:hover {
|
||||
background: #2980b9;
|
||||
}
|
||||
.add-btn {
|
||||
background: #27ae60;
|
||||
}
|
||||
.add-btn:hover {
|
||||
background: #229954;
|
||||
}
|
||||
.prompt-item {
|
||||
background: #f0f8f0;
|
||||
border: 1px solid #a8e6cf;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 15px;
|
||||
position: relative;
|
||||
}
|
||||
.prompt-item:hover {
|
||||
border-color: #2d5016;
|
||||
}
|
||||
.prompt-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.prompt-id {
|
||||
background: #2d5016;
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.prompt-name {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
.prompt-description {
|
||||
color: #666;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.prompt-content {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: 0.9em;
|
||||
color: #2d5016;
|
||||
border-left: 3px solid #2d5016;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.prompt-actions {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.action-btn {
|
||||
background: #4a7c59;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.8em;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
.action-btn:hover {
|
||||
background: #2d5016;
|
||||
}
|
||||
.delete-btn {
|
||||
background: #e74c3c;
|
||||
}
|
||||
.delete-btn:hover {
|
||||
background: #c0392b;
|
||||
}
|
||||
.restore-btn {
|
||||
background: #3498db;
|
||||
}
|
||||
.restore-btn:hover {
|
||||
background: #2980b9;
|
||||
}
|
||||
.default-badge {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 0.7em;
|
||||
margin-left: 8px;
|
||||
}
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #666;
|
||||
}
|
||||
.lang-switcher {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
margin-left: auto;
|
||||
}
|
||||
.lang-btn {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9em;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
.lang-btn:hover {
|
||||
background: #5a6268;
|
||||
}
|
||||
.lang-btn.active {
|
||||
background: #3498db;
|
||||
}
|
||||
.lang-btn.active:hover {
|
||||
background: #2980b9;
|
||||
}
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 2px solid #e9ecef;
|
||||
}
|
||||
.tab-btn {
|
||||
background: #f8f9fa;
|
||||
color: #6c757d;
|
||||
border: none;
|
||||
padding: 12px 20px;
|
||||
border-radius: 6px 6px 0 0;
|
||||
cursor: pointer;
|
||||
font-size: 1em;
|
||||
transition: all 0.3s ease;
|
||||
border-bottom: 3px solid transparent;
|
||||
}
|
||||
.tab-btn:hover {
|
||||
background: #e9ecef;
|
||||
color: #495057;
|
||||
}
|
||||
.tab-btn.active {
|
||||
background: #3498db;
|
||||
color: white;
|
||||
border-bottom-color: #2980b9;
|
||||
}
|
||||
.tab-content {
|
||||
display: none;
|
||||
}
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Мобильная адаптация */
|
||||
@media (max-width: 768px) {
|
||||
body { padding: 10px; }
|
||||
.container { margin: 0; border-radius: 8px; box-shadow: 0 10px 20px rgba(0,0,0,0.1); }
|
||||
.header { padding: 20px; }
|
||||
.header h1 { font-size: 2em; }
|
||||
.content { padding: 20px; }
|
||||
.nav-buttons { flex-direction: column; gap: 8px; }
|
||||
.nav-btn { text-align: center; padding: 12px 16px; font-size: 14px; }
|
||||
.lang-switcher { margin-left: 0; }
|
||||
.tabs { flex-direction: column; gap: 8px; }
|
||||
.tab-btn { text-align: center; }
|
||||
.prompt-item { padding: 15px; }
|
||||
.prompt-content { font-size: 0.85em; }
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.header h1 { font-size: 1.8em; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>⚙️ Системные промпты</h1>
|
||||
<p>Управление системными промптами Linux Command GPT</p>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="nav-buttons">
|
||||
<a href="/" class="nav-btn">🏠 Главная</a>
|
||||
<a href="/run" class="nav-btn">🚀 Выполнение</a>
|
||||
<a href="/history" class="nav-btn">📝 История</a>
|
||||
<button class="nav-btn add-btn" onclick="showAddForm()">➕ Добавить промпт</button>
|
||||
<div class="lang-switcher">
|
||||
<button class="lang-btn {{if eq .Lang "ru"}}active{{end}}" onclick="switchLang('ru')">🇷🇺 RU</button>
|
||||
<button class="lang-btn {{if eq .Lang "en"}}active{{end}}" onclick="switchLang('en')">🇺🇸 EN</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Вкладки -->
|
||||
<div class="tabs">
|
||||
<button class="tab-btn active" onclick="switchTab('system')">⚙️ Системные промпты</button>
|
||||
<button class="tab-btn" onclick="switchTab('verbose')">📝 Промпты подробности (v/vv/vvv)</button>
|
||||
</div>
|
||||
|
||||
<!-- Вкладка системных промптов -->
|
||||
<div id="system-tab" class="tab-content active">
|
||||
{{if .Prompts}}
|
||||
{{range .Prompts}}
|
||||
<div class="prompt-item">
|
||||
<div class="prompt-actions">
|
||||
<button class="action-btn" onclick="editPrompt({{.ID}}, '{{.Name}}', '{{.Description}}', '{{.Content}}')">✏️</button>
|
||||
<button class="action-btn restore-btn" onclick="restorePrompt({{.ID}})" title="Восстановить к значению по умолчанию">🔄</button>
|
||||
<button class="action-btn delete-btn" onclick="deletePrompt({{.ID}})">🗑️</button>
|
||||
</div>
|
||||
<div class="prompt-header">
|
||||
<div>
|
||||
<span class="prompt-id">#{{.ID}}</span>
|
||||
<span class="prompt-name">{{.Name}}</span>
|
||||
{{if .IsDefault}}<span class="default-badge">Встроенный</span>{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="prompt-description">{{.Description}}</div>
|
||||
<div class="prompt-content">{{.Content}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<div class="empty-state">
|
||||
<h3>⚙️ Промпты не найдены</h3>
|
||||
<p>Добавьте пользовательские промпты для настройки поведения системы</p>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<!-- Вкладка промптов подробности -->
|
||||
<div id="verbose-tab" class="tab-content">
|
||||
{{if .VerbosePrompts}}
|
||||
{{range .VerbosePrompts}}
|
||||
<div class="prompt-item">
|
||||
<div class="prompt-actions">
|
||||
<button class="action-btn" onclick="editVerbosePrompt('{{.Mode}}', '{{.Content}}')">✏️</button>
|
||||
<button class="action-btn restore-btn" onclick="restoreVerbosePrompt('{{.Mode}}')" title="Восстановить к значению по умолчанию">🔄</button>
|
||||
</div>
|
||||
<div class="prompt-header">
|
||||
<div>
|
||||
<span class="prompt-id">#{{.Mode}}</span>
|
||||
<span class="prompt-name">{{.Name}}</span>
|
||||
{{if .IsDefault}}<span class="default-badge">Встроенный</span>{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="prompt-description">{{.Description}}</div>
|
||||
<div class="prompt-content">{{.Content}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<div class="empty-state">
|
||||
<h3>📝 Промпты подробности</h3>
|
||||
<p>Промпты для режимов v, vv, vvv</p>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Форма добавления/редактирования -->
|
||||
<div id="promptForm" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000;">
|
||||
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 30px; border-radius: 12px; max-width: 600px; width: 90%;">
|
||||
<h3 id="formTitle">Добавить промпт</h3>
|
||||
<form id="promptFormData">
|
||||
<input type="hidden" id="promptId" name="id">
|
||||
<div style="margin-bottom: 15px;">
|
||||
<label style="display: block; margin-bottom: 5px; font-weight: 600;">Название:</label>
|
||||
<input type="text" id="promptName" name="name" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;" required>
|
||||
</div>
|
||||
<div style="margin-bottom: 15px;">
|
||||
<label style="display: block; margin-bottom: 5px; font-weight: 600;">Описание:</label>
|
||||
<input type="text" id="promptDescription" name="description" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;" required>
|
||||
</div>
|
||||
<div style="margin-bottom: 20px;">
|
||||
<label style="display: block; margin-bottom: 5px; font-weight: 600;">Содержание:</label>
|
||||
<textarea id="promptContent" name="content" rows="6" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-family: monospace;" required></textarea>
|
||||
</div>
|
||||
<div style="text-align: right;">
|
||||
<button type="button" onclick="hideForm()" style="background: #6c757d; color: white; border: none; padding: 8px 16px; border-radius: 4px; margin-right: 10px; cursor: pointer;">Отмена</button>
|
||||
<button type="submit" style="background: #2d5016; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer;">Сохранить</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function showAddForm() {
|
||||
document.getElementById('formTitle').textContent = 'Добавить промпт';
|
||||
document.getElementById('promptFormData').reset();
|
||||
document.getElementById('promptId').value = '';
|
||||
document.getElementById('promptForm').style.display = 'block';
|
||||
}
|
||||
|
||||
function editPrompt(id, name, description, content) {
|
||||
document.getElementById('formTitle').textContent = 'Редактировать промпт';
|
||||
document.getElementById('promptId').value = id;
|
||||
document.getElementById('promptName').value = name;
|
||||
document.getElementById('promptDescription').value = description;
|
||||
document.getElementById('promptContent').value = content;
|
||||
document.getElementById('promptForm').style.display = 'block';
|
||||
}
|
||||
|
||||
function hideForm() {
|
||||
document.getElementById('promptForm').style.display = 'none';
|
||||
}
|
||||
|
||||
function switchTab(tabName) {
|
||||
// Скрываем все вкладки
|
||||
document.querySelectorAll('.tab-content').forEach(tab => {
|
||||
tab.classList.remove('active');
|
||||
});
|
||||
|
||||
// Убираем активный класс с кнопок
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
});
|
||||
|
||||
// Показываем нужную вкладку
|
||||
document.getElementById(tabName + '-tab').classList.add('active');
|
||||
|
||||
// Активируем нужную кнопку
|
||||
event.target.classList.add('active');
|
||||
}
|
||||
|
||||
function switchLang(lang) {
|
||||
// Сохраняем текущие промпты перед переключением языка
|
||||
saveCurrentPrompts(lang);
|
||||
|
||||
// Перезагружаем страницу с новым языком
|
||||
const url = new URL(window.location);
|
||||
url.searchParams.set('lang', lang);
|
||||
window.location.href = url.toString();
|
||||
}
|
||||
|
||||
function saveCurrentPrompts(lang) {
|
||||
// Отправляем запрос для сохранения текущих промптов с новым языком
|
||||
fetch('/prompts/save-lang', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
lang: lang
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error saving prompts:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function editVerbosePrompt(mode, content) {
|
||||
// Редактирование промпта подробности
|
||||
alert('Редактирование промптов подробности будет реализовано');
|
||||
}
|
||||
|
||||
function deletePrompt(id) {
|
||||
if (confirm('Вы уверены, что хотите удалить промпт #' + id + '?')) {
|
||||
fetch('/prompts/delete/' + id, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Ошибка при удалении промпта');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Ошибка при удалении промпта');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('promptFormData').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(this);
|
||||
const id = formData.get('id');
|
||||
const url = id ? '/prompts/edit/' + id : '/prompts/add';
|
||||
const method = id ? 'PUT' : 'POST';
|
||||
|
||||
fetch(url, {
|
||||
method: method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: formData.get('name'),
|
||||
description: formData.get('description'),
|
||||
content: formData.get('content')
|
||||
})
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Ошибка при сохранении промпта');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Ошибка при сохранении промпта');
|
||||
});
|
||||
});
|
||||
|
||||
// Функция восстановления системного промпта
|
||||
function restorePrompt(id) {
|
||||
if (confirm('Восстановить промпт к значению по умолчанию?')) {
|
||||
fetch('/prompts/restore/' + id, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert('Промпт восстановлен');
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Ошибка: ' + data.error);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Ошибка при восстановлении промпта');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Функция восстановления verbose промпта
|
||||
function restoreVerbosePrompt(mode) {
|
||||
if (confirm('Восстановить промпт к значению по умолчанию?')) {
|
||||
fetch('/prompts/restore-verbose/' + mode, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert('Промпт восстановлен');
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Ошибка: ' + data.error);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Ошибка при восстановлении промпта');
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
||||
329
serve/templates/results.go
Normal file
329
serve/templates/results.go
Normal file
@@ -0,0 +1,329 @@
|
||||
package templates
|
||||
|
||||
// ResultsPageTemplate шаблон главной страницы со списком файлов
|
||||
const ResultsPageTemplate = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>LCG Results - Linux Command GPT</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #56ab2f 0%, #a8e6cf 100%);
|
||||
min-height: 100vh;
|
||||
}
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
.header {
|
||||
background: linear-gradient(135deg, #2d5016 0%, #4a7c59 100%);
|
||||
color: white;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
.header h1 {
|
||||
margin: 0;
|
||||
font-size: 2.5em;
|
||||
font-weight: 300;
|
||||
}
|
||||
.header p {
|
||||
margin: 10px 0 0 0;
|
||||
opacity: 0.9;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
.content {
|
||||
padding: 30px;
|
||||
}
|
||||
.nav-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.stat-card {
|
||||
background: #f0f8f0;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
border-left: 4px solid #2d5016;
|
||||
}
|
||||
.stat-number {
|
||||
font-size: 2em;
|
||||
font-weight: bold;
|
||||
color: #2d5016;
|
||||
}
|
||||
.stat-label {
|
||||
color: #666;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.files-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
.file-card {
|
||||
background: white;
|
||||
border: 1px solid #e1e5e9;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
.file-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(45,80,22,0.2);
|
||||
border-color: #2d5016;
|
||||
}
|
||||
.file-card-content {
|
||||
cursor: pointer;
|
||||
}
|
||||
.file-actions {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.delete-btn {
|
||||
background: #e74c3c;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.8em;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
.delete-btn:hover {
|
||||
background: #c0392b;
|
||||
}
|
||||
.file-name {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
.file-info {
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.file-preview {
|
||||
background: #f0f8f0;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: 0.85em;
|
||||
color: #2d5016;
|
||||
max-height: 100px;
|
||||
overflow: hidden;
|
||||
border-left: 3px solid #2d5016;
|
||||
}
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #666;
|
||||
}
|
||||
.empty-state h3 {
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.nav-btn, .nav-button {
|
||||
background: #3498db;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 1em;
|
||||
text-decoration: none;
|
||||
transition: background 0.3s ease;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
}
|
||||
.nav-btn:hover, .nav-button:hover {
|
||||
background: #2980b9;
|
||||
}
|
||||
|
||||
/* Мобильная адаптация */
|
||||
@media (max-width: 768px) {
|
||||
body { padding: 10px; }
|
||||
.container { margin: 0; border-radius: 8px; box-shadow: 0 10px 20px rgba(0,0,0,0.1); }
|
||||
.header { padding: 20px; }
|
||||
.header h1 { font-size: 2em; }
|
||||
.content { padding: 20px; }
|
||||
.files-grid { grid-template-columns: 1fr; }
|
||||
.stats { grid-template-columns: 1fr 1fr; }
|
||||
.nav-buttons { flex-direction: column; gap: 8px; }
|
||||
.nav-btn, .nav-button { text-align: center; padding: 12px 16px; font-size: 14px; }
|
||||
.search-container input { font-size: 16px; }
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.header h1 { font-size: 1.8em; }
|
||||
.stats { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🚀 LCG Results</h1>
|
||||
<p>Просмотр сохраненных результатов Linux Command GPT</p>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="nav-buttons">
|
||||
<button class="nav-btn" onclick="location.reload()">🔄 Обновить</button>
|
||||
<a href="/run" class="nav-btn">🚀 Выполнение</a>
|
||||
<a href="/history" class="nav-btn">📝 История</a>
|
||||
<a href="/prompts" class="nav-btn">⚙️ Промпты</a>
|
||||
</div>
|
||||
|
||||
<!-- Поиск -->
|
||||
<div class="search-container">
|
||||
<input type="text" id="searchInput" placeholder="🔍 Поиск по содержимому файлов..."
|
||||
style="width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 16px;">
|
||||
<div id="searchResults" style="margin-top: 10px; color: #666; font-size: 14px;"></div>
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat-card">
|
||||
<div class="stat-number">{{.TotalFiles}}</div>
|
||||
<div class="stat-label">Всего файлов</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number">{{.RecentFiles}}</div>
|
||||
<div class="stat-label">За последние 7 дней</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{if .Files}}
|
||||
<div class="files-grid">
|
||||
{{range .Files}}
|
||||
<div class="file-card" data-content="{{.Content}}">
|
||||
<div class="file-actions">
|
||||
<button class="delete-btn" onclick="deleteFile('{{.Name}}')" title="Удалить файл">🗑️</button>
|
||||
</div>
|
||||
<div class="file-card-content" onclick="window.location.href='/file/{{.Name}}'">
|
||||
<div class="file-name">{{.Name}}</div>
|
||||
<div class="file-info">
|
||||
📅 {{.ModTime}} | 📏 {{.Size}}
|
||||
</div>
|
||||
<div class="file-preview">{{.Preview}}</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="empty-state">
|
||||
<h3>📁 Папка пуста</h3>
|
||||
<p>Здесь будут отображаться сохраненные результаты после использования команды lcg</p>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function deleteFile(filename) {
|
||||
if (confirm('Вы уверены, что хотите удалить файл "' + filename + '"?\\n\\nЭто действие нельзя отменить.')) {
|
||||
fetch('/delete/' + encodeURIComponent(filename), {
|
||||
method: 'DELETE'
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Ошибка при удалении файла');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Ошибка при удалении файла');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Поиск по содержимому файлов
|
||||
function performSearch() {
|
||||
const searchTerm = document.getElementById('searchInput').value.trim();
|
||||
const searchResults = document.getElementById('searchResults');
|
||||
const fileCards = document.querySelectorAll('.file-card');
|
||||
|
||||
if (searchTerm === '') {
|
||||
// Показать все файлы
|
||||
fileCards.forEach(card => {
|
||||
card.style.display = 'block';
|
||||
});
|
||||
searchResults.textContent = '';
|
||||
return;
|
||||
}
|
||||
|
||||
let visibleCount = 0;
|
||||
let totalCount = fileCards.length;
|
||||
|
||||
fileCards.forEach(card => {
|
||||
const fileName = card.querySelector('.file-name').textContent.toLowerCase();
|
||||
const fullContent = card.getAttribute('data-content').toLowerCase();
|
||||
|
||||
// Проверяем поиск по полному содержимому файла
|
||||
const fileContent = fileName + ' ' + fullContent;
|
||||
|
||||
let matches = false;
|
||||
|
||||
// Проверяем, есть ли фраза в кавычках
|
||||
if (searchTerm.startsWith("'") && searchTerm.endsWith("'")) {
|
||||
// Поиск точной фразы
|
||||
const phrase = searchTerm.slice(1, -1).toLowerCase();
|
||||
matches = fileContent.includes(phrase);
|
||||
} else {
|
||||
// Поиск по отдельным словам
|
||||
const words = searchTerm.toLowerCase().split(/\s+/);
|
||||
matches = words.every(word => fileContent.includes(word));
|
||||
}
|
||||
|
||||
if (matches) {
|
||||
card.style.display = 'block';
|
||||
visibleCount++;
|
||||
} else {
|
||||
card.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Обновляем информацию о результатах
|
||||
if (visibleCount === 0) {
|
||||
searchResults.textContent = '🔍 Ничего не найдено';
|
||||
searchResults.style.color = '#e74c3c';
|
||||
} else if (visibleCount === totalCount) {
|
||||
searchResults.textContent = '';
|
||||
} else {
|
||||
searchResults.textContent = '🔍 Найдено: ' + visibleCount + ' из ' + totalCount + ' файлов';
|
||||
searchResults.style.color = '#27ae60';
|
||||
}
|
||||
}
|
||||
|
||||
// Обработчик ввода в поле поиска
|
||||
document.getElementById('searchInput').addEventListener('input', performSearch);
|
||||
|
||||
// Обработчик Enter в поле поиска
|
||||
document.getElementById('searchInput').addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
performSearch();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
||||
@@ -1,92 +1,134 @@
|
||||
#!/bin/bash
|
||||
|
||||
# REPO=registry.direct-dev.ru/go-lcg
|
||||
REPO=kuznetcovay/go-lcg
|
||||
VERSION=$1
|
||||
if [ -z "$VERSION" ]; then
|
||||
VERSION=v1.0.1
|
||||
fi
|
||||
BRANCH=main
|
||||
# Включаем строгий режим для лучшей отладки
|
||||
set -euo pipefail
|
||||
|
||||
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 {
|
||||
echo "Restoring original branch: ${CURRENT_BRANCH}"
|
||||
git checkout "${CURRENT_BRANCH}"
|
||||
# Функции для логирования
|
||||
log_info() {
|
||||
echo -e "${GREEN}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
# Check if the current branch is different from the target branch
|
||||
if [ "$CURRENT_BRANCH" != "$BRANCH" ]; then
|
||||
# Set a trap to restore the branch on exit
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# Функция для обработки ошибок
|
||||
handle_error() {
|
||||
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
|
||||
echo "Switching to branch: ${BRANCH}"
|
||||
git checkout ${BRANCH}
|
||||
fi
|
||||
|
||||
# Fetch all tags from the remote repository
|
||||
git fetch --tags
|
||||
# Переключаемся на нужную ветку если необходимо
|
||||
if [[ "$CURRENT_BRANCH" != "$BRANCH" ]]; then
|
||||
log_info "Переключаемся на ветку: $BRANCH"
|
||||
git checkout "$BRANCH"
|
||||
fi
|
||||
|
||||
# Check if the specified version tag exists
|
||||
if git rev-parse "refs/tags/${VERSION}" >/dev/null 2>&1; then
|
||||
echo "Tag ${VERSION} already exists. Halting script."
|
||||
# Получаем теги
|
||||
log_info "Получаем теги из удаленного репозитория..."
|
||||
git fetch --tags
|
||||
|
||||
# Проверяем существование тега
|
||||
if git rev-parse "refs/tags/${VERSION}" >/dev/null 2>&1; then
|
||||
log_error "Тег ${VERSION} уже существует. Прерываем выполнение."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Run go tests
|
||||
# if ! go test -v -run=^Test; then
|
||||
# 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
|
||||
}
|
||||
# Создаем директорию для бинарных файлов
|
||||
mkdir -p binaries-for-upload
|
||||
|
||||
cp bin-linux-amd64/lcg "binaries-for-upload/lcg.amd64.${VERSION}"
|
||||
# Собираем бинарные файлы для обеих платформ
|
||||
build_binary "amd64"
|
||||
build_binary "arm64"
|
||||
|
||||
# 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."
|
||||
# Собираем и пушим 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
|
||||
|
||||
cp bin-linux-arm64/lcg "binaries-for-upload/lcg.arm64.${VERSION}"
|
||||
# Выполняем git операции
|
||||
git_operations
|
||||
|
||||
# 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
|
||||
}
|
||||
log_info "Сборка версии $VERSION завершена успешно!"
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
# Запускаем основную функцию
|
||||
main "$@"
|
||||
|
||||
|
||||
10
shell-code/curl.sh
Normal file
10
shell-code/curl.sh
Normal file
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
execute_command() {
|
||||
curl -s -X POST "http://localhost:8085/api/execute" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"prompt\": \"$1\", \"verbose\": \"$2\"}" | \
|
||||
jq -r '.'
|
||||
}
|
||||
|
||||
execute_command "$1" "$2"
|
||||
5
shell-code/pre-release.sh
Normal file
5
shell-code/pre-release.sh
Normal 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
|
||||
214
shell-code/release.py
Normal file
214
shell-code/release.py
Normal file
@@ -0,0 +1,214 @@
|
||||
#!/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("Релиз с тегом {tag} создан успешно")
|
||||
return release_data
|
||||
else:
|
||||
error(f"Ошибка создания релиза с тегом {tag}: {response.status_code}")
|
||||
debug(f"Ответ: {response.text}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def upload_file(session, upload_url, file_path):
|
||||
"""Загрузка файла в релиз"""
|
||||
filename = file_path.name
|
||||
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
134
shell-code/release.sh
Normal 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"
|
||||
6
shell-code/run-with-proxy.sh
Normal file
6
shell-code/run-with-proxy.sh
Normal 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
6
shell-code/run_ollama.sh
Normal 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
81
shell-code/test_api.py
Normal 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()
|
||||
@@ -1,52 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Variables
|
||||
VERSION_FILE="VERSION.txt"
|
||||
|
||||
GITHUB_TOKEN="${GITHUB_TOKEN}" # Replace with your GitHub token
|
||||
|
||||
REPO="direct-dev-ru/binaries" # Replace with your GitHub username/repo
|
||||
|
||||
TAG=lcg.$(cat "$VERSION_FILE")
|
||||
|
||||
echo TAG: $TAG
|
||||
|
||||
RELEASE_DIR="/home/su/projects/golang/linux-command-gpt/binaries-for-upload"
|
||||
|
||||
body="{\"tag_name\":\"${TAG}\", \"target_commitish\":\"main\", \"name\":\"${TAG}\", \
|
||||
\"body\":\"${TAG}\", \"draft\":false, \"prerelease\":false, \"generate_release_notes\":false}"
|
||||
|
||||
echo BODY: $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."
|
||||
Reference in New Issue
Block a user