mirror of
https://github.com/Direct-Dev-Ru/go-lcg.git
synced 2025-11-16 09:39:56 +00:00
Compare commits
24 Commits
lcg.v2.0.1
...
v2.0.14
| Author | SHA1 | Date | |
|---|---|---|---|
| 4779c4bca4 | |||
| 1e2ce929b2 | |||
| 9aa5aefdad | |||
| 7455987c0f | |||
| 3e143ee7a1 | |||
| 96a8060afb | |||
| 99b1a74034 | |||
| 89d15bfdc9 | |||
| 5c672ecc39 | |||
| 164f32dbaf | |||
| 1545fe2508 | |||
| deb80f2b37 | |||
| 3c95eb85db | |||
| 5b78e775c1 | |||
| 9044b02d27 | |||
| e7c11879a1 | |||
| 6444c35bbb | |||
| 6ec41355d3 | |||
| 7a0d0746d4 | |||
| 0da366cad5 | |||
| 3be2880dd2 | |||
| c70effda73 | |||
| 611bd17ac1 | |||
| e1bd79db8c |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -17,3 +17,6 @@ gpt_results
|
||||
shell-code/jwt.admin.token
|
||||
run.sh
|
||||
lcg_history.json
|
||||
deploy/0.create_sealed_secrets.sh
|
||||
deploy/0.create_git_secrets.sh
|
||||
deploy/0.create_app_secrets.sh
|
||||
|
||||
@@ -1,53 +1,32 @@
|
||||
# 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
|
||||
|
||||
# Goreleaser configuration version 2
|
||||
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
|
||||
- id: lcg
|
||||
binary: "lcg_{{ .Version }}"
|
||||
goos:
|
||||
- linux
|
||||
- windows
|
||||
- darwin
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
ldflags:
|
||||
- -s -w
|
||||
- -X main.version={{.Version}}
|
||||
- -X main.commit={{.Commit}}
|
||||
- -X main.date={{.Date}}
|
||||
main: .
|
||||
dir: .
|
||||
|
||||
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).
|
||||
- id: lcg
|
||||
ids:
|
||||
- lcg
|
||||
formats:
|
||||
- binary
|
||||
name_template: "{{ .Binary }}_{{ .Os }}_{{ .Arch }}"
|
||||
files:
|
||||
- "lcg_{{ .Version }}"
|
||||
|
||||
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"djlint.showInstallError": false
|
||||
}
|
||||
@@ -1,6 +1,36 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
Версия 2.0.6 (2025-10-28)
|
||||
=========================
|
||||
|
||||
## ✨ НОВОЕ И ИЗМЕНЕНО
|
||||
|
||||
- 🌐 Поддержка BasePath для всех веб‑роутов и шаблонов
|
||||
- Новый параметр: `LCG_BASE_URL` (пример: `/lcg`) — префикс для всех страниц и API
|
||||
- Обновлены редиректы и middleware с учетом BasePath
|
||||
- 🧭 Кастомная страница 404 (красная тема), показывается для любого неизвестного пути под BasePath
|
||||
- 📱 Улучшена мобильная верстка результатов — стиль карточек как в истории
|
||||
- 🗂️ Человекочитаемые заголовки результатов: преобразование имени файла в «заголовок — дата время»
|
||||
- 🗑️ Иконки удаления: единый бледно‑красный крест ✖ в результатах и истории
|
||||
|
||||
## 🐛 ИСПРАВЛЕНИЯ
|
||||
|
||||
- 🛡️ Исправлен просмотр/удаление файла при включенном BasePath (правильный разбор URL)
|
||||
- 🧰 На старте сервера гарантируется создание `ResultFolder` и пустого `ResultHistory` (без 500)
|
||||
- 🚧 Главная страница обрабатывается только по точному пути BasePath, а не по произвольным под‑путям
|
||||
|
||||
## ⚙️ КОНФИГУРАЦИЯ
|
||||
|
||||
- 🔍 Debug режим теперь включается и флагом `--debug`, и переменной `LCG_DEBUG=1|true`
|
||||
- 🍪 Уточнена работа с `CookiePath`/`BasePath` в middleware
|
||||
|
||||
## 📚 ДОКУМЕНТАЦИЯ
|
||||
|
||||
- Обновлены `README.md`, `USAGE_GUIDE.md`, `API_GUIDE.md`, `REVERSE_PROXY_GUIDE.md` — добавлены примеры с BasePath и примечания к 404
|
||||
|
||||
---
|
||||
|
||||
Версия 2.0.1 (2025-10-22)
|
||||
=========================
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
v2.0.1
|
||||
v2.0.14
|
||||
|
||||
1
build-conditions.yaml
Normal file
1
build-conditions.yaml
Normal file
@@ -0,0 +1 @@
|
||||
no-serve: false
|
||||
58
build.sh
Executable file
58
build.sh
Executable file
@@ -0,0 +1,58 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 🚀 LCG Build Script (Root)
|
||||
# Скрипт для сборки из корневой директории проекта
|
||||
|
||||
set -e
|
||||
|
||||
# Цвета для вывода
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Функция для вывода сообщений
|
||||
log() {
|
||||
echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1"
|
||||
}
|
||||
|
||||
success() {
|
||||
echo -e "${GREEN}✅ $1${NC}"
|
||||
}
|
||||
|
||||
warning() {
|
||||
echo -e "${YELLOW}⚠️ $1${NC}"
|
||||
}
|
||||
|
||||
error() {
|
||||
echo -e "${RED}❌ $1${NC}"
|
||||
}
|
||||
|
||||
# Параметры
|
||||
REPOSITORY=${1:-"your-registry.com/lcg"}
|
||||
VERSION=${2:-"latest"}
|
||||
PLATFORMS=${3:-"linux/amd64,linux/arm64"}
|
||||
|
||||
log "🚀 Сборка LCG из корневой директории..."
|
||||
|
||||
# Проверяем, что мы в корневой директории
|
||||
if [ ! -f "go.mod" ]; then
|
||||
error "Этот скрипт должен запускаться из корневой директории проекта (где находится go.mod)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Записываем версию в файл VERSION.txt
|
||||
echo "$VERSION" > VERSION.txt
|
||||
log "📝 Версия записана в VERSION.txt: $VERSION"
|
||||
|
||||
# Запускаем полную сборку
|
||||
log "🚀 Запуск полной сборки..."
|
||||
./deploy/full-build.sh "$REPOSITORY" "$VERSION" "$PLATFORMS"
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
success "🎉 Сборка завершена успешно!"
|
||||
else
|
||||
error "Ошибка при сборке"
|
||||
exit 1
|
||||
fi
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -20,9 +21,20 @@ type HistoryEntry struct {
|
||||
|
||||
func read(historyPath string) ([]HistoryEntry, error) {
|
||||
data, err := os.ReadFile(historyPath)
|
||||
if err != nil || len(data) == 0 {
|
||||
if err != nil {
|
||||
// Если файл не существует, создаем пустой файл истории
|
||||
if os.IsNotExist(err) {
|
||||
emptyHistory := []HistoryEntry{}
|
||||
if writeErr := write(historyPath, emptyHistory); writeErr != nil {
|
||||
return nil, fmt.Errorf("не удалось создать файл истории: %v", writeErr)
|
||||
}
|
||||
return emptyHistory, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return []HistoryEntry{}, nil
|
||||
}
|
||||
var items []HistoryEntry
|
||||
if err := json.Unmarshal(data, &items); err != nil {
|
||||
return nil, err
|
||||
@@ -50,6 +62,12 @@ func ShowHistory(historyPath string, printColored func(string, string), colorYel
|
||||
printColored("📝 История пуста\n", colorYellow)
|
||||
return
|
||||
}
|
||||
|
||||
// Сортируем записи по времени в убывающем порядке (новые сначала)
|
||||
sort.Slice(items, func(i, j int) bool {
|
||||
return items[i].Timestamp.After(items[j].Timestamp)
|
||||
})
|
||||
|
||||
printColored("📝 История (из файла):\n", colorYellow)
|
||||
for _, h := range items {
|
||||
ts := h.Timestamp.Format("2006-01-02 15:04:05")
|
||||
|
||||
118
config/config.go
118
config/config.go
@@ -3,6 +3,8 @@ package config
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -10,10 +12,12 @@ type Config struct {
|
||||
Cwd string
|
||||
Host string
|
||||
ProxyUrl string
|
||||
AppName string
|
||||
Completions string
|
||||
Model string
|
||||
Prompt string
|
||||
ApiKeyFile string
|
||||
ConfigFolder string
|
||||
ResultFolder string
|
||||
PromptFolder string
|
||||
ProviderType string
|
||||
@@ -25,6 +29,7 @@ type Config struct {
|
||||
AllowExecution bool
|
||||
MainFlags MainFlags
|
||||
Server ServerConfig
|
||||
Validation ValidationConfig
|
||||
}
|
||||
|
||||
type MainFlags struct {
|
||||
@@ -39,6 +44,37 @@ type MainFlags struct {
|
||||
type ServerConfig struct {
|
||||
Port string
|
||||
Host string
|
||||
HealthUrl string
|
||||
ProxyUrl string
|
||||
BasePath string
|
||||
ConfigFolder string
|
||||
AllowHTTP bool
|
||||
SSLCertFile string
|
||||
SSLKeyFile string
|
||||
RequireAuth bool
|
||||
Password string
|
||||
Domain string
|
||||
CookieSecure bool
|
||||
CookiePath string
|
||||
CookieTTLHours int
|
||||
}
|
||||
|
||||
type ValidationConfig struct {
|
||||
MaxSystemPromptLength int
|
||||
MaxUserMessageLength int
|
||||
MaxPromptNameLength int
|
||||
MaxPromptDescLength int
|
||||
MaxCommandLength int
|
||||
MaxExplanationLength int
|
||||
}
|
||||
|
||||
func GetEnvBool(key string, defaultValue bool) bool {
|
||||
if value, exists := os.LookupEnv(key); exists {
|
||||
if boolValue, err := strconv.ParseBool(value); err == nil {
|
||||
return boolValue
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func getEnv(key, defaultValue string) string {
|
||||
@@ -48,6 +84,33 @@ func getEnv(key, defaultValue string) string {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func getEnvInt(key string, defaultValue int) int {
|
||||
if value, exists := os.LookupEnv(key); exists {
|
||||
if intValue, err := strconv.Atoi(value); err == nil {
|
||||
return intValue
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func getServerAllowHTTP() bool {
|
||||
// Если переменная явно установлена, используем её
|
||||
if value, exists := os.LookupEnv("LCG_SERVER_ALLOW_HTTP"); exists {
|
||||
if boolValue, err := strconv.ParseBool(value); err == nil {
|
||||
return boolValue
|
||||
}
|
||||
}
|
||||
|
||||
// Если переменная не установлена, определяем по умолчанию на основе хоста
|
||||
host := getEnv("LCG_SERVER_HOST", "localhost")
|
||||
return isSecureHost(host)
|
||||
}
|
||||
|
||||
func isSecureHost(host string) bool {
|
||||
secureHosts := []string{"localhost", "127.0.0.1", "::1"}
|
||||
return slices.Contains(secureHosts, host)
|
||||
}
|
||||
|
||||
func Load() Config {
|
||||
cwd, _ := os.Getwd()
|
||||
|
||||
@@ -55,14 +118,21 @@ func Load() Config {
|
||||
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"))
|
||||
privateResultsDir := path.Join(homedir, ".config", "lcg", "gpt_results")
|
||||
os.MkdirAll(privateResultsDir, 0700)
|
||||
resultFolder := getEnv("LCG_RESULT_FOLDER", privateResultsDir)
|
||||
|
||||
os.MkdirAll(path.Join(homedir, ".config", "lcg", "gpt_sys_prompts"), 0755)
|
||||
promptFolder := getEnv("LCG_PROMPT_FOLDER", path.Join(homedir, ".config", "lcg", "gpt_sys_prompts"))
|
||||
privatePromptsDir := path.Join(homedir, ".config", "lcg", "gpt_sys_prompts")
|
||||
os.MkdirAll(privatePromptsDir, 0700)
|
||||
promptFolder := getEnv("LCG_PROMPT_FOLDER", privatePromptsDir)
|
||||
|
||||
privateConfigDir := path.Join(homedir, ".config", "lcg", "config")
|
||||
os.MkdirAll(privateConfigDir, 0700)
|
||||
configFolder := getEnv("LCG_CONFIG_FOLDER", privateConfigDir)
|
||||
|
||||
return Config{
|
||||
Cwd: cwd,
|
||||
AppName: getEnv("LCG_APP_NAME", "Linux Command GPT"),
|
||||
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"),
|
||||
@@ -71,6 +141,7 @@ func Load() Config {
|
||||
ApiKeyFile: getEnv("LCG_API_KEY_FILE", ".openai_api_key"),
|
||||
ResultFolder: resultFolder,
|
||||
PromptFolder: promptFolder,
|
||||
ConfigFolder: configFolder,
|
||||
ProviderType: getEnv("LCG_PROVIDER", "ollama"),
|
||||
JwtToken: getEnv("LCG_JWT_TOKEN", ""),
|
||||
PromptID: getEnv("LCG_PROMPT_ID", "1"),
|
||||
@@ -81,6 +152,27 @@ func Load() Config {
|
||||
Server: ServerConfig{
|
||||
Port: getEnv("LCG_SERVER_PORT", "8080"),
|
||||
Host: getEnv("LCG_SERVER_HOST", "localhost"),
|
||||
ConfigFolder: getEnv("LCG_CONFIG_FOLDER", path.Join(homedir, ".config", "lcg", "config")),
|
||||
AllowHTTP: getServerAllowHTTP(),
|
||||
SSLCertFile: getEnv("LCG_SERVER_SSL_CERT_FILE", ""),
|
||||
SSLKeyFile: getEnv("LCG_SERVER_SSL_KEY_FILE", ""),
|
||||
RequireAuth: isServerRequireAuth(),
|
||||
Password: getEnv("LCG_SERVER_PASSWORD", "admin#123456"),
|
||||
Domain: getEnv("LCG_DOMAIN", getEnv("LCG_SERVER_HOST", "localhost")),
|
||||
CookieSecure: isCookieSecure(),
|
||||
CookiePath: getEnv("LCG_COOKIE_PATH", "/lcg"),
|
||||
CookieTTLHours: getEnvInt("LCG_COOKIE_TTL_HOURS", 168), // 7 дней по умолчанию
|
||||
BasePath: getEnv("LCG_BASE_URL", "/lcg"),
|
||||
HealthUrl: getEnv("LCG_HEALTH_URL", "/api/v1/protected/sberchat/health"),
|
||||
ProxyUrl: getEnv("LCG_PROXY_URL", "/api/v1/protected/sberchat/chat"),
|
||||
},
|
||||
Validation: ValidationConfig{
|
||||
MaxSystemPromptLength: getEnvInt("LCG_MAX_SYSTEM_PROMPT_LENGTH", 2000),
|
||||
MaxUserMessageLength: getEnvInt("LCG_MAX_USER_MESSAGE_LENGTH", 4000),
|
||||
MaxPromptNameLength: getEnvInt("LCG_MAX_PROMPT_NAME_LENGTH", 2000),
|
||||
MaxPromptDescLength: getEnvInt("LCG_MAX_PROMPT_DESC_LENGTH", 5000),
|
||||
MaxCommandLength: getEnvInt("LCG_MAX_COMMAND_LENGTH", 8000),
|
||||
MaxExplanationLength: getEnvInt("LCG_MAX_EXPLANATION_LENGTH", 20000),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -103,6 +195,24 @@ func isAllowExecutionEnabled() bool {
|
||||
return vLower == "1" || vLower == "true"
|
||||
}
|
||||
|
||||
func isServerRequireAuth() bool {
|
||||
v := strings.TrimSpace(getEnv("LCG_SERVER_REQUIRE_AUTH", ""))
|
||||
if v == "" {
|
||||
return false
|
||||
}
|
||||
vLower := strings.ToLower(v)
|
||||
return vLower == "1" || vLower == "true"
|
||||
}
|
||||
|
||||
func isCookieSecure() bool {
|
||||
v := strings.TrimSpace(getEnv("LCG_COOKIE_SECURE", ""))
|
||||
if v == "" {
|
||||
return false
|
||||
}
|
||||
vLower := strings.ToLower(v)
|
||||
return vLower == "1" || vLower == "true"
|
||||
}
|
||||
|
||||
var AppConfig Config
|
||||
|
||||
func init() {
|
||||
|
||||
32
deploy/.goreleaser.yaml
Normal file
32
deploy/.goreleaser.yaml
Normal file
@@ -0,0 +1,32 @@
|
||||
# Goreleaser configuration version 2
|
||||
version: 2
|
||||
|
||||
builds:
|
||||
- id: lcg
|
||||
binary: "lcg_{{ .Version }}"
|
||||
goos:
|
||||
- linux
|
||||
- windows
|
||||
- darwin
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
ldflags:
|
||||
- -s -w
|
||||
- -X main.version={{.Version}}
|
||||
- -X main.commit={{.Commit}}
|
||||
- -X main.date={{.Date}}
|
||||
main: .
|
||||
dir: .
|
||||
|
||||
archives:
|
||||
- id: lcg
|
||||
ids:
|
||||
- lcg
|
||||
formats:
|
||||
- binary
|
||||
name_template: "{{ .Binary }}_{{ .Os }}_{{ .Arch }}"
|
||||
files:
|
||||
- "lcg_{{ .Version }}"
|
||||
21
deploy/0.create_secrets.example.sh
Normal file
21
deploy/0.create_secrets.example.sh
Normal file
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# https://dev.to/ashokan/sealed-secrets-the-secret-sauce-for-managing-secrets-2hg6
|
||||
# head -c 64 /dev/urandom | base64 -w 0
|
||||
export KUBECONFIG=/home/su/.kube/config_hlab
|
||||
|
||||
kubectl create secret generic lcg-secrets -n lcg \
|
||||
--from-literal=LCG_SERVER_PASSWORDL= \
|
||||
--from-literal=LCG_CSRF_SECRET=\
|
||||
--from-literal=LCG_JWT_SECRET=\
|
||||
--from-literal=LCG_JWT_TOKEN=\
|
||||
--dry-run=client -o yaml | tee secret-cfg.yaml
|
||||
|
||||
kubeseal --controller-name=sealed-secrets-controller --controller-namespace=kube-system -o yaml <secret-cfg.yaml | tee sealed-cfg.yaml
|
||||
|
||||
rm -f secret-cfg.yaml
|
||||
|
||||
kubectl apply -f sealed-cfg.yaml
|
||||
cp sealed-cfg.yaml ../kustomize/secret.yaml
|
||||
|
||||
kubectl get secret lcg-secrets -n lcg -o json | jq ".data | map_values(@base64d)"
|
||||
7
deploy/0.namespace.yaml
Normal file
7
deploy/0.namespace.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: lcg
|
||||
labels:
|
||||
name: lcg
|
||||
app: linux-command-gpt
|
||||
45
deploy/1.configmap.tmpl.yaml
Normal file
45
deploy/1.configmap.tmpl.yaml
Normal file
@@ -0,0 +1,45 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: lcg-config
|
||||
namespace: lcg
|
||||
data:
|
||||
# Основные настройки
|
||||
LCG_VERSION: "${VERSION}"
|
||||
LCG_BASE_PATH: "/lcg"
|
||||
LCG_SERVER_HOST: "0.0.0.0"
|
||||
LCG_SERVER_PORT: "8080"
|
||||
LCG_SERVER_ALLOW_HTTP: "true"
|
||||
LCG_APP_NAME: "Linux Command GPT"
|
||||
LCG_RESULT_FOLDER: "/app/data/results"
|
||||
LCG_PROMPT_FOLDER: "/app/data/prompts"
|
||||
LCG_CONFIG_FOLDER: "/app/data/config"
|
||||
LCG_NO_HISTORY: "false"
|
||||
LCG_ALLOW_EXECUTION: "false"
|
||||
LCG_DEBUG: "true"
|
||||
LCG_PROVIDER: "proxy"
|
||||
|
||||
# Настройки аутентификации
|
||||
LCG_SERVER_REQUIRE_AUTH: "true"
|
||||
|
||||
LCG_COOKIE_SECURE: "true"
|
||||
LCG_COOKIE_TTL_HOURS: "168"
|
||||
LCG_DOMAIN: "direct-dev.ru"
|
||||
LCG_COOKIE_PATH: "/lcg"
|
||||
|
||||
# Настройки провайдера (по умолчанию)
|
||||
LCG_PROVIDER_TYPE: "proxy"
|
||||
LCG_HOST: "https://direct-dev.ru"
|
||||
LCG_HEALTH_URL: "/api/v1/protected/sberchat/health"
|
||||
LCG_PROXY_URL: "/api/v1/protected/sberchat/chat"
|
||||
LCG_MODEL: "GigaChat-2-Max"
|
||||
|
||||
# Настройки валидации
|
||||
LCG_MAX_SYSTEM_PROMPT_LENGTH: "2000"
|
||||
LCG_MAX_USER_MESSAGE_LENGTH: "4000"
|
||||
LCG_MAX_PROMPT_NAME_LENGTH: "2000"
|
||||
LCG_MAX_PROMPT_DESC_LENGTH: "50000"
|
||||
|
||||
# Настройки таймаутов
|
||||
LCG_TIMEOUT: "300"
|
||||
|
||||
77
deploy/2.lcg-flux.yaml
Normal file
77
deploy/2.lcg-flux.yaml
Normal file
@@ -0,0 +1,77 @@
|
||||
apiVersion: source.toolkit.fluxcd.io/v1
|
||||
kind: GitRepository
|
||||
metadata:
|
||||
name: go-lcg
|
||||
namespace: flux-system
|
||||
spec:
|
||||
interval: 3m
|
||||
url: https://github.com/Direct-Dev-Ru/go-lcg.git
|
||||
ref:
|
||||
branch: release
|
||||
secretRef:
|
||||
name: git-secrets
|
||||
|
||||
---
|
||||
|
||||
# apiVersion: source.toolkit.fluxcd.io/v1
|
||||
# kind: GitRepository
|
||||
# metadata:
|
||||
# name: linux-command-gpt
|
||||
# namespace: flux-system
|
||||
# spec:
|
||||
# interval: 3m
|
||||
# url: https://direct-dev.ru/gitea/GiteaAdmin/go-lcg.git
|
||||
# ref:
|
||||
# branch: release
|
||||
# secretRef:
|
||||
# name: gitea-token
|
||||
|
||||
|
||||
---
|
||||
apiVersion: kustomize.toolkit.fluxcd.io/v1
|
||||
kind: Kustomization
|
||||
metadata:
|
||||
name: lcg
|
||||
namespace: flux-system
|
||||
spec:
|
||||
healthChecks:
|
||||
- kind: Deployment
|
||||
name: lcg
|
||||
namespace: lcg
|
||||
interval: 3m15s
|
||||
path: ./kustomize
|
||||
prune: true
|
||||
sourceRef:
|
||||
kind: GitRepository
|
||||
name: go-lcg
|
||||
targetNamespace: lcg
|
||||
timeout: 2m0s
|
||||
|
||||
---
|
||||
|
||||
apiVersion: image.toolkit.fluxcd.io/v1beta2
|
||||
kind: ImageRepository
|
||||
metadata:
|
||||
name: lcg
|
||||
namespace: flux-system
|
||||
spec:
|
||||
image: kuznetcovay/lcg
|
||||
interval: 3m
|
||||
secretRef:
|
||||
name: regcred
|
||||
|
||||
---
|
||||
|
||||
apiVersion: image.toolkit.fluxcd.io/v1beta2
|
||||
kind: ImagePolicy
|
||||
metadata:
|
||||
name: lcg
|
||||
namespace: flux-system
|
||||
spec:
|
||||
imageRepositoryRef:
|
||||
name: lcg
|
||||
policy:
|
||||
semver:
|
||||
range: '>=1.0.0'
|
||||
|
||||
---
|
||||
91
deploy/3.deployment.tmpl.yaml
Normal file
91
deploy/3.deployment.tmpl.yaml
Normal file
@@ -0,0 +1,91 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: lcg
|
||||
namespace: lcg
|
||||
labels:
|
||||
app: lcg
|
||||
version: ${VERSION}
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: lcg
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: lcg
|
||||
spec:
|
||||
containers:
|
||||
- name: lcg
|
||||
image: ${REPOSITORY}:${VERSION}
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
protocol: TCP
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: lcg-config
|
||||
- secretRef:
|
||||
name: lcg-secrets
|
||||
env:
|
||||
# Pod information
|
||||
- name: POD_NAME
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.name
|
||||
- name: POD_NAMESPACE
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.namespace
|
||||
- name: NODE_NAME
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: spec.nodeName
|
||||
resources:
|
||||
limits:
|
||||
cpu: 1000m
|
||||
memory: 1Gi
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 512Mi
|
||||
volumeMounts:
|
||||
- name: lcg-data
|
||||
mountPath: /app/data
|
||||
- name: lcg-config
|
||||
mountPath: /app/config
|
||||
readOnly: true
|
||||
# Health checks
|
||||
startupProbe:
|
||||
tcpSocket:
|
||||
port: 8080
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 5
|
||||
failureThreshold: 30
|
||||
readinessProbe:
|
||||
tcpSocket:
|
||||
port: 8080
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
port: 8080
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 60
|
||||
volumes:
|
||||
- name: lcg-data
|
||||
persistentVolumeClaim:
|
||||
claimName: lcg-data
|
||||
- name: lcg-config
|
||||
configMap:
|
||||
name: lcg-config
|
||||
# Security context
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1001
|
||||
readOnlyRootFilesystem: false
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
restartPolicy: Always
|
||||
114
deploy/4.build-binaries.sh
Executable file
114
deploy/4.build-binaries.sh
Executable file
@@ -0,0 +1,114 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 🚀 LCG Binary Build Script
|
||||
# Скрипт для сборки бинарных файлов с помощью goreleaser на хосте
|
||||
|
||||
set -e
|
||||
|
||||
# Цвета для вывода
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Функция для вывода сообщений
|
||||
log() {
|
||||
echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1"
|
||||
}
|
||||
|
||||
success() {
|
||||
echo -e "${GREEN}✅ $1${NC}"
|
||||
}
|
||||
|
||||
warning() {
|
||||
echo -e "${YELLOW}⚠️ $1${NC}"
|
||||
}
|
||||
|
||||
error() {
|
||||
echo -e "${RED}❌ $1${NC}"
|
||||
}
|
||||
|
||||
# Параметры
|
||||
VERSION=${1:-"dev"}
|
||||
# CLEAN=${2:-"true"}
|
||||
|
||||
# Записываем версию в файл VERSION.txt (в корневой директории проекта)
|
||||
echo "$VERSION" > VERSION.txt
|
||||
log "📝 Версия записана в VERSION.txt: $VERSION"
|
||||
|
||||
log "🚀 Сборка бинарных файлов LCG с goreleaser..."
|
||||
|
||||
# Проверяем наличие goreleaser
|
||||
if ! command -v goreleaser &> /dev/null; then
|
||||
error "goreleaser не найден. Установите goreleaser:"
|
||||
echo " curl -sL https://github.com/goreleaser/goreleaser/releases/latest/download/goreleaser_Linux_x86_64.tar.gz | tar -xz -C /usr/local/bin goreleaser"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Проверяем наличие Go
|
||||
if ! command -v go &> /dev/null; then
|
||||
error "Go не найден. Установите Go для сборки."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Переходим в корневую директорию проекта
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
log "📁 Рабочая директория: $(pwd)"
|
||||
log "📁 Папка dist будет создана в: $(pwd)/dist"
|
||||
|
||||
# Очищаем предыдущие сборки если нужно
|
||||
# if [ "$CLEAN" = "true" ]; then
|
||||
# log "🧹 Очистка предыдущих сборок..."
|
||||
# rm -rf dist/
|
||||
# goreleaser clean
|
||||
# fi
|
||||
|
||||
# Проверяем наличие .goreleaser.yaml
|
||||
if [ ! -f "deploy/.goreleaser.yaml" ]; then
|
||||
error "Файл .goreleaser.yaml не найден в папке deploy/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Копируем конфигурацию goreleaser в корень проекта
|
||||
log "📋 Копирование конфигурации goreleaser..."
|
||||
cp deploy/.goreleaser.yaml .goreleaser.yaml
|
||||
|
||||
# Устанавливаем переменные окружения для версии
|
||||
export GORELEASER_CURRENT_TAG="$VERSION"
|
||||
|
||||
# Собираем бинарные файлы
|
||||
log "🏗️ Сборка бинарных файлов для всех платформ..."
|
||||
goreleaser build --snapshot --clean
|
||||
|
||||
# Проверяем результат
|
||||
if [ -d "dist" ]; then
|
||||
log "📊 Собранные бинарные файлы:"
|
||||
find dist -name "lcg_*" -type f | while read -r binary; do
|
||||
echo " $binary ($(stat -c%s "$binary") bytes, $(file "$binary" | cut -d: -f2))"
|
||||
done
|
||||
|
||||
success "🎉 Бинарные файлы успешно собраны!"
|
||||
|
||||
# Показываем структуру dist/
|
||||
log "📁 Структура папки dist/:"
|
||||
tree -h dist/ 2>/dev/null || find dist -type f | sort
|
||||
|
||||
else
|
||||
error "Папка dist/ не создана. Проверьте конфигурацию goreleaser."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Очищаем временный файл конфигурации
|
||||
rm -f .goreleaser.yaml
|
||||
|
||||
success "🎉 Сборка бинарных файлов завершена!"
|
||||
|
||||
# Показываем команды для Docker сборки
|
||||
echo ""
|
||||
log "📝 Следующие шаги:"
|
||||
echo " cd deploy"
|
||||
echo " docker buildx build --platform linux/amd64,linux/arm64 --tag your-registry.com/lcg:$VERSION --push ."
|
||||
echo " # или используйте скрипт:"
|
||||
echo " ./5.build-docker.sh your-registry.com/lcg $VERSION"
|
||||
12
deploy/4.pvc.yaml
Normal file
12
deploy/4.pvc.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: lcg-data
|
||||
namespace: lcg
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 5Gi
|
||||
storageClassName: nfs
|
||||
183
deploy/5.build-docker.sh
Executable file
183
deploy/5.build-docker.sh
Executable file
@@ -0,0 +1,183 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 🐳 LCG Docker Build Script
|
||||
# Скрипт для сборки Docker образа с предварительно собранными бинарными файлами
|
||||
|
||||
set -e
|
||||
|
||||
# Цвета для вывода
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Функция для вывода сообщений
|
||||
log() {
|
||||
echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1"
|
||||
}
|
||||
|
||||
success() {
|
||||
echo -e "${GREEN}✅ $1${NC}"
|
||||
}
|
||||
|
||||
warning() {
|
||||
echo -e "${YELLOW}⚠️ $1${NC}"
|
||||
}
|
||||
|
||||
error() {
|
||||
echo -e "${RED}❌ $1${NC}"
|
||||
}
|
||||
|
||||
# Параметры
|
||||
REPOSITORY=${1:-"your-registry.com/lcg"}
|
||||
VERSION=${2:-""}
|
||||
PLATFORMS=${3:-"linux/amd64,linux/arm64"}
|
||||
|
||||
if [ -z "$VERSION" ]; then
|
||||
error "Версия не указана! Использование: $0 <repository> <version>"
|
||||
echo "Пример: $0 your-registry.com/lcg v1.0.0 <platforms>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "🐳 Сборка Docker образа LCG..."
|
||||
|
||||
# Проверяем наличие docker
|
||||
if ! command -v docker &> /dev/null; then
|
||||
error "Docker не найден. Установите Docker для сборки образов."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Проверяем наличие docker buildx
|
||||
if ! docker buildx version &> /dev/null; then
|
||||
error "Docker Buildx не найден. Установите Docker Buildx для мультиплатформенной сборки."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Проверяем наличие бинарных файлов в текущей директории (если запускаем из корня)
|
||||
if [ ! -d "dist" ]; then
|
||||
error "Папка dist/ не найдена. Сначала соберите бинарные файлы:"
|
||||
echo " ./deploy/4.build-binaries.sh $VERSION"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Проверяем наличие бинарных файлов для всех платформ
|
||||
MISSING_BINARIES=()
|
||||
|
||||
# Ищем бинарные файлы с версией в имени
|
||||
AMD64_BINARY=$(find dist -name "*linux_amd64*" -type d | head -1)
|
||||
echo "AMD64_BINARY: $AMD64_BINARY"
|
||||
ARM64_BINARY=$(find dist -name "*linux_arm64*" -type d | head -1)
|
||||
echo "ARM64_BINARY: $ARM64_BINARY"
|
||||
|
||||
# Проверяем наличие бинарных файлов в найденных папках и соответствие версии
|
||||
if [ -n "$AMD64_BINARY" ]; then
|
||||
AMD64_FILE=$(find "$AMD64_BINARY" -name "lcg_*" -type f | head -1)
|
||||
if [ -z "$AMD64_FILE" ]; then
|
||||
AMD64_BINARY=""
|
||||
else
|
||||
# Извлекаем версию из имени файла
|
||||
FILE_VERSION=$(basename "$AMD64_FILE" | sed 's/lcg_//' | sed 's/-SNAPSHOT.*//')
|
||||
# Нормализуем версии для сравнения (убираем префикс 'v' если есть)
|
||||
NORMALIZED_FILE_VERSION=$(echo "$FILE_VERSION" | sed 's/^v//')
|
||||
NORMALIZED_VERSION=$(echo "$VERSION" | sed 's/^v//')
|
||||
if [ "$NORMALIZED_FILE_VERSION" != "$NORMALIZED_VERSION" ]; then
|
||||
error "Версия в имени бинарного файла ($FILE_VERSION) не совпадает с переданной версией ($VERSION)"
|
||||
echo "Файл: $AMD64_FILE"
|
||||
echo "Ожидаемая версия: $VERSION"
|
||||
echo "Версия в файле: $FILE_VERSION"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "$ARM64_BINARY" ]; then
|
||||
ARM64_FILE=$(find "$ARM64_BINARY" -name "lcg_*" -type f | head -1)
|
||||
if [ -z "$ARM64_FILE" ]; then
|
||||
ARM64_BINARY=""
|
||||
else
|
||||
# Извлекаем версию из имени файла
|
||||
FILE_VERSION=$(basename "$ARM64_FILE" | sed 's/lcg_//' | sed 's/-SNAPSHOT.*//')
|
||||
# Нормализуем версии для сравнения (убираем префикс 'v' если есть)
|
||||
NORMALIZED_FILE_VERSION=$(echo "$FILE_VERSION" | sed 's/^v//')
|
||||
NORMALIZED_VERSION=$(echo "$VERSION" | sed 's/^v//')
|
||||
if [ "$NORMALIZED_FILE_VERSION" != "$NORMALIZED_VERSION" ]; then
|
||||
error "Версия в имени бинарного файла ($FILE_VERSION) не совпадает с переданной версией ($VERSION)"
|
||||
echo "Файл: $ARM64_FILE"
|
||||
echo "Ожидаемая версия: $VERSION"
|
||||
echo "Версия в файле: $FILE_VERSION"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$AMD64_BINARY" ]; then
|
||||
MISSING_BINARIES+=("linux/amd64")
|
||||
fi
|
||||
if [ -z "$ARM64_BINARY" ]; then
|
||||
MISSING_BINARIES+=("linux/arm64")
|
||||
fi
|
||||
|
||||
if [ ${#MISSING_BINARIES[@]} -gt 0 ]; then
|
||||
error "Отсутствуют бинарные файлы для платформ: ${MISSING_BINARIES[*]}"
|
||||
echo "Сначала соберите бинарные файлы:"
|
||||
echo " ./4.build-binaries.sh $VERSION"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Показываем найденные файлы и их версии
|
||||
log "📊 Найденные бинарные файлы:"
|
||||
if [ -n "$AMD64_FILE" ]; then
|
||||
echo " AMD64: $AMD64_FILE"
|
||||
fi
|
||||
if [ -n "$ARM64_FILE" ]; then
|
||||
echo " ARM64: $ARM64_FILE"
|
||||
fi
|
||||
|
||||
# Создаем builder если не существует
|
||||
log "🔧 Настройка Docker Buildx..."
|
||||
docker buildx create --name lcg-builder --use 2>/dev/null || docker buildx use lcg-builder
|
||||
|
||||
# Копируем бинарные файлы и файл версии в папку deploy
|
||||
log "📋 Копирование бинарных файлов и файла версии..."
|
||||
cp -r dist ./deploy/dist
|
||||
cp VERSION.txt ./deploy/VERSION.txt 2>/dev/null || echo "dev" > ./deploy/VERSION.txt
|
||||
|
||||
# Сборка для всех платформ
|
||||
log "🏗️ Сборка образа для платформ: $PLATFORMS"
|
||||
log "📦 Репозиторий: $REPOSITORY"
|
||||
log "🏷️ Версия: $VERSION"
|
||||
|
||||
# Сборка и push
|
||||
docker buildx build \
|
||||
--platform "$PLATFORMS" \
|
||||
--tag "$REPOSITORY:$VERSION" \
|
||||
--tag "$REPOSITORY:latest" \
|
||||
--push \
|
||||
--file deploy/Dockerfile \
|
||||
deploy/
|
||||
|
||||
# Очищаем скопированные файлы
|
||||
rm -rf ./deploy/dist
|
||||
|
||||
success "🎉 Образ успешно собран и отправлен в репозиторий!"
|
||||
|
||||
# Показываем информацию о собранном образе
|
||||
log "📊 Информация о собранном образе:"
|
||||
echo " Репозиторий: $REPOSITORY"
|
||||
echo " Версия: $VERSION"
|
||||
echo " Платформы: $PLATFORMS"
|
||||
echo " Теги: $REPOSITORY:$VERSION, $REPOSITORY:latest"
|
||||
|
||||
# Проверяем образы в репозитории
|
||||
log "🔍 Проверка образов в репозитории..."
|
||||
docker buildx imagetools inspect "$REPOSITORY:$VERSION" || warning "Не удалось проверить образ в репозитории"
|
||||
|
||||
success "🎉 Сборка завершена успешно!"
|
||||
|
||||
# Показываем команды для использования
|
||||
echo ""
|
||||
log "📝 Полезные команды:"
|
||||
echo " docker pull $REPOSITORY:$VERSION"
|
||||
echo " docker run -p 8080:8080 $REPOSITORY:$VERSION"
|
||||
echo " docker buildx imagetools inspect $REPOSITORY:$VERSION"
|
||||
204
deploy/6.full-build.sh
Executable file
204
deploy/6.full-build.sh
Executable file
@@ -0,0 +1,204 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 🚀 LCG Full Build Script
|
||||
# Полный скрипт сборки: бинарные файлы + Docker образ
|
||||
|
||||
set -e
|
||||
|
||||
# Цвета для вывода
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Функция для вывода сообщений
|
||||
log() {
|
||||
echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1"
|
||||
}
|
||||
|
||||
success() {
|
||||
echo -e "${GREEN}✅ $1${NC}"
|
||||
}
|
||||
|
||||
warning() {
|
||||
echo -e "${YELLOW}⚠️ $1${NC}"
|
||||
}
|
||||
|
||||
error() {
|
||||
echo -e "${RED}❌ $1${NC}"
|
||||
}
|
||||
|
||||
# Параметры
|
||||
|
||||
REPOSITORY=${1:-"kuznetcovay/lcg"}
|
||||
VERSION=${2:-""}
|
||||
PLATFORMS=${3:-"linux/amd64,linux/arm64"}
|
||||
|
||||
if [ -z "$VERSION" ]; then
|
||||
error "Версия не указана! Использование: $0 <repository> <version> <platforms>"
|
||||
echo "Пример: $0 kuznetcovay/lcg v1.0.0 linux/amd64,linux/arm64"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Записываем версию в файл VERSION.txt (в корневой директории проекта)
|
||||
cd "$(dirname "$0")/.."
|
||||
echo "$VERSION" > VERSION.txt
|
||||
log "📝 Версия записана в VERSION.txt: $VERSION"
|
||||
|
||||
log "🚀 Полная сборка LCG (бинарные файлы + Docker образ)..."
|
||||
|
||||
# Этап 1: Сборка бинарных файлов
|
||||
log "📦 Этап 1: Сборка бинарных файлов с goreleaser..."
|
||||
if ! ./deploy/4.build-binaries.sh "$VERSION"; then
|
||||
error "Ошибка при сборке бинарных файлов"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
success "✅ Бинарные файлы собраны успешно"
|
||||
|
||||
# Этап 2: Сборка Docker образа
|
||||
log "🐳 Этап 2: Сборка Docker образа..."
|
||||
if ! ./deploy/5.build-docker.sh "$REPOSITORY" "$VERSION" "$PLATFORMS"; then
|
||||
error "Ошибка при сборке Docker образа"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
success "✅ Docker образы собраны успешно"
|
||||
|
||||
# Этап 3: Генерация deployment.yaml
|
||||
log "📝 Этап 3: Генерация deployment.yaml..."
|
||||
# Generate deployment.yaml with env substitution
|
||||
export REPOSITORY=$REPOSITORY
|
||||
export VERSION=$VERSION
|
||||
export PLATFORMS=$PLATFORMS
|
||||
export KUBECONFIG="${HOME}/.kube/config_hlab" && kubectx default
|
||||
|
||||
if ! envsubst < deploy/1.configmap.tmpl.yaml > kustomize/configmap.yaml; then
|
||||
error "Ошибка при генерации deploy/1.configmap.yaml"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
success "✅ kustomize/configmap.yaml сгенерирован успешно"
|
||||
|
||||
if ! envsubst < deploy/3.deployment.tmpl.yaml > kustomize/deployment.yaml; then
|
||||
error "Ошибка при генерации kustomize/deployment.yaml"
|
||||
exit 1
|
||||
fi
|
||||
success "✅ kustomize/deployment.yaml сгенерирован успешно"
|
||||
|
||||
if ! envsubst < deploy/ingress-route.tmpl.yaml > kustomize/ingress-route.yaml; then
|
||||
error "Ошибка при генерации kustomize/ingress-route.yaml"
|
||||
exit 1
|
||||
fi
|
||||
success "✅ kustomize/ingress-route.yaml сгенерирован успешно"
|
||||
|
||||
if ! envsubst < deploy/service.tmpl.yaml > kustomize/service.yaml; then
|
||||
error "Ошибка при генерации kustomize/service.yaml"
|
||||
exit 1
|
||||
fi
|
||||
success "✅ kustomize/service.yaml сгенерирован успешно"
|
||||
|
||||
if ! envsubst < deploy/kustomization.tmpl.yaml > kustomize/kustomization.yaml; then
|
||||
error "Ошибка при генерации kustomize/kustomization.yaml"
|
||||
exit 1
|
||||
fi
|
||||
success "✅ kustomize/kustomization.yaml сгенерирован успешно"
|
||||
|
||||
# отключить reconciliation flux
|
||||
if kubectl get kustomization lcg -n flux-system > /dev/null 2>&1; then
|
||||
kubectl patch kustomization lcg -n flux-system --type=merge -p '{"spec":{"suspend":true}}'
|
||||
else
|
||||
echo "ℹ️ Kustomization 'lcg' does not exist in 'flux-system' namespace. Skipping suspend."
|
||||
fi
|
||||
sleep 5
|
||||
|
||||
|
||||
# зафиксировать изменения в текущей ветке, если она не main
|
||||
current_branch=$(git rev-parse --abbrev-ref HEAD)
|
||||
if [ "$current_branch" != "main" ]; then
|
||||
log "🔧 Исправления в текущей ветке: $current_branch"
|
||||
# считать, что изменения уже сделаны
|
||||
git add .
|
||||
git commit -m "Исправления в ветке $current_branch"
|
||||
fi
|
||||
|
||||
# переключиться на ветку main и слить с текущей веткой, если не находимся на main
|
||||
if [ "$current_branch" != "main" ]; then
|
||||
git checkout main
|
||||
git merge --no-ff -m "Merged branch '$current_branch' into main while building $VERSION" "$current_branch"
|
||||
git push origin main
|
||||
elif [ "$current_branch" = "main" ]; then
|
||||
log "🔄 Вы находитесь на ветке main. Слияние с release..."
|
||||
git add .
|
||||
git commit -m "Исправления в ветке $current_branch"
|
||||
git push origin main
|
||||
fi
|
||||
|
||||
# переключиться на ветку release и слить с веткой main
|
||||
if git show-ref --quiet refs/heads/release; then
|
||||
log "ℹ️ Branch 'release' exists. Proceeding with merge."
|
||||
git checkout release
|
||||
git merge --no-ff -m "Merged main into release while building $VERSION" main
|
||||
else
|
||||
log "❌ Branch 'release' does not exist. Please create the branch before proceeding."
|
||||
git checkout -b release
|
||||
git merge --no-ff -m "Merged main into release while building $VERSION" main
|
||||
fi
|
||||
|
||||
# если тег $VERSION существует, удалить его и принудительно запушить
|
||||
tag_exists=$(git tag -l "$VERSION")
|
||||
if [ "$tag_exists" ]; then
|
||||
log "🗑️ Удаление существующего тега $VERSION"
|
||||
git tag -d "$VERSION"
|
||||
git push origin ":refs/tags/$VERSION"
|
||||
fi
|
||||
|
||||
# Create tag $VERSION and push to remote release branch and all tags
|
||||
git tag "$VERSION"
|
||||
git push origin release
|
||||
git push origin --tags
|
||||
|
||||
# Push main branch
|
||||
git checkout main
|
||||
git push origin main
|
||||
|
||||
|
||||
# Включить reconciliation flux
|
||||
if kubectl get kustomization lcg -n flux-system > /dev/null 2>&1; then
|
||||
kubectl patch kustomization lcg -n flux-system --type=merge -p '{"spec":{"suspend":false}}'
|
||||
else
|
||||
echo "ℹ️ Kustomization 'lcg' does not exist in 'flux-system' namespace. Skipping suspend."
|
||||
fi
|
||||
echo "🔄 Flux will automatically deploy $VERSION version in ~4-6 minutes..."
|
||||
|
||||
# Итоговая информация
|
||||
echo ""
|
||||
log "🎉 Полная сборка завершена успешно!"
|
||||
echo ""
|
||||
log "📊 Результат:"
|
||||
echo " Репозиторий: $REPOSITORY"
|
||||
echo " Версия: $VERSION"
|
||||
echo " Платформы: $PLATFORMS"
|
||||
echo " Теги: $REPOSITORY:$VERSION, $REPOSITORY:latest"
|
||||
echo ""
|
||||
echo ""
|
||||
log "🔍 Информация о git коммитах:"
|
||||
git_log=$(git log release -1 --pretty=format:"%H - %s")
|
||||
echo "$git_log"
|
||||
echo ""
|
||||
|
||||
|
||||
log "📝 Команды для использования:"
|
||||
echo " docker pull $REPOSITORY:$VERSION"
|
||||
echo " docker run -p 8080:8080 $REPOSITORY:$VERSION"
|
||||
echo " docker buildx imagetools inspect $REPOSITORY:$VERSION"
|
||||
echo ""
|
||||
log "🔍 Проверка образа:"
|
||||
echo " docker run --rm $REPOSITORY:$VERSION /app/lcg --version"
|
||||
echo ""
|
||||
log "📝 Команды для использования:"
|
||||
|
||||
echo " kubectl get pods"
|
||||
echo " kubectl get services"
|
||||
echo " kubectl get ingress"
|
||||
39
deploy/Dockerfile
Normal file
39
deploy/Dockerfile
Normal file
@@ -0,0 +1,39 @@
|
||||
# Однофазный build для LCG с предварительно собранным бинарным файлом
|
||||
FROM alpine:3.22.2
|
||||
|
||||
# Устанавливаем зависимости
|
||||
RUN apk --no-cache add ca-certificates tzdata
|
||||
|
||||
# Создаем пользователя
|
||||
RUN adduser -D -s /bin/sh lcg
|
||||
|
||||
# Создаем директории и файлы
|
||||
RUN mkdir -p /app/data /app/config /home/lcg/.config/lcg/gpt_results /home/lcg/.config/lcg/gpt_sys_prompts && \
|
||||
echo '[]' > /home/lcg/.config/lcg/gpt_results/lcg_history.json && \
|
||||
chown -R lcg:lcg /app /home/lcg/.config
|
||||
|
||||
# Копируем файл версии
|
||||
COPY VERSION.txt /app/VERSION.txt
|
||||
|
||||
# Копируем предварительно собранный бинарный файл
|
||||
# Ищем папку с бинарным файлом для текущей архитектуры
|
||||
COPY dist/lcg_linux_${TARGETARCH}*/lcg_* /app/lcg
|
||||
|
||||
# Устанавливаем права
|
||||
RUN chmod +x /app/lcg
|
||||
|
||||
# Переключаемся на пользователя lcg
|
||||
USER lcg
|
||||
|
||||
# Устанавливаем рабочую директорию
|
||||
WORKDIR /app
|
||||
|
||||
# Открываем порт
|
||||
EXPOSE 8080
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/login || exit 1
|
||||
|
||||
# Запускаем приложение
|
||||
CMD ["./lcg", "serve", "-H", "0.0.0.0", "-p", "8080"]
|
||||
1
deploy/VERSION.txt
Normal file
1
deploy/VERSION.txt
Normal file
@@ -0,0 +1 @@
|
||||
v2.0.14
|
||||
42
deploy/hpa.yaml
Normal file
42
deploy/hpa.yaml
Normal file
@@ -0,0 +1,42 @@
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: lcg-hpa
|
||||
namespace: lcg
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: lcg
|
||||
minReplicas: 2
|
||||
maxReplicas: 10
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: 70
|
||||
- type: Resource
|
||||
resource:
|
||||
name: memory
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: 80
|
||||
behavior:
|
||||
scaleDown:
|
||||
stabilizationWindowSeconds: 300
|
||||
policies:
|
||||
- type: Percent
|
||||
value: 10
|
||||
periodSeconds: 60
|
||||
scaleUp:
|
||||
stabilizationWindowSeconds: 60
|
||||
policies:
|
||||
- type: Percent
|
||||
value: 50
|
||||
periodSeconds: 60
|
||||
- type: Pods
|
||||
value: 2
|
||||
periodSeconds: 60
|
||||
selectPolicy: Max
|
||||
63
deploy/ingress-route.tmpl.yaml
Normal file
63
deploy/ingress-route.tmpl.yaml
Normal file
@@ -0,0 +1,63 @@
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: IngressRoute
|
||||
metadata:
|
||||
name: lcg-route
|
||||
namespace: lcg
|
||||
labels:
|
||||
app: lcg
|
||||
spec:
|
||||
entryPoints:
|
||||
- websecure
|
||||
routes:
|
||||
- kind: Rule
|
||||
match: Host(`direct-dev.ru`) && PathPrefix(`/lcg`)
|
||||
services:
|
||||
- name: lcg
|
||||
port: 8080
|
||||
tls:
|
||||
secretName: le-root-direct-dev-ru
|
||||
---
|
||||
|
||||
# apiVersion: traefik.io/v1alpha1
|
||||
# kind: IngressRoute
|
||||
# metadata:
|
||||
# name: lcg-route
|
||||
# namespace: lcg
|
||||
# spec:
|
||||
# entryPoints:
|
||||
# - websecure
|
||||
# routes:
|
||||
# - kind: Rule
|
||||
# match: Host(`direct-dev.ru`) && PathPrefix(`/lcg`)
|
||||
# services:
|
||||
# - name: lcg
|
||||
# port: 8080
|
||||
# middlewares:
|
||||
# - name: lcg-strip-prefix
|
||||
# tls:
|
||||
# secretName: le-root-direct-dev-ru
|
||||
# ---
|
||||
# apiVersion: traefik.io/v1alpha1
|
||||
# kind: Middleware
|
||||
# metadata:
|
||||
# name: lcg-strip-prefix
|
||||
# namespace: lcg
|
||||
# spec:
|
||||
# stripPrefix:
|
||||
# prefixes:
|
||||
# - /lcg
|
||||
# ---
|
||||
# apiVersion: traefik.io/v1alpha1
|
||||
# kind: Middleware
|
||||
# metadata:
|
||||
# name: lcg-headers
|
||||
# namespace: lcg
|
||||
# spec:
|
||||
# headers:
|
||||
# customRequestHeaders:
|
||||
# X-Forwarded-Proto: "https"
|
||||
# X-Forwarded-Port: "443"
|
||||
# customResponseHeaders:
|
||||
# X-Frame-Options: "DENY"
|
||||
# X-Content-Type-Options: "nosniff"
|
||||
# X-XSS-Protection: "1; mode=block"
|
||||
25
deploy/kustomization.tmpl.yaml
Normal file
25
deploy/kustomization.tmpl.yaml
Normal file
@@ -0,0 +1,25 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
# Namespace
|
||||
namespace: lcg
|
||||
|
||||
# Resources
|
||||
resources:
|
||||
- configmap.yaml
|
||||
- secret.yaml
|
||||
- deployment.yaml
|
||||
- service.yaml
|
||||
- ingress-route.yaml
|
||||
|
||||
# Common labels
|
||||
# commonLabels:
|
||||
# app: lcg
|
||||
# version: ${VERSION}
|
||||
# managed-by: kustomize
|
||||
|
||||
# Images
|
||||
# images:
|
||||
# - name: lcg
|
||||
# newName: ${REPOSITORY}
|
||||
# newTag: ${VERSION}
|
||||
170
deploy/release-goreleaser.sh
Normal file
170
deploy/release-goreleaser.sh
Normal file
@@ -0,0 +1,170 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# release-goreleaser.sh
|
||||
# Копирует deploy/.goreleaser.yaml в корень, запускает релиз и удаляет файл.
|
||||
#
|
||||
# Использование:
|
||||
# deploy/release-goreleaser.sh # обычный релиз на GitHub (нужен GITHUB_TOKEN)
|
||||
# deploy/release-goreleaser.sh --snapshot # локальный снепшот без публикации
|
||||
|
||||
ROOT_DIR="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
||||
SRC_CFG="$ROOT_DIR/deploy/.goreleaser.yaml"
|
||||
DST_CFG="$ROOT_DIR/.goreleaser.yaml"
|
||||
|
||||
log() { echo -e "\033[36m[release]\033[0m $*"; }
|
||||
err() { echo -e "\033[31m[error]\033[0m $*" >&2; }
|
||||
|
||||
if ! command -v goreleaser >/dev/null 2>&1; then
|
||||
err "goreleaser не найден. Установите: https://goreleaser.com/install/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$SRC_CFG" ]]; then
|
||||
err "Не найден файл конфигурации: $SRC_CFG"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MODE="release"
|
||||
if [[ "${1:-}" == "--snapshot" ]]; then
|
||||
MODE="snapshot"
|
||||
shift || true
|
||||
fi
|
||||
|
||||
if [[ -f "$DST_CFG" ]]; then
|
||||
err "В корне уже существует .goreleaser.yaml. Удалите/переименуйте перед запуском."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cleanup() {
|
||||
if [[ -f "$DST_CFG" ]]; then
|
||||
rm -f "$DST_CFG" || true
|
||||
log "Удалил временный $DST_CFG"
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
log "Копирую конфиг: $SRC_CFG -> $DST_CFG"
|
||||
cp "$SRC_CFG" "$DST_CFG"
|
||||
|
||||
pushd "$ROOT_DIR" >/dev/null
|
||||
|
||||
EXTRA_FLAGS=()
|
||||
PREV_HEAD="$(git rev-parse HEAD 2>/dev/null || echo "")"
|
||||
|
||||
git add .
|
||||
git commit --amend --no-edit || true
|
||||
|
||||
## Версию берём из deploy/VERSION.txt или VERSION.txt в корне
|
||||
VERSION_FILE="$ROOT_DIR/deploy/VERSION.txt"
|
||||
[[ -f "$VERSION_FILE" ]] || VERSION_FILE="$ROOT_DIR/VERSION.txt"
|
||||
if [[ -f "$VERSION_FILE" ]]; then
|
||||
VERSION_RAW="$(head -n1 "$VERSION_FILE" | tr -d ' \t\r\n')"
|
||||
if [[ -n "$VERSION_RAW" ]]; then
|
||||
TAG="$VERSION_RAW"
|
||||
[[ "$TAG" == v* ]] || TAG="v$TAG"
|
||||
export GORELEASER_CURRENT_TAG="$TAG"
|
||||
log "Версия релиза: $TAG (из $(realpath --relative-to="$ROOT_DIR" "$VERSION_FILE" 2>/dev/null || echo "$VERSION_FILE"))"
|
||||
fi
|
||||
fi
|
||||
|
||||
create_and_push_tag() {
|
||||
local tag="$1"
|
||||
if git rev-parse "$tag" >/dev/null 2>&1; then
|
||||
log "Git tag уже существует: $tag"
|
||||
else
|
||||
log "Создаю git tag: $tag"
|
||||
git tag -a "$tag" -m "Release $tag"
|
||||
if [[ "${NO_GIT_PUSH:-false}" != "true" ]]; then
|
||||
log "Пушу тег $tag на origin"
|
||||
git push origin "$tag"
|
||||
else
|
||||
log "Пропущен пуш тега (NO_GIT_PUSH=true)"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
move_tag_to_head() {
|
||||
local tag="$1"
|
||||
if [[ -z "$tag" ]]; then
|
||||
return 0
|
||||
fi
|
||||
if git rev-parse "$tag" >/dev/null 2>&1; then
|
||||
log "Переношу тег $tag на текущий коммит (HEAD)"
|
||||
git tag -f "$tag" HEAD
|
||||
if [[ "${NO_GIT_PUSH:-false}" != "true" ]]; then
|
||||
log "Форс‑пуш тега $tag на origin"
|
||||
git push -f origin "$tag"
|
||||
else
|
||||
log "Пропущен пуш тега (NO_GIT_PUSH=true)"
|
||||
fi
|
||||
else
|
||||
log "Тега $tag нет — пропускаю перенос"
|
||||
fi
|
||||
}
|
||||
|
||||
fetch_token_from_k8s() {
|
||||
export KUBECONFIG=/home/su/.kube/config_hlab
|
||||
local ns="${K8S_NAMESPACE:-flux-system}"
|
||||
local name="${K8S_SECRET_NAME:-git-secrets}"
|
||||
# Предпочитаем jq (как в примере), при отсутствии используем jsonpath + base64 -d
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
kubectl get secret "$name" -n "$ns" -o json \
|
||||
| jq -r '.data.password | @base64d'
|
||||
else
|
||||
kubectl get secret "$name" -n "$ns" -o jsonpath='{.data.password}' \
|
||||
| base64 -d 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ "$MODE" == "snapshot" ]]; then
|
||||
log "Запуск goreleaser (snapshot, без публикации)"
|
||||
goreleaser release --snapshot --clean --config "$DST_CFG" "${EXTRA_FLAGS[@]}"
|
||||
else
|
||||
# Если версия определена и тега нет — создадим (goreleaser ориентируется на теги)
|
||||
if [[ -n "${GORELEASER_CURRENT_TAG:-}" ]]; then
|
||||
create_and_push_tag "$GORELEASER_CURRENT_TAG"
|
||||
# Перемещаем тег на текущий HEAD (если существовал ранее, закрепим на последнем коммите)
|
||||
move_tag_to_head "$GORELEASER_CURRENT_TAG"
|
||||
else
|
||||
# Если версия не задана, попробуем взять последний существующий тег и перенести его на HEAD
|
||||
LAST_TAG="$(git describe --tags --abbrev=0 2>/dev/null || true)"
|
||||
if [[ -n "$LAST_TAG" ]]; then
|
||||
move_tag_to_head "$LAST_TAG"
|
||||
export GORELEASER_CURRENT_TAG="$LAST_TAG"
|
||||
log "Использую последний тег: $LAST_TAG"
|
||||
fi
|
||||
fi
|
||||
if [[ -z "${GITHUB_TOKEN:-}" ]]; then
|
||||
log "GITHUB_TOKEN не задан — пробую получить из k8s секрета (${K8S_NAMESPACE:-flux-system}/${K8S_SECRET_NAME:-git-secrets}, ключ: password)"
|
||||
if ! command -v kubectl >/dev/null 2>&1; then
|
||||
err "kubectl не найден, а GITHUB_TOKEN не задан. Установите kubectl или экспортируйте GITHUB_TOKEN."
|
||||
exit 1
|
||||
fi
|
||||
TOKEN_FROM_K8S="$(fetch_token_from_k8s || true)"
|
||||
if [[ -n "$TOKEN_FROM_K8S" && "$TOKEN_FROM_K8S" != "null" ]]; then
|
||||
export GITHUB_TOKEN="$TOKEN_FROM_K8S"
|
||||
log "GITHUB_TOKEN получен из секрета Kubernetes."
|
||||
else
|
||||
err "Не удалось получить GITHUB_TOKEN из секрета Kubernetes. Экспортируйте GITHUB_TOKEN и повторите."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
log "Запуск goreleaser (публикация на GitHub)"
|
||||
goreleaser release --clean --config "$DST_CFG" "${EXTRA_FLAGS[@]}"
|
||||
fi
|
||||
|
||||
popd >/dev/null
|
||||
|
||||
# Откатываем временный коммит, если он был
|
||||
if [[ "${TEMP_COMMIT_DONE:-false}" == "true" && -n "$PREV_HEAD" ]]; then
|
||||
if git reset --soft "$PREV_HEAD" >/dev/null 2>&1; then
|
||||
log "Откатил временный коммит"
|
||||
else
|
||||
log "Не удалось откатить временный коммит — проверьте историю вручную"
|
||||
fi
|
||||
fi
|
||||
|
||||
log "Готово."
|
||||
|
||||
|
||||
16
deploy/service.tmpl.yaml
Normal file
16
deploy/service.tmpl.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: lcg
|
||||
namespace: lcg
|
||||
labels:
|
||||
app: lcg
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- port: 8080
|
||||
targetPort: 8080
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
app: lcg
|
||||
232
docs/CONFIG_COMMAND.md
Normal file
232
docs/CONFIG_COMMAND.md
Normal file
@@ -0,0 +1,232 @@
|
||||
# 🔧 Команда config - Управление конфигурацией
|
||||
|
||||
## 📋 Описание
|
||||
|
||||
Команда `config` позволяет просматривать текущую конфигурацию приложения, включая все настройки, переменные окружения и значения по умолчанию.
|
||||
|
||||
## 🚀 Использование
|
||||
|
||||
### Краткий вывод конфигурации (по умолчанию)
|
||||
|
||||
```bash
|
||||
lcg config
|
||||
# или
|
||||
lcg co
|
||||
```
|
||||
|
||||
**Вывод:**
|
||||
|
||||
``` text
|
||||
Provider: ollama
|
||||
Host: http://192.168.87.108:11434/
|
||||
Model: hf.co/yandex/YandexGPT-5-Lite-8B-instruct-GGUF:Q4_K_M
|
||||
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.
|
||||
Timeout: 300 seconds
|
||||
```
|
||||
|
||||
### Полный вывод конфигурации
|
||||
|
||||
```bash
|
||||
lcg config --full
|
||||
# или
|
||||
lcg config -f
|
||||
# или
|
||||
lcg co --full
|
||||
# или
|
||||
lcg co -f
|
||||
```
|
||||
|
||||
**Вывод (JSON формат):**
|
||||
|
||||
```json
|
||||
{
|
||||
"cwd": "/home/user/projects/golang/linux-command-gpt",
|
||||
"host": "http://192.168.87.108:11434/",
|
||||
"proxy_url": "/api/v1/protected/sberchat/chat",
|
||||
"completions": "api/chat",
|
||||
"model": "hf.co/yandex/YandexGPT-5-Lite-8B-instruct-GGUF:Q4_K_M",
|
||||
"prompt": "Reply with linux command and nothing else. Output with plain response - no need formatting. No need explanation. No need code blocks. No need ` symbols.",
|
||||
"api_key_file": ".openai_api_key",
|
||||
"result_folder": "/home/user/.config/lcg/gpt_results",
|
||||
"prompt_folder": "/home/user/.config/lcg/gpt_sys_prompts",
|
||||
"provider_type": "ollama",
|
||||
"jwt_token": "***not set***",
|
||||
"prompt_id": "1",
|
||||
"timeout": "300",
|
||||
"result_history": "/home/user/.config/lcg/gpt_results/lcg_history.json",
|
||||
"no_history_env": "",
|
||||
"allow_execution": false,
|
||||
"main_flags": {
|
||||
"file": "",
|
||||
"no_history": false,
|
||||
"sys": "",
|
||||
"prompt_id": 0,
|
||||
"timeout": 0,
|
||||
"debug": false
|
||||
},
|
||||
"server": {
|
||||
"port": "8080",
|
||||
"host": "localhost"
|
||||
},
|
||||
"validation": {
|
||||
"max_system_prompt_length": 1000,
|
||||
"max_user_message_length": 2000,
|
||||
"max_prompt_name_length": 2000,
|
||||
"max_prompt_desc_length": 5000,
|
||||
"max_command_length": 8000,
|
||||
"max_explanation_length": 20000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 📊 Структура полной конфигурации
|
||||
|
||||
### Основные настройки
|
||||
|
||||
- **cwd** - текущая рабочая директория
|
||||
- **host** - адрес API сервера
|
||||
- **proxy_url** - путь к API эндпоинту
|
||||
- **completions** - путь к эндпоинту completions
|
||||
- **model** - используемая модель ИИ
|
||||
- **prompt** - системный промпт по умолчанию
|
||||
- **api_key_file** - файл с API ключом
|
||||
- **result_folder** - папка для сохранения результатов
|
||||
- **prompt_folder** - папка с системными промптами
|
||||
- **provider_type** - тип провайдера (ollama/proxy)
|
||||
- **jwt_token** - статус JWT токена (***set***/***from file***/***not set***)
|
||||
- **prompt_id** - ID промпта по умолчанию
|
||||
- **timeout** - таймаут запросов в секундах
|
||||
- **result_history** - файл истории запросов
|
||||
- **no_history_env** - переменная окружения для отключения истории
|
||||
- **allow_execution** - разрешение выполнения команд
|
||||
|
||||
### Флаги командной строки (main_flags)
|
||||
|
||||
- **file** - файл для чтения
|
||||
- **no_history** - отключение истории
|
||||
- **sys** - системный промпт
|
||||
- **prompt_id** - ID промпта
|
||||
- **timeout** - таймаут
|
||||
- **debug** - отладочный режим
|
||||
|
||||
### Настройки сервера (server)
|
||||
|
||||
- **port** - порт веб-сервера
|
||||
- **host** - хост веб-сервера
|
||||
|
||||
### Настройки валидации (validation)
|
||||
|
||||
- **max_system_prompt_length** - максимальная длина системного промпта
|
||||
- **max_user_message_length** - максимальная длина пользовательского сообщения
|
||||
- **max_prompt_name_length** - максимальная длина названия промпта
|
||||
- **max_prompt_desc_length** - максимальная длина описания промпта
|
||||
- **max_command_length** - максимальная длина команды/ответа
|
||||
- **max_explanation_length** - максимальная длина объяснения
|
||||
|
||||
## 🔒 Безопасность
|
||||
|
||||
При выводе полной конфигурации чувствительные данные маскируются:
|
||||
|
||||
- **JWT токены** - показывается статус (***set***/***from file***/***not set***)
|
||||
- **API ключи** - не выводятся в открытом виде
|
||||
- **Пароли** - не сохраняются в конфигурации
|
||||
|
||||
## 📝 Примеры использования
|
||||
|
||||
### Просмотр текущих настроек
|
||||
|
||||
```bash
|
||||
# Краткий вывод
|
||||
lcg config
|
||||
|
||||
# Полный вывод
|
||||
lcg config --full
|
||||
```
|
||||
|
||||
### Проверка настроек валидации
|
||||
|
||||
```bash
|
||||
# Показать только настройки валидации
|
||||
lcg config --full | jq '.validation'
|
||||
```
|
||||
|
||||
### Проверка настроек сервера
|
||||
|
||||
```bash
|
||||
# Показать только настройки сервера
|
||||
lcg config --full | jq '.server'
|
||||
```
|
||||
|
||||
### Проверка переменных окружения
|
||||
|
||||
```bash
|
||||
# Показать все переменные окружения LCG
|
||||
env | grep LCG
|
||||
```
|
||||
|
||||
## 🔧 Интеграция с другими инструментами
|
||||
|
||||
### Использование с jq
|
||||
|
||||
```bash
|
||||
# Получить только модель
|
||||
lcg config --full | jq -r '.model'
|
||||
|
||||
# Получить настройки валидации
|
||||
lcg config --full | jq '.validation'
|
||||
|
||||
# Получить все пути
|
||||
lcg config --full | jq '{result_folder, prompt_folder, result_history}'
|
||||
```
|
||||
|
||||
### Использование с grep
|
||||
|
||||
```bash
|
||||
# Найти все настройки с "timeout"
|
||||
lcg config --full | grep -i timeout
|
||||
|
||||
# Найти все пути
|
||||
lcg config --full | grep -E "(folder|history)"
|
||||
```
|
||||
|
||||
### Сохранение конфигурации в файл
|
||||
|
||||
```bash
|
||||
# Сохранить полную конфигурацию
|
||||
lcg config --full > config.json
|
||||
|
||||
# Сохранить только настройки валидации
|
||||
lcg config --full | jq '.validation' > validation.json
|
||||
```
|
||||
|
||||
## 🐛 Отладка
|
||||
|
||||
### Проверка загрузки конфигурации
|
||||
|
||||
```bash
|
||||
# Показать все настройки
|
||||
lcg config --full
|
||||
|
||||
# Проверить переменные окружения
|
||||
env | grep LCG
|
||||
|
||||
# Проверить файлы конфигурации
|
||||
ls -la ~/.config/lcg/
|
||||
```
|
||||
|
||||
### Типичные проблемы
|
||||
|
||||
1. **Неправильные пути** - проверьте `result_folder` и `prompt_folder`
|
||||
2. **Отсутствующие токены** - проверьте `jwt_token` статус
|
||||
3. **Неправильные лимиты** - проверьте секцию `validation`
|
||||
|
||||
## 📚 Связанные команды
|
||||
|
||||
- `lcg --help` - общая справка
|
||||
- `lcg config --help` - справка по команде config
|
||||
- `lcg serve` - запуск веб-сервера
|
||||
- `lcg prompts list` - список промптов
|
||||
|
||||
---
|
||||
|
||||
**Примечание**: Команда `config` показывает актуальное состояние конфигурации после применения всех переменных окружения и значений по умолчанию.
|
||||
231
docs/CSRF_TESTING_GUIDE.md
Normal file
231
docs/CSRF_TESTING_GUIDE.md
Normal file
@@ -0,0 +1,231 @@
|
||||
# 🛡️ Руководство по тестированию CSRF защиты
|
||||
|
||||
## 📋 Обзор
|
||||
|
||||
Это руководство поможет вам протестировать CSRF защиту в LCG приложении и понять, как работают CSRF атаки.
|
||||
|
||||
## 🚀 Быстрый старт
|
||||
|
||||
### 1. Запуск сервера с CSRF защитой
|
||||
|
||||
```bash
|
||||
# Запуск с аутентификацией и CSRF защитой
|
||||
LCG_SERVER_REQUIRE_AUTH=true ./lcg serve -p 8080
|
||||
```
|
||||
|
||||
### 2. Автоматическое тестирование
|
||||
|
||||
```bash
|
||||
# Запуск автоматических тестов
|
||||
./test_csrf.sh
|
||||
```
|
||||
|
||||
### 3. Ручное тестирование
|
||||
|
||||
```bash
|
||||
# Откройте в браузере
|
||||
open csrf_test.html
|
||||
```
|
||||
|
||||
## 🧪 Типы тестов
|
||||
|
||||
### ✅ **Тест 1: Защищенные запросы**
|
||||
|
||||
- **Цель**: Проверить, что POST запросы без CSRF токена блокируются
|
||||
- **Ожидаемый результат**: 403 Forbidden
|
||||
- **Endpoints**: `/api/execute`, `/api/save-result`, `/api/add-to-history`
|
||||
|
||||
### ✅ **Тест 2: Разрешенные запросы**
|
||||
|
||||
- **Цель**: Проверить, что GET запросы работают
|
||||
- **Ожидаемый результат**: 200 OK
|
||||
- **Endpoints**: `/login`, `/`, `/history`
|
||||
|
||||
### ✅ **Тест 3: CSRF токены**
|
||||
|
||||
- **Цель**: Проверить наличие CSRF токенов в формах
|
||||
- **Ожидаемый результат**: Токены присутствуют в HTML
|
||||
|
||||
### ✅ **Тест 4: Поддельные токены**
|
||||
|
||||
- **Цель**: Проверить защиту от поддельных токенов
|
||||
- **Ожидаемый результат**: 403 Forbidden
|
||||
|
||||
## 🎯 Сценарии атак
|
||||
|
||||
### **Сценарий 1: Выполнение команд**
|
||||
|
||||
```html
|
||||
<!-- Злонамеренная форма -->
|
||||
<form action="http://localhost:8080/api/execute" method="POST">
|
||||
<input type="hidden" name="prompt" value="rm -rf /">
|
||||
<input type="hidden" name="system_id" value="1">
|
||||
<button type="submit">Нажми меня!</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
### **Сценарий 2: Сохранение данных**
|
||||
|
||||
```html
|
||||
<!-- Злонамеренная форма -->
|
||||
<form action="http://localhost:8080/api/save-result" method="POST">
|
||||
<input type="hidden" name="result" value="Вредоносные данные">
|
||||
<input type="hidden" name="command" value="malicious_command">
|
||||
<button type="submit">Сохранить</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
### **Сценарий 3: JavaScript атака**
|
||||
|
||||
```javascript
|
||||
// Злонамеренный JavaScript
|
||||
fetch('http://localhost:8080/api/execute', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({prompt: 'whoami', system_id: '1'})
|
||||
});
|
||||
```
|
||||
|
||||
## 🔍 Анализ результатов
|
||||
|
||||
### **✅ Защита работает, если:**
|
||||
|
||||
- Все POST запросы возвращают 403 Forbidden
|
||||
- В ответах есть "CSRF token required"
|
||||
- GET запросы работают нормально
|
||||
- CSRF токены присутствуют в формах
|
||||
|
||||
### **❌ Уязвимость есть, если:**
|
||||
|
||||
- POST запросы выполняются успешно (200 OK)
|
||||
- Команды выполняются на сервере
|
||||
- Данные сохраняются без CSRF токенов
|
||||
- Нет проверки Origin/Referer заголовков
|
||||
|
||||
## 🛠️ Инструменты тестирования
|
||||
|
||||
### **1. Автоматический скрипт**
|
||||
|
||||
```bash
|
||||
./test_csrf.sh
|
||||
```
|
||||
|
||||
- Тестирует все основные endpoints
|
||||
- Проверяет CSRF токены
|
||||
- Выводит подробный отчет
|
||||
|
||||
### **2. HTML тестовая страница**
|
||||
|
||||
```bash
|
||||
open csrf_test.html
|
||||
```
|
||||
|
||||
- Интерактивное тестирование
|
||||
- Визуальная проверка результатов
|
||||
- Тестирование в браузере
|
||||
|
||||
### **3. Демонстрационная атака**
|
||||
|
||||
```bash
|
||||
open csrf_demo.html
|
||||
```
|
||||
|
||||
- Показывает, как работают CSRF атаки
|
||||
- Демонстрирует уязвимости
|
||||
- Образовательные цели
|
||||
|
||||
## 🔧 Настройка тестов
|
||||
|
||||
### **Переменные окружения для тестирования:**
|
||||
|
||||
```bash
|
||||
# Включить аутентификацию
|
||||
LCG_SERVER_REQUIRE_AUTH=true
|
||||
|
||||
# Настроить CSRF защиту
|
||||
LCG_COOKIE_SECURE=false
|
||||
LCG_DOMAIN=.localhost
|
||||
LCG_COOKIE_PATH=/
|
||||
|
||||
# Запуск сервера
|
||||
./lcg serve -H 0.0.0.0 -p 8080
|
||||
```
|
||||
|
||||
### **Настройка reverse proxy для тестирования:**
|
||||
|
||||
```bash
|
||||
# Для тестирования за reverse proxy
|
||||
LCG_SERVER_REQUIRE_AUTH=true \
|
||||
LCG_SERVER_ALLOW_HTTP=true \
|
||||
LCG_DOMAIN=.yourdomain.com \
|
||||
LCG_COOKIE_PATH=/lcg \
|
||||
LCG_COOKIE_SECURE=false \
|
||||
./lcg serve -H 0.0.0.0 -p 8080
|
||||
```
|
||||
|
||||
## 📊 Интерпретация результатов
|
||||
|
||||
### **Успешные тесты:**
|
||||
|
||||
``` text
|
||||
✅ CSRF защита /api/execute: PASS - Запрос заблокирован (403 Forbidden)
|
||||
✅ CSRF защита /api/save-result: PASS - Запрос заблокирован (403 Forbidden)
|
||||
✅ CSRF защита /api/add-to-history: PASS - Запрос заблокирован (403 Forbidden)
|
||||
✅ GET запросы: PASS - GET запросы работают (HTTP 200)
|
||||
✅ CSRF токен на странице входа: PASS - Токен найден
|
||||
✅ CSRF защита от поддельного токена: PASS - Поддельный токен заблокирован (403 Forbidden)
|
||||
```
|
||||
|
||||
### **Проблемные тесты:**
|
||||
|
||||
``` text
|
||||
❌ CSRF защита /api/execute: FAIL - Запрос прошел (HTTP 200)
|
||||
❌ CSRF защита /api/save-result: FAIL - Запрос прошел (HTTP 200)
|
||||
❌ CSRF токен на странице входа: FAIL - Токен не найден
|
||||
```
|
||||
|
||||
## 🚨 Частые проблемы
|
||||
|
||||
### **1. Cookies не работают**
|
||||
|
||||
- Проверьте настройки `LCG_DOMAIN`
|
||||
- Убедитесь, что `LCG_COOKIE_PATH` правильный
|
||||
- Проверьте настройки reverse proxy
|
||||
|
||||
### **2. CSRF токены не генерируются**
|
||||
|
||||
- Убедитесь, что `LCG_SERVER_REQUIRE_AUTH=true`
|
||||
- Проверьте инициализацию CSRF менеджера
|
||||
- Проверьте логи сервера
|
||||
|
||||
### **3. Запросы проходят без токенов**
|
||||
|
||||
- Проверьте middleware в `serve/middleware.go`
|
||||
- Убедитесь, что CSRF middleware применяется
|
||||
- Проверьте исключения в middleware
|
||||
|
||||
## 📝 Рекомендации
|
||||
|
||||
### **Для разработчиков:**
|
||||
|
||||
1. Всегда тестируйте CSRF защиту
|
||||
2. Используйте автоматические тесты
|
||||
3. Проверяйте все POST endpoints
|
||||
4. Валидируйте CSRF токены
|
||||
|
||||
### **Для администраторов:**
|
||||
|
||||
1. Регулярно запускайте тесты
|
||||
2. Мониторьте логи на подозрительную активность
|
||||
3. Настройте правильные заголовки в reverse proxy
|
||||
4. Используйте HTTPS в продакшене
|
||||
|
||||
## 🎓 Образовательные материалы
|
||||
|
||||
- **OWASP CSRF Prevention Cheat Sheet**: <https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html>
|
||||
- **CSRF атаки**: <https://owasp.org/www-community/attacks/csrf>
|
||||
- **SameSite cookies**: <https://web.dev/samesite-cookies-explained/>
|
||||
|
||||
---
|
||||
|
||||
**⚠️ ВНИМАНИЕ**: Эти тесты предназначены только для проверки безопасности вашего собственного приложения. Не используйте их для атак на чужие системы!
|
||||
@@ -133,10 +133,15 @@ 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`
|
||||
- Browse results at `http://localhost:8080/` (or `http://localhost:8080<BASE_PATH>/` if `LCG_BASE_URL` set)
|
||||
- Execute requests at `.../run`
|
||||
- Manage prompts at `.../prompts`
|
||||
- View history at `.../history`
|
||||
|
||||
Notes:
|
||||
- Base path: set `LCG_BASE_URL` (e.g. `/lcg`) to prefix all routes and API.
|
||||
- Custom 404: unknown paths under base path render a modern 404 page.
|
||||
- Debug: enable via flag `--debug` or env `LCG_DEBUG=1|true`.
|
||||
|
||||
**REST API:**
|
||||
|
||||
337
docs/RELEASE_GUIDE.md
Normal file
337
docs/RELEASE_GUIDE.md
Normal file
@@ -0,0 +1,337 @@
|
||||
# 🚀 Гайд по созданию релизов с помощью GoReleaser
|
||||
|
||||
Этот документ описывает процесс создания релизов для проекта `linux-command-gpt` с использованием GoReleaser.
|
||||
|
||||
## 📋 Содержание
|
||||
|
||||
- [Установка GoReleaser](#установка-goreleaser)
|
||||
- [Конфигурация](#конфигурация)
|
||||
- [Процесс создания релиза](#процесс-создания-релиза)
|
||||
- [Автоматизация](#автоматизация)
|
||||
- [Устранение проблем](#устранение-проблем)
|
||||
|
||||
## 🔧 Установка GoReleaser
|
||||
|
||||
### Linux/macOS
|
||||
|
||||
```bash
|
||||
# Скачать и установить последнюю версию
|
||||
curl -sL https://git.io/goreleaser | bash
|
||||
|
||||
# Или через Homebrew (macOS)
|
||||
brew install goreleaser
|
||||
|
||||
# Или через Go
|
||||
go install github.com/goreleaser/goreleaser@latest
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
```powershell
|
||||
# Через Chocolatey
|
||||
choco install goreleaser
|
||||
|
||||
# Или скачать с GitHub Releases
|
||||
# https://github.com/goreleaser/goreleaser/releases
|
||||
```
|
||||
|
||||
## ⚙️ Конфигурация
|
||||
|
||||
### Файл `.goreleaser.yaml`
|
||||
|
||||
В проекте используется следующая конфигурация GoReleaser:
|
||||
|
||||
```yaml
|
||||
version: 2
|
||||
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
- go generate ./...
|
||||
|
||||
builds:
|
||||
- binary: lcg
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
- arm
|
||||
goos:
|
||||
- linux
|
||||
- darwin
|
||||
|
||||
archives:
|
||||
- formats: [tar.gz]
|
||||
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 }}
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
formats: [zip]
|
||||
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- "^docs:"
|
||||
- "^test:"
|
||||
|
||||
release:
|
||||
footer: >-
|
||||
---
|
||||
Released by [GoReleaser](https://github.com/goreleaser/goreleaser).
|
||||
```
|
||||
|
||||
### Ключевые настройки
|
||||
|
||||
- **builds**: Сборка для Linux, macOS, Windows (amd64, arm64, arm)
|
||||
- **archives**: Создание архивов tar.gz для Unix и zip для Windows
|
||||
- **changelog**: Автоматическое создание changelog из git commits
|
||||
- **release**: Настройки GitHub релиза
|
||||
|
||||
## 🚀 Процесс создания релиза
|
||||
|
||||
### 1. Подготовка
|
||||
|
||||
```bash
|
||||
# Убедитесь, что все изменения закоммичены
|
||||
git status
|
||||
|
||||
# Обновите версию в VERSION.txt
|
||||
echo "v2.0.2" > VERSION.txt
|
||||
|
||||
# Создайте тег
|
||||
git tag v2.0.2
|
||||
git push origin v2.0.2
|
||||
```
|
||||
|
||||
### 2. Настройка переменных окружения
|
||||
|
||||
```bash
|
||||
# Установите GitHub токен
|
||||
export GITHUB_TOKEN="your_github_token_here"
|
||||
|
||||
# Или создайте файл .env
|
||||
echo "GITHUB_TOKEN=your_github_token_here" > .env
|
||||
```
|
||||
|
||||
### 3. Создание релиза
|
||||
|
||||
#### Полный релиз
|
||||
|
||||
```bash
|
||||
# Создать релиз с загрузкой на GitHub
|
||||
goreleaser release
|
||||
|
||||
# Создать релиз без загрузки (только локально)
|
||||
goreleaser release --clean
|
||||
```
|
||||
|
||||
#### Тестовый релиз (snapshot)
|
||||
|
||||
```bash
|
||||
# Создать тестовую сборку
|
||||
goreleaser release --snapshot
|
||||
|
||||
# Тестовая сборка без загрузки
|
||||
goreleaser release --snapshot --clean
|
||||
```
|
||||
|
||||
### 4. Проверка результатов
|
||||
|
||||
После выполнения команды GoReleaser создаст:
|
||||
|
||||
- **Архивы**: `dist/` - готовые архивы для всех платформ
|
||||
- **Чексуммы**: `dist/checksums.txt` - контрольные суммы файлов
|
||||
- **GitHub релиз**: Автоматически созданный релиз на GitHub
|
||||
|
||||
## 🤖 Автоматизация
|
||||
|
||||
### GitHub Actions
|
||||
|
||||
Создайте файл `.github/workflows/release.yml`:
|
||||
|
||||
```yaml
|
||||
name: Release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.21'
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v4
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: latest
|
||||
args: release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
```
|
||||
|
||||
### Локальные скрипты
|
||||
|
||||
В проекте есть готовые скрипты:
|
||||
|
||||
```bash
|
||||
# Предварительная подготовка
|
||||
./shell-code/pre-release.sh
|
||||
|
||||
# Создание релиза
|
||||
./shell-code/release.sh
|
||||
```
|
||||
|
||||
## 📁 Структура релиза
|
||||
|
||||
После создания релиза в директории `dist/` будут созданы:
|
||||
|
||||
```
|
||||
dist/
|
||||
├── artifacts.json # Метаданные артефактов
|
||||
├── CHANGELOG.md # Автоматически созданный changelog
|
||||
├── config.yaml # Конфигурация GoReleaser
|
||||
├── digests.txt # Хеши файлов
|
||||
├── go-lcg_2.0.1_checksums.txt
|
||||
├── go-lcg_Darwin_arm64.tar.gz
|
||||
├── go-lcg_Darwin_x86_64.tar.gz
|
||||
├── go-lcg_Linux_arm64.tar.gz
|
||||
├── go-lcg_Linux_i386.tar.gz
|
||||
├── go-lcg_Linux_x86_64.tar.gz
|
||||
├── go-lcg_Windows_arm64.zip
|
||||
├── go-lcg_Windows_i386.zip
|
||||
├── go-lcg_Windows_x86_64.zip
|
||||
└── metadata.json # Метаданные релиза
|
||||
```
|
||||
|
||||
## 🔍 Устранение проблем
|
||||
|
||||
### Правильные флаги GoReleaser
|
||||
|
||||
**Важно**: В современных версиях GoReleaser флаг `--skip-publish` больше не поддерживается. Используйте:
|
||||
|
||||
- `--clean` - очищает директорию `dist/` перед сборкой
|
||||
- `--snapshot` - создает тестовую сборку без создания тега
|
||||
- `--debug` - подробный вывод для отладки
|
||||
- `--skip-validate` - пропускает валидацию конфигурации
|
||||
|
||||
### Частые ошибки
|
||||
|
||||
#### 1. Ошибка аутентификации GitHub
|
||||
|
||||
```
|
||||
Error: failed to get GitHub token: missing github token
|
||||
```
|
||||
|
||||
**Решение**: Установите `GITHUB_TOKEN` в переменные окружения.
|
||||
|
||||
#### 2. Ошибка создания тега
|
||||
|
||||
```
|
||||
Error: git tag v1.0.0 already exists
|
||||
```
|
||||
|
||||
**Решение**: Удалите существующий тег или используйте другую версию.
|
||||
|
||||
#### 3. Ошибка сборки
|
||||
|
||||
```
|
||||
Error: failed to build for linux/amd64
|
||||
```
|
||||
|
||||
**Решение**: Проверьте, что код компилируется локально:
|
||||
|
||||
```bash
|
||||
go build -o lcg .
|
||||
```
|
||||
|
||||
### Отладка
|
||||
|
||||
```bash
|
||||
# Подробный вывод
|
||||
goreleaser release --debug
|
||||
|
||||
# Проверка конфигурации
|
||||
goreleaser check
|
||||
|
||||
# Только сборка без релиза
|
||||
goreleaser build
|
||||
|
||||
# Создание релиза без публикации (только локальная сборка)
|
||||
goreleaser release --clean
|
||||
|
||||
# Создание snapshot релиза без публикации
|
||||
goreleaser release --snapshot --clean
|
||||
```
|
||||
|
||||
## 📝 Лучшие практики
|
||||
|
||||
### 1. Версионирование
|
||||
|
||||
- Используйте семантическое версионирование (SemVer)
|
||||
- Обновляйте `VERSION.txt` перед созданием релиза
|
||||
- Создавайте теги в формате `v1.0.0`
|
||||
|
||||
### 2. Changelog
|
||||
|
||||
- Пишите понятные commit messages
|
||||
- Используйте conventional commits для автоматического changelog
|
||||
- Исключайте технические коммиты из changelog
|
||||
|
||||
### 3. Тестирование
|
||||
|
||||
- Всегда тестируйте snapshot релизы перед полным релизом
|
||||
- Проверяйте сборки на разных платформах
|
||||
- Тестируйте установку из релиза
|
||||
|
||||
### 4. Безопасность
|
||||
|
||||
- Никогда не коммитьте токены в репозиторий
|
||||
- Используйте GitHub Secrets для CI/CD
|
||||
- Регулярно обновляйте токены доступа
|
||||
|
||||
## 🎯 Пример полного процесса
|
||||
|
||||
```bash
|
||||
# 1. Подготовка
|
||||
git checkout main
|
||||
git pull origin main
|
||||
|
||||
# 2. Обновление версии
|
||||
echo "v2.0.2" > VERSION.txt
|
||||
git add VERSION.txt
|
||||
git commit -m "chore: bump version to v2.0.2"
|
||||
|
||||
# 3. Создание тега
|
||||
git tag v2.0.2
|
||||
git push origin v2.0.2
|
||||
|
||||
# 4. Создание релиза
|
||||
export GITHUB_TOKEN="your_token"
|
||||
goreleaser release
|
||||
|
||||
# 5. Проверка
|
||||
ls -la dist/
|
||||
```
|
||||
|
||||
## 📚 Дополнительные ресурсы
|
||||
|
||||
- [Официальная документация GoReleaser](https://goreleaser.com/)
|
||||
- [Примеры конфигураций](https://github.com/goreleaser/goreleaser/tree/main/examples)
|
||||
- [GitHub Actions для GoReleaser](https://github.com/goreleaser/goreleaser-action)
|
||||
|
||||
---
|
||||
|
||||
**Примечание**: Этот гайд создан специально для проекта `linux-command-gpt`. Для других проектов может потребоваться адаптация конфигурации.
|
||||
258
docs/REVERSE_PROXY_GUIDE.md
Normal file
258
docs/REVERSE_PROXY_GUIDE.md
Normal file
@@ -0,0 +1,258 @@
|
||||
# 🔄 Гайд по настройке LCG за Reverse Proxy
|
||||
|
||||
## 📋 Переменные окружения для Reverse Proxy
|
||||
|
||||
### 🔧 **Основные настройки:**
|
||||
|
||||
```bash
|
||||
# Включить аутентификацию
|
||||
LCG_SERVER_REQUIRE_AUTH=true
|
||||
|
||||
# Настроить домен для cookies (опционально)
|
||||
LCG_DOMAIN=.yourdomain.com
|
||||
|
||||
# Настроить путь для cookies (для префикса пути)
|
||||
LCG_COOKIE_PATH=/lcg
|
||||
|
||||
# Управление Secure флагом cookies
|
||||
LCG_COOKIE_SECURE=false
|
||||
|
||||
# Разрешить HTTP (для работы за reverse proxy)
|
||||
LCG_SERVER_ALLOW_HTTP=true
|
||||
|
||||
# Настроить хост и порт
|
||||
LCG_SERVER_HOST=0.0.0.0
|
||||
LCG_SERVER_PORT=8080
|
||||
|
||||
# Пароль для входа (по умолчанию: admin#123456)
|
||||
LCG_SERVER_PASSWORD=your_secure_password
|
||||
```
|
||||
|
||||
## 🚀 **Запуск за Reverse Proxy**
|
||||
|
||||
### **1. Nginx конфигурация:**
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name yourdomain.com;
|
||||
|
||||
# SSL настройки
|
||||
ssl_certificate /path/to/cert.pem;
|
||||
ssl_certificate_key /path/to/key.pem;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:8080;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# Важно для работы cookies
|
||||
proxy_cookie_domain localhost yourdomain.com;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### **2. Apache конфигурация:**
|
||||
|
||||
```apache
|
||||
<VirtualHost *:443>
|
||||
ServerName yourdomain.com
|
||||
SSLEngine on
|
||||
SSLCertificateFile /path/to/cert.pem
|
||||
SSLCertificateKeyFile /path/to/key.pem
|
||||
|
||||
ProxyPreserveHost On
|
||||
ProxyPass / http://localhost:8080/
|
||||
ProxyPassReverse / http://localhost:8080/
|
||||
|
||||
# Настройки для cookies
|
||||
ProxyPassReverseCookieDomain localhost yourdomain.com
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
### **3. Caddy конфигурация:**
|
||||
|
||||
```caddy
|
||||
yourdomain.com {
|
||||
reverse_proxy localhost:8080 {
|
||||
header_up Host {host}
|
||||
header_up X-Real-IP {remote}
|
||||
header_up X-Forwarded-For {remote}
|
||||
header_up X-Forwarded-Proto {scheme}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🏃♂️ **Команды запуска**
|
||||
|
||||
### **Базовый запуск:**
|
||||
|
||||
```bash
|
||||
LCG_SERVER_REQUIRE_AUTH=true LCG_SERVER_ALLOW_HTTP=true ./lcg serve -H 0.0.0.0 -p 8080
|
||||
```
|
||||
|
||||
### **С настройкой домена:**
|
||||
|
||||
```bash
|
||||
LCG_SERVER_REQUIRE_AUTH=true \
|
||||
LCG_SERVER_ALLOW_HTTP=true \
|
||||
LCG_DOMAIN=.yourdomain.com \
|
||||
LCG_COOKIE_SECURE=false \
|
||||
./lcg serve -H 0.0.0.0 -p 8080
|
||||
```
|
||||
|
||||
### **С кастомным паролем:**
|
||||
|
||||
```bash
|
||||
LCG_SERVER_REQUIRE_AUTH=true \
|
||||
LCG_SERVER_ALLOW_HTTP=true \
|
||||
LCG_SERVER_PASSWORD=my_secure_password \
|
||||
LCG_DOMAIN=.yourdomain.com \
|
||||
./lcg serve -H 0.0.0.0 -p 8080
|
||||
```
|
||||
|
||||
## 🔒 **Безопасность**
|
||||
|
||||
### **Рекомендуемые настройки:**
|
||||
|
||||
- ✅ `LCG_SERVER_REQUIRE_AUTH=true` - всегда включайте аутентификацию
|
||||
- ✅ `LCG_COOKIE_SECURE=false` - для HTTP за reverse proxy
|
||||
- ✅ `LCG_DOMAIN=.yourdomain.com` - для правильной работы cookies
|
||||
- ✅ Сильный пароль в `LCG_SERVER_PASSWORD`
|
||||
|
||||
### **Настройки Reverse Proxy:**
|
||||
|
||||
- ✅ Передавайте заголовки `X-Forwarded-*`
|
||||
- ✅ Настройте `proxy_cookie_domain` в Nginx
|
||||
- ✅ Используйте HTTPS на уровне reverse proxy
|
||||
|
||||
## 🐳 **Docker Compose пример**
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
services:
|
||||
lcg:
|
||||
image: your-lcg-image
|
||||
environment:
|
||||
- LCG_SERVER_REQUIRE_AUTH=true
|
||||
- LCG_SERVER_ALLOW_HTTP=true
|
||||
- LCG_DOMAIN=.yourdomain.com
|
||||
- LCG_COOKIE_SECURE=false
|
||||
- LCG_SERVER_PASSWORD=secure_password
|
||||
ports:
|
||||
- "8080:8080"
|
||||
restart: unless-stopped
|
||||
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/nginx.conf
|
||||
- ./ssl:/etc/nginx/ssl
|
||||
ports:
|
||||
- "443:443"
|
||||
depends_on:
|
||||
- lcg
|
||||
```
|
||||
|
||||
## 🔍 **Диагностика проблем**
|
||||
|
||||
### **Проверка cookies:**
|
||||
|
||||
```bash
|
||||
# Проверить установку cookies
|
||||
curl -I https://yourdomain.com/login
|
||||
|
||||
# Проверить домен cookies
|
||||
curl -v https://yourdomain.com/login 2>&1 | grep -i cookie
|
||||
```
|
||||
|
||||
### **Логи приложения:**
|
||||
|
||||
```bash
|
||||
# Запуск с debug режимом
|
||||
LCG_SERVER_REQUIRE_AUTH=true \
|
||||
LCG_SERVER_ALLOW_HTTP=true \
|
||||
./lcg -d serve -H 0.0.0.0 -p 8080
|
||||
```
|
||||
|
||||
## 📝 **Примечания**
|
||||
|
||||
- **SameSite=Lax** - более мягкий режим для reverse proxy
|
||||
- **Domain cookies** - работают только с указанным доменом
|
||||
- **Secure=false** - обязательно для HTTP за reverse proxy
|
||||
- **X-Forwarded-* заголовки** - важны для правильной работы
|
||||
|
||||
## 🆘 **Частые проблемы**
|
||||
|
||||
1. **Cookies не работают** → Проверьте `LCG_DOMAIN` и настройки reverse proxy
|
||||
2. **Ошибка 403 CSRF** → Проверьте передачу cookies через reverse proxy
|
||||
3. **Не работает аутентификация** → Убедитесь что `LCG_SERVER_REQUIRE_AUTH=true`
|
||||
4. **Проблемы с HTTPS** → Настройте `LCG_COOKIE_SECURE=false` для HTTP за reverse proxy
|
||||
|
||||
## 🛣️ **Конфигурация с префиксом пути**
|
||||
|
||||
### **Пример: example.com/lcg**
|
||||
|
||||
#### **Переменные окружения для префикса:**
|
||||
|
||||
```bash
|
||||
LCG_SERVER_REQUIRE_AUTH=true \
|
||||
LCG_SERVER_ALLOW_HTTP=true \
|
||||
LCG_DOMAIN=.example.com \
|
||||
LCG_COOKIE_PATH=/lcg \
|
||||
LCG_COOKIE_SECURE=false \
|
||||
./lcg serve -H 0.0.0.0 -p 8080
|
||||
```
|
||||
|
||||
#### **Nginx с префиксом:**
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name example.com;
|
||||
|
||||
location /lcg/ {
|
||||
proxy_pass http://localhost:8080/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# Важно для работы cookies с префиксом
|
||||
proxy_cookie_domain localhost example.com;
|
||||
proxy_cookie_path / /lcg/;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### **Apache с префиксом:**
|
||||
|
||||
```apache
|
||||
<VirtualHost *:443>
|
||||
ServerName example.com
|
||||
SSLEngine on
|
||||
|
||||
ProxyPreserveHost On
|
||||
ProxyPass /lcg/ http://localhost:8080/
|
||||
ProxyPassReverse /lcg/ http://localhost:8080/
|
||||
|
||||
# Настройки для cookies с префиксом
|
||||
ProxyPassReverseCookieDomain localhost example.com
|
||||
ProxyPassReverseCookiePath / /lcg/
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
#### **Caddy с префиксом:**
|
||||
|
||||
```caddy
|
||||
example.com {
|
||||
reverse_proxy /lcg/* localhost:8080 {
|
||||
header_up Host {host}
|
||||
header_up X-Real-IP {remote}
|
||||
header_up X-Forwarded-For {remote}
|
||||
header_up X-Forwarded-Proto {scheme}
|
||||
}
|
||||
}
|
||||
```
|
||||
118
docs/ROADMAP.md
Normal file
118
docs/ROADMAP.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# Дорожная карта развития (функциональность и безопасность)
|
||||
|
||||
Документ описывает план развития проекта на ближайшие релизы с фокусом на улучшение функциональности и усиление безопасности.
|
||||
|
||||
## Принципы
|
||||
|
||||
- Безопасность по умолчанию: новые возможности включают безопасные дефолты, опционально ослабляются.
|
||||
- Обратная совместимость: не ломать существующие сценарии CLI и API.
|
||||
- Прозрачность: чёткие Changelog, версии по SemVer, миграции и откаты.
|
||||
- Качество: тесты, линтеры, аудит зависимостей, автоматизация релизов.
|
||||
|
||||
## Вехи и цели
|
||||
|
||||
### v2.1.0 — Формализация API и UX улучшения
|
||||
|
||||
- REST API
|
||||
- Описать `POST /execute` в OpenAPI (swagger.yaml/json) и приложить в репозитории.
|
||||
- Валидация входа по схеме: обязательные поля, ограничения длины, лимит размера тела.
|
||||
- Явные коды ошибок и структура ответа (коды/сообщения).
|
||||
- Безопасность API (первый этап)
|
||||
- Дополнить защиту: ограничение размера тела (например, 64KB), тайм-ауты на чтение/запись.
|
||||
- Rate limit (встроенный простой токен-бакет, по IP). Конфиг через env.
|
||||
- Логирование попыток доступа и ошибок API (с редактированием PII).
|
||||
- Веб-интерфейс
|
||||
- Улучшения мобильной версии (доступность, контраст, a11y-метки).
|
||||
- Переключатель темы (light/dark), сохранение предпочтений.
|
||||
- Промпты
|
||||
- Экспорт/импорт системных промптов (JSON) из UI/CLI.
|
||||
- Превью при редактировании промптов в UI.
|
||||
- Документация
|
||||
- `API_GUIDE.md`: синхронизировать с OpenAPI.
|
||||
- `USAGE_GUIDE.md`: добавить раздел «Ограничения API и лимиты».
|
||||
|
||||
### v2.2.0 — Усиление безопасности и управление доступом
|
||||
|
||||
- Аутентификация/Авторизация для веб-сервера
|
||||
- Ввести токен доступа для API: `LCG_SERVER_TOKEN` (Bearer), отключаемо.
|
||||
- Сессии UI (опционально): cookie HttpOnly + SameSite=strict, CSRF-защита форм.
|
||||
- CORS: явный список разрешённых Origin через `LCG_CORS_ORIGINS`.
|
||||
- Транспорт и заголовки безопасности
|
||||
- Рекомендации по TLS терминации (пример конфигов nginx/caddy) в `serve/README.md`.
|
||||
- Security headers: CSP, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, HSTS (при HTTPS).
|
||||
- Хранилище и история
|
||||
- Опциональное шифрование истории на диске (`LCG_HISTORY_ENCRYPTION_KEY_FILE`).
|
||||
- Права на файлы истории и результатов: 0600, директории 0700.
|
||||
- Настраиваемая ретенция истории (дни/размер). Авто-очистка.
|
||||
- Наблюдаемость
|
||||
- Аудит-лог действий UI/API с маскированием чувствительных данных.
|
||||
- Включаемые/отключаемые метрики (prometheus endpoint — опционально, по отдельному порту/токену).
|
||||
|
||||
### v2.3.0 — Расширяемость и производительность
|
||||
|
||||
- Плагины провайдеров
|
||||
- Интерфейс адаптеров (провайдеры LLM), регистрация через конфиг/билд-теги.
|
||||
- Образцы адаптеров и гайды по разработке.
|
||||
- Производительность
|
||||
- Пулы HTTP-клиентов, connection reuse, упреждающие таймауты на уровне контекста.
|
||||
- Кэширование результатов подробных объяснений (опционально, по ключу запроса).
|
||||
- Расширения API
|
||||
- Пакетная обработка запросов (batch) с квотой.
|
||||
- Пагинация и фильтрация для `/history` (если будет публичный REST).
|
||||
- Дистрибуция
|
||||
- Улучшения .goreleaser: публикация SBOM, подписи (cosign), детерминированные сборки.
|
||||
- Готовые пакеты: deb/rpm, инструкции для brew/scoop (по возможности).
|
||||
|
||||
## Backlog (кандидаты)
|
||||
|
||||
- Потоковая генерация (stream) и WebSocket-канал (при наличии поддержки у провайдеров).
|
||||
- Оффлайн-режим/кэширование моделей для локальных провайдеров.
|
||||
- Расширенный поиск по результатам/истории, теги и сохранённые фильтры.
|
||||
- Резервное копирование и восстановление каталога результатов/истории.
|
||||
- Улучшение доступности (a11y), горячие клавиши, локализация интерфейса.
|
||||
|
||||
## Техническое качество
|
||||
|
||||
- Обновление стека
|
||||
- Обновить Go (минимум 1.20+), пересобрать и протестировать совместимость.
|
||||
- Регулярные обновления зависимостей и проверка уязвимостей (`govulncheck`).
|
||||
- Линтеры и проверка качества
|
||||
- Включить `golangci-lint`, `staticcheck`, `gosec` в CI.
|
||||
- Форматирование и единый стиль, pre-commit хуки.
|
||||
- Тесты
|
||||
- Unit-тесты на `serve/*` (маршруты, валидация входных данных, заголовки).
|
||||
- Интеграционные тесты API `/execute` (позитив/негатив, лимиты, токены).
|
||||
- Фаззинг критичных функций парсинга/валидации.
|
||||
- CI/CD
|
||||
- GitHub Actions: сборка, тесты, линты, релизы. Генерация чек-сумм, подписи, SBOM.
|
||||
- Автоматическая публикация релизов и проверок артефактов.
|
||||
|
||||
## Конфигурация (новые/уточняемые переменные)
|
||||
|
||||
- `LCG_SERVER_TOKEN` — токен доступа для API (Bearer). Отключаемый режим.
|
||||
- `LCG_RATE_LIMIT` — глобальные лимиты (например, `60/m`, `5/s`).
|
||||
- `LCG_CORS_ORIGINS` — список разрешённых Origin.
|
||||
- `LCG_HISTORY_ENCRYPTION_KEY_FILE` — путь к ключу для шифрования истории (опц.).
|
||||
- `LCG_MAX_BODY_BYTES` — максимальный размер тела запроса, байты (по умолчанию 65536).
|
||||
- `LCG_BROWSER_PATH` — путь к браузеру для `--browser`.
|
||||
|
||||
## Политика релизов
|
||||
|
||||
- SemVer: MINOR — функционал без ломаний, PATCH — багфиксы/мелкие улучшения.
|
||||
- Каждый релиз: обновлённый `CHANGELOG.txt`, теги `vX.Y.Z`, двуязычная документация (RU/EN при возможности).
|
||||
- Security Advisories: отдельный раздел/ISSUE шаблон для отчётов об уязвимостях.
|
||||
|
||||
## Критерии приемки (примеры)
|
||||
|
||||
- v2.1.0: OpenAPI спецификация доступна, API валидируется по схеме, лимит размера тела и таймауты соблюдаются, добавлены тесты в CI.
|
||||
- v2.2.0: Доступ к API с токеном включаем/отключаем через env; активированы security-заголовки; есть базовые правила CORS; аудит-лог включается флагом.
|
||||
- v2.3.0: Пулы клиентов, бенчмарки показывают улучшение p95 латентности, есть механизм подключения новых провайдеров.
|
||||
|
||||
## Риски и смягчение
|
||||
|
||||
- Ломание совместимости при усилении безопасности → режим совместимости через env/флаги.
|
||||
- Рост сложности конфигурации → шаблоны конфигов и «рецепты» в README/serve/README.md.
|
||||
- Производительные регрессии из-за валидации/лимитов → профилирование и кэширование на горячих путях.
|
||||
|
||||
---
|
||||
Последнее обновление: 2025-10-22
|
||||
199
docs/SECURITY_FEATURES.md
Normal file
199
docs/SECURITY_FEATURES.md
Normal file
@@ -0,0 +1,199 @@
|
||||
# 🔒 Функции безопасности LCG
|
||||
|
||||
## 🛡️ Автоматическое принуждение к HTTPS
|
||||
|
||||
### Логика безопасности
|
||||
|
||||
Приложение автоматически определяет, нужно ли использовать HTTPS:
|
||||
|
||||
1. **Небезопасные хосты** (не localhost/127.0.0.1) → **принудительно HTTPS**
|
||||
2. **Безопасные хосты** (localhost/127.0.0.1) → HTTP (если не указано иное)
|
||||
3. **Переменная `LCG_SERVER_ALLOW_HTTP=true`** → разрешает HTTP для любых хостов
|
||||
|
||||
### Примеры
|
||||
|
||||
```bash
|
||||
# Небезопасно - принудительно HTTPS
|
||||
LCG_SERVER_HOST=192.168.1.100 lcg serve
|
||||
# Результат: https://192.168.1.100:8080
|
||||
|
||||
# Безопасно - HTTP по умолчанию
|
||||
LCG_SERVER_HOST=localhost lcg serve
|
||||
# Результат: http://localhost:8080
|
||||
|
||||
# Принудительно HTTP для любого хоста
|
||||
LCG_SERVER_HOST=192.168.1.100 LCG_SERVER_ALLOW_HTTP=true lcg serve
|
||||
# Результат: http://192.168.1.100:8080
|
||||
```
|
||||
|
||||
## 🔐 SSL/TLS сертификаты
|
||||
|
||||
### Автоматическая генерация
|
||||
|
||||
Приложение автоматически генерирует самоподписанный сертификат если:
|
||||
|
||||
1. Не указаны переменные `LCG_SERVER_SSL_CERT_FILE` и `LCG_SERVER_SSL_KEY_FILE`
|
||||
2. Не найдены файлы в `~/.config/lcg/server/ssl/cert.pem` и `~/.config/lcg/server/ssl/key.pem`
|
||||
|
||||
### Расположение сертификатов
|
||||
|
||||
``` text
|
||||
~/.config/lcg/
|
||||
├── config/
|
||||
│ └── server/
|
||||
│ └── ssl/
|
||||
│ ├── cert.pem # Сертификат
|
||||
│ └── key.pem # Приватный ключ
|
||||
```
|
||||
|
||||
### Переменные окружения
|
||||
|
||||
| Переменная | Описание | По умолчанию |
|
||||
|------------|----------|--------------|
|
||||
| `LCG_CONFIG_FOLDER` | Папка конфигурации | `~/.config/lcg/config` |
|
||||
| `LCG_SERVER_ALLOW_HTTP` | Разрешить HTTP для любых хостов | `false` |
|
||||
| `LCG_SERVER_SSL_CERT_FILE` | Путь к сертификату | `""` (авто) |
|
||||
| `LCG_SERVER_SSL_KEY_FILE` | Путь к ключу | `""` (авто) |
|
||||
|
||||
## 🚀 Примеры использования
|
||||
|
||||
### Безопасный режим (по умолчанию)
|
||||
|
||||
```bash
|
||||
# Локальный сервер - HTTP
|
||||
lcg serve
|
||||
|
||||
# Внешний сервер - принудительно HTTPS
|
||||
LCG_SERVER_HOST=192.168.1.100 lcg serve
|
||||
```
|
||||
|
||||
### Настройка SSL сертификатов
|
||||
|
||||
```bash
|
||||
# Использовать собственные сертификаты
|
||||
LCG_SERVER_SSL_CERT_FILE=/path/to/cert.pem \
|
||||
LCG_SERVER_SSL_KEY_FILE=/path/to/key.pem \
|
||||
lcg serve
|
||||
|
||||
# Разрешить HTTP для внешних хостов
|
||||
LCG_SERVER_HOST=192.168.1.100 \
|
||||
LCG_SERVER_ALLOW_HTTP=true \
|
||||
lcg serve
|
||||
```
|
||||
|
||||
### Docker контейнер
|
||||
|
||||
```dockerfile
|
||||
FROM golang:1.21-alpine AS builder
|
||||
# ... build steps ...
|
||||
|
||||
FROM alpine:latest
|
||||
COPY --from=builder /app/lcg /usr/local/bin/
|
||||
ENV LCG_SERVER_HOST=0.0.0.0
|
||||
ENV LCG_SERVER_ALLOW_HTTP=false
|
||||
CMD ["lcg", "serve"]
|
||||
```
|
||||
|
||||
### Systemd сервис
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=LCG Server
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=lcg
|
||||
WorkingDirectory=/opt/lcg
|
||||
ExecStart=/opt/lcg/lcg serve
|
||||
Environment=LCG_SERVER_HOST=0.0.0.0
|
||||
Environment=LCG_SERVER_ALLOW_HTTP=false
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
## 🔧 Технические детали
|
||||
|
||||
### Генерация сертификата
|
||||
|
||||
Самоподписанный сертификат генерируется с:
|
||||
|
||||
- **Размер ключа**: 2048 бит RSA
|
||||
- **Срок действия**: 1 год
|
||||
- **Поддерживаемые хосты**: localhost, 127.0.0.1, указанный хост
|
||||
- **Использование**: Server Authentication
|
||||
|
||||
### Безопасные хосты
|
||||
|
||||
Следующие хосты считаются безопасными для HTTP:
|
||||
|
||||
- `localhost`
|
||||
- `127.0.0.1`
|
||||
- `::1` (IPv6 localhost)
|
||||
|
||||
### Проверка безопасности
|
||||
|
||||
```go
|
||||
// Проверка хоста
|
||||
if !ssl.IsSecureHost(host) {
|
||||
// Принудительно HTTPS
|
||||
useHTTPS = true
|
||||
}
|
||||
|
||||
// Проверка разрешения HTTP
|
||||
if config.AppConfig.Server.AllowHTTP {
|
||||
useHTTPS = false
|
||||
}
|
||||
```
|
||||
|
||||
## 🛠️ Отладка
|
||||
|
||||
### Проверка конфигурации
|
||||
|
||||
```bash
|
||||
# Показать текущую конфигурацию
|
||||
lcg config --full | jq '.server'
|
||||
|
||||
# Проверить SSL сертификаты
|
||||
ls -la ~/.config/lcg/config/server/ssl/
|
||||
|
||||
# Проверить переменные окружения
|
||||
env | grep LCG_SERVER
|
||||
```
|
||||
|
||||
### Логи безопасности
|
||||
|
||||
```bash
|
||||
# Запуск с отладкой
|
||||
LCG_SERVER_HOST=192.168.1.100 lcg serve --debug
|
||||
|
||||
# Проверка SSL
|
||||
openssl x509 -in ~/.config/lcg/config/server/ssl/cert.pem -text -noout
|
||||
```
|
||||
|
||||
## ⚠️ Важные замечания
|
||||
|
||||
### Безопасность
|
||||
|
||||
1. **Самоподписанные сертификаты** - браузеры будут показывать предупреждение
|
||||
2. **Продакшен** - используйте настоящие SSL сертификаты от CA
|
||||
3. **Сетевой доступ** - HTTPS защищает трафик, но не аутентификацию
|
||||
|
||||
### Производительность
|
||||
|
||||
1. **HTTPS** - небольшая нагрузка на CPU для шифрования
|
||||
2. **Сертификаты** - генерируются один раз, затем кэшируются
|
||||
3. **Память** - сертификаты загружаются в память при запуске
|
||||
|
||||
## 📚 Связанные файлы
|
||||
|
||||
- `config/config.go` - конфигурация безопасности
|
||||
- `ssl/ssl.go` - генерация и управление сертификатами
|
||||
- `serve/serve.go` - HTTP/HTTPS сервер
|
||||
- `SECURITY_FEATURES.md` - эта документация
|
||||
|
||||
---
|
||||
|
||||
**Результат**: Приложение теперь автоматически обеспечивает безопасность соединения в зависимости от конфигурации хоста!
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Что это
|
||||
|
||||
Linux Command GPT (`lcg`) преобразует описание на естественном языке в готовую Linux‑команду. Инструмент поддерживает сменные провайдеры LLM (Ollama или Proxy), управление системными промптами, историю запросов, сохранение результатов, HTTP сервер для просмотра результатов и интерактивные действия над сгенерированной командой.
|
||||
Linux Command GPT (`lcg`) преобразует описание на естественном языке в готовую команду для Linux или Windows. Инструмент автоматически определяет операционную систему и использует соответствующие промпты. Поддерживает сменные провайдеры LLM (Ollama или Proxy), управление системными промптами, историю запросов, сохранение результатов, HTTP сервер для просмотра результатов, аутентификацию, CSRF защиту, интерактивные действия над сгенерированной командой и деплой в Kubernetes.
|
||||
|
||||
## Требования
|
||||
|
||||
@@ -60,11 +60,18 @@ lcg --file /path/to/context.txt "хочу вывести список дирек
|
||||
Действия: (c)копировать, (s)сохранить, (r)перегенерировать, (e)выполнить, (v|vv|vvv)подробно, (n)ничего:
|
||||
```
|
||||
|
||||
### Что нового в 2.0.1
|
||||
### Что нового в 3.0.0
|
||||
|
||||
- Улучшена мобильная версия веб‑интерфейса: корректные размеры кнопок, шрифтов и отступов; адаптивная верстка
|
||||
- Учитывается `prefers-reduced-motion` для снижения анимаций, если это задано в системе
|
||||
- Добавлен REST эндпоинт `POST /execute` (только через curl) — см. подробности и примеры в `API_GUIDE.md`
|
||||
- **Аутентификация**: Добавлена система аутентификации с JWT токенами и HTTP-only cookies
|
||||
- **CSRF защита**: Полная защита от CSRF атак с токенами и middleware
|
||||
- **Безопасность**: Улучшенная безопасность с проверкой токенов и сессий
|
||||
- **Kubernetes деплой**: Полный набор манифестов для деплоя в Kubernetes с Traefik
|
||||
- **Flux CD**: GitOps конфигурация для автоматического деплоя
|
||||
- **Reverse Proxy**: Поддержка работы за reverse proxy с настройкой cookies
|
||||
- **Веб-интерфейс**: Улучшенный веб-интерфейс с современным дизайном
|
||||
- **Мониторинг**: Prometheus метрики и ServiceMonitor
|
||||
- **Масштабирование**: HPA для автоматического масштабирования
|
||||
- **Тестирование**: Инструменты для тестирования CSRF защиты
|
||||
|
||||
## Переменные окружения
|
||||
|
||||
@@ -90,6 +97,13 @@ lcg --file /path/to/context.txt "хочу вывести список дирек
|
||||
| `LCG_ALLOW_EXECUTION` | пусто | Если `1`/`true` — включает возможность выполнения команд через опцию `(e)` в меню действий. |
|
||||
| `LCG_SERVER_PORT` | `8080` | Порт для HTTP сервера просмотра результатов. |
|
||||
| `LCG_SERVER_HOST` | `localhost` | Хост для HTTP сервера просмотра результатов. |
|
||||
| `LCG_SERVER_REQUIRE_AUTH` | `false` | Требовать аутентификацию для доступа к веб-интерфейсу. |
|
||||
| `LCG_SERVER_PASSWORD` | `admin#123456` | Пароль для аутентификации. |
|
||||
| `LCG_COOKIE_SECURE` | `false` | Использовать Secure флаг для cookies (для HTTPS). |
|
||||
| `LCG_DOMAIN` | пусто | Домен для cookies (для reverse proxy). |
|
||||
| `LCG_COOKIE_PATH` | `/` | Путь для cookies (для reverse proxy). |
|
||||
| `LCG_COOKIE_TTL_HOURS` | `168` | Время жизни cookies в часах (по умолчанию 7 дней). |
|
||||
| `LCG_CSRF_SECRET` | пусто | Секрет для CSRF токенов (генерируется автоматически). |
|
||||
|
||||
Примеры настройки:
|
||||
|
||||
@@ -104,6 +118,14 @@ export LCG_PROVIDER=proxy
|
||||
export LCG_HOST=http://localhost:8080
|
||||
export LCG_MODEL=GigaChat-2
|
||||
export LCG_JWT_TOKEN=your_jwt_token_here
|
||||
|
||||
# Аутентификация и безопасность
|
||||
export LCG_SERVER_REQUIRE_AUTH=true
|
||||
export LCG_SERVER_PASSWORD=my_secure_password
|
||||
export LCG_COOKIE_SECURE=false
|
||||
export LCG_DOMAIN=.example.com
|
||||
export LCG_COOKIE_PATH=/lcg
|
||||
export LCG_COOKIE_TTL_HOURS=72 # 3 дня
|
||||
```
|
||||
|
||||
## Базовый синтаксис
|
||||
@@ -146,6 +168,8 @@ lcg [глобальные опции] <описание команды>
|
||||
- `--port, -p` — порт сервера (по умолчанию из `LCG_SERVER_PORT`)
|
||||
- `--host, -H` — хост сервера (по умолчанию из `LCG_SERVER_HOST`)
|
||||
- `--browser, -b` — открыть браузер автоматически после старта
|
||||
- `--require-auth` — включить аутентификацию (переопределяет `LCG_SERVER_REQUIRE_AUTH`)
|
||||
- `--password` — пароль для аутентификации (переопределяет `LCG_SERVER_PASSWORD`)
|
||||
|
||||
### Подробные объяснения (v/vv/vvv)
|
||||
|
||||
@@ -196,6 +220,39 @@ lcg [глобальные опции] <описание команды>
|
||||
- Не вставляйте секреты в запросы. Перед выполнением (`e`) проверяйте команду вручную.
|
||||
- Для структуры API см. `API_CONTRACT.md` (эндпоинты и форматы запросов/ответов).
|
||||
|
||||
## Поддержка операционных систем
|
||||
|
||||
### Автоматическое определение ОС
|
||||
|
||||
Приложение автоматически определяет операционную систему и использует соответствующие промпты:
|
||||
|
||||
- **Linux/Unix системы** (включая macOS): используются промпты для Linux команд
|
||||
- **Windows**: используются промпты для Windows команд (PowerShell, CMD, Batch)
|
||||
|
||||
### Промпты для Windows
|
||||
|
||||
На Windows системах доступны следующие встроенные промпты:
|
||||
|
||||
| ID | Name | Описание |
|
||||
| --- | --- | --- |
|
||||
| 1 | windows-command | Основной промпт для генерации Windows команд |
|
||||
| 2 | windows-command-with-explanation | Промпт с подробным объяснением команд |
|
||||
| 3 | windows-command-safe | Безопасный анализ команд с предупреждениями |
|
||||
| 4 | windows-command-verbose | Подробный анализ с техническими деталями |
|
||||
| 5 | windows-command-simple | Простое и понятное объяснение |
|
||||
|
||||
### Примеры использования на Windows
|
||||
|
||||
```cmd
|
||||
# PowerShell команды
|
||||
lcg "хочу получить список всех процессов"
|
||||
lcg "показать информацию о дисках"
|
||||
|
||||
# CMD команды
|
||||
lcg "создать папку test и перейти в неё"
|
||||
lcg "найти все файлы .txt в текущей директории"
|
||||
```
|
||||
|
||||
## Системные промпты
|
||||
|
||||
### Управление промптами
|
||||
@@ -210,6 +267,9 @@ lcg [глобальные опции] <описание команды>
|
||||
|
||||
### Встроенные промпты (ID 1–5)
|
||||
|
||||
Промпты автоматически выбираются в зависимости от операционной системы:
|
||||
|
||||
**Linux/Unix системы:**
|
||||
| ID | Name | Описание |
|
||||
| --- | --- | --- |
|
||||
| 1 | linux-command | «Ответь только Linux‑командой, без форматирования и объяснений». |
|
||||
@@ -218,6 +278,15 @@ lcg [глобальные опции] <описание команды>
|
||||
| 4 | linux-command-verbose | Команда с подробными объяснениями флагов и альтернатив. |
|
||||
| 5 | linux-command-simple | Простые команды, избегать сложных опций. |
|
||||
|
||||
**Windows системы:**
|
||||
| ID | Name | Описание |
|
||||
| --- | --- | --- |
|
||||
| 1 | windows-command | «Ответь только Windows‑командой, без форматирования и объяснений». |
|
||||
| 2 | windows-command-with-explanation | Сгенерируй команду и кратко объясни, что она делает (формат: COMMAND: explanation). |
|
||||
| 3 | windows-command-safe | Безопасные команды (без потери данных). Вывод — только команда. |
|
||||
| 4 | windows-command-verbose | Команда с подробными объяснениями флагов и альтернатив. |
|
||||
| 5 | windows-command-simple | Простые команды, избегать сложных опций. |
|
||||
|
||||
### Промпты подробности (ID 6–8)
|
||||
|
||||
| ID | Name | Описание |
|
||||
@@ -275,6 +344,12 @@ lcg serve
|
||||
- **Современный дизайн** — адаптивный интерфейс с карточками файлов
|
||||
- **Сортировка** — файлы отсортированы по дате изменения (новые сверху)
|
||||
- **Превью содержимого** — первые 200 символов каждого файла
|
||||
- **Аутентификация** — защищенный доступ с JWT токенами
|
||||
- **CSRF защита** — защита от межсайтовых атак
|
||||
- **История запросов** (`/history`) — просмотр истории всех запросов
|
||||
- **Управление промптами** (`/prompts`) — редактирование системных промптов
|
||||
- **Выполнение команд** (`/run`) — интерактивное выполнение команд
|
||||
- **Безопасность** — HTTP-only cookies, проверка токенов
|
||||
|
||||
Структура файла (команда):
|
||||
|
||||
@@ -358,6 +433,14 @@ lcg serve --port 9090
|
||||
|
||||
# Запуск на всех интерфейсах
|
||||
lcg serve --host 0.0.0.0 --port 8080
|
||||
|
||||
# Запуск с аутентификацией
|
||||
lcg serve --require-auth --password my_secure_password
|
||||
|
||||
# Запуск с переменными окружения
|
||||
export LCG_SERVER_REQUIRE_AUTH=true
|
||||
export LCG_SERVER_PASSWORD=admin#123456
|
||||
lcg serve
|
||||
```
|
||||
|
||||
## История
|
||||
@@ -377,6 +460,10 @@ lcg history list
|
||||
- Для `ollama`/`proxy` API‑ключ не нужен; команды `update-key`/`delete-key` просто уведомят об этом.
|
||||
- HTTP сервер не запускается: проверьте, что порт свободен, используйте `--port` для смены порта.
|
||||
- Веб-интерфейс не отображает файлы: убедитесь, что в `LCG_RESULT_FOLDER` есть `.md` файлы.
|
||||
- **Аутентификация не работает**: проверьте `LCG_SERVER_REQUIRE_AUTH=true` и правильность пароля.
|
||||
- **CSRF ошибки**: убедитесь, что токены передаются в заголовках `X-CSRF-Token`.
|
||||
- **Cookies не сохраняются**: проверьте настройки `LCG_DOMAIN` и `LCG_COOKIE_PATH` для reverse proxy.
|
||||
- **Kubernetes деплой не работает**: проверьте права доступа к кластеру и наличие всех манифестов.
|
||||
|
||||
## JSON‑история запросов
|
||||
|
||||
@@ -408,17 +495,148 @@ lcg history list
|
||||
|
||||
## Доступ к локальному API
|
||||
|
||||
Эндпоинт: `POST /execute` (только через curl).
|
||||
### Основные эндпоинты
|
||||
|
||||
- `POST /api/execute` — выполнение запросов к LLM
|
||||
- `POST /api/save-result` — сохранение результатов
|
||||
- `POST /api/add-to-history` — добавление в историю
|
||||
- `GET /api/login` — страница аутентификации
|
||||
- `POST /api/login` — аутентификация
|
||||
- `POST /api/logout` — выход из системы
|
||||
- `GET /metrics` — Prometheus метрики
|
||||
|
||||
### Примеры использования
|
||||
|
||||
```bash
|
||||
# Запустить сервер
|
||||
lcg serve
|
||||
|
||||
# Выполнить запрос
|
||||
curl -X POST http://localhost:8080/execute \
|
||||
# Выполнить запрос (без аутентификации)
|
||||
curl -X POST http://localhost:8080/api/execute \
|
||||
-H "Content-Type: application/json" \
|
||||
-A curl \
|
||||
-d '{"prompt": "create directory test", "verbose": "vv"}'
|
||||
|
||||
# Аутентификация
|
||||
curl -X POST http://localhost:8080/api/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username": "admin", "password": "admin#123456"}'
|
||||
|
||||
# Выполнение с CSRF токеном
|
||||
curl -X POST http://localhost:8080/api/execute \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-CSRF-Token: your_csrf_token" \
|
||||
-H "Cookie: auth_token=your_jwt_token" \
|
||||
-d '{"prompt": "create directory test"}'
|
||||
```
|
||||
|
||||
Подробности и примеры: `API_GUIDE.md`.
|
||||
|
||||
## Kubernetes деплой
|
||||
|
||||
### Быстрый деплой
|
||||
|
||||
```bash
|
||||
# Переход в папку деплоя
|
||||
cd deploy
|
||||
|
||||
# Полный деплой (сборка + деплой + проверка)
|
||||
./full-deploy.sh
|
||||
|
||||
# Или поэтапно
|
||||
./build.sh lcg latest
|
||||
./deploy.sh
|
||||
./health-check.sh
|
||||
```
|
||||
|
||||
### Использование Make
|
||||
|
||||
```bash
|
||||
# Справка
|
||||
make help
|
||||
|
||||
# Сборка и деплой
|
||||
make build
|
||||
make deploy
|
||||
|
||||
# Мониторинг
|
||||
make status
|
||||
make logs
|
||||
make monitor
|
||||
|
||||
# Удаление
|
||||
make undeploy
|
||||
```
|
||||
|
||||
### Flux CD (GitOps)
|
||||
|
||||
```bash
|
||||
# Настройка Flux CD
|
||||
cd deploy/flux
|
||||
./setup-flux.sh
|
||||
|
||||
# Создание Kustomization
|
||||
./create_kustomization.sh
|
||||
|
||||
# Мониторинг
|
||||
kubectl get kustomization lcg -n flux-system
|
||||
```
|
||||
|
||||
### Конфигурация для reverse proxy
|
||||
|
||||
```bash
|
||||
# Настройка для работы за reverse proxy
|
||||
export LCG_SERVER_REQUIRE_AUTH=true
|
||||
export LCG_SERVER_ALLOW_HTTP=true
|
||||
export LCG_DOMAIN=.example.com
|
||||
export LCG_COOKIE_PATH=/lcg
|
||||
export LCG_COOKIE_SECURE=false
|
||||
|
||||
# Запуск
|
||||
./lcg serve -H 0.0.0.0 -p 8080
|
||||
```
|
||||
|
||||
### Мониторинг и безопасность
|
||||
|
||||
- **Prometheus метрики**: `/metrics` endpoint
|
||||
- **Health checks**: автоматические проверки готовности
|
||||
- **HPA**: автоматическое масштабирование (2-10 replicas)
|
||||
- **CSRF защита**: токены для всех POST запросов
|
||||
- **Аутентификация**: JWT токены в HTTP-only cookies
|
||||
- **Security context**: non-root пользователь, минимальные права
|
||||
|
||||
Подробности: `deploy/README.md` и `deploy/flux/README.md`.
|
||||
|
||||
## Тестирование CSRF защиты
|
||||
|
||||
### Автоматическое тестирование
|
||||
|
||||
```bash
|
||||
# Запуск тестов CSRF защиты
|
||||
./test_csrf.sh
|
||||
|
||||
# Проверка результатов
|
||||
echo "Проверьте вывод на наличие ошибок 403 Forbidden"
|
||||
```
|
||||
|
||||
### Ручное тестирование
|
||||
|
||||
```bash
|
||||
# Откройте csrf_test.html в браузере
|
||||
open csrf_test.html
|
||||
|
||||
# Или используйте curl для тестирования
|
||||
curl -X POST http://localhost:8080/api/execute \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"prompt": "test"}' \
|
||||
-v
|
||||
```
|
||||
|
||||
### Демонстрация уязвимости
|
||||
|
||||
```bash
|
||||
# Откройте csrf_demo.html для демонстрации атаки
|
||||
open csrf_demo.html
|
||||
```
|
||||
|
||||
Подробности: `CSRF_TESTING_GUIDE.md`.
|
||||
205
docs/VALIDATION_CONFIG.md
Normal file
205
docs/VALIDATION_CONFIG.md
Normal file
@@ -0,0 +1,205 @@
|
||||
# 🔧 Конфигурация валидации длины полей
|
||||
|
||||
## 📋 Переменные окружения
|
||||
|
||||
Все настройки валидации можно настроить через переменные окружения:
|
||||
|
||||
### Основные лимиты
|
||||
|
||||
| Переменная | Описание | По умолчанию |
|
||||
|------------|----------|--------------|
|
||||
| `LCG_MAX_SYSTEM_PROMPT_LENGTH` | Максимальная длина системного промпта | 1000 |
|
||||
| `LCG_MAX_USER_MESSAGE_LENGTH` | Максимальная длина пользовательского сообщения | 2000 |
|
||||
| `LCG_MAX_PROMPT_NAME_LENGTH` | Максимальная длина названия промпта | 200 |
|
||||
| `LCG_MAX_PROMPT_DESC_LENGTH` | Максимальная длина описания промпта | 500 |
|
||||
| `LCG_MAX_COMMAND_LENGTH` | Максимальная длина команды/ответа | 2000 |
|
||||
| `LCG_MAX_EXPLANATION_LENGTH` | Максимальная длина объяснения | 2000 |
|
||||
|
||||
## 🚀 Примеры использования
|
||||
|
||||
### Установка через переменные окружения
|
||||
|
||||
```bash
|
||||
# Увеличить лимит системного промпта до 2к символов
|
||||
export LCG_MAX_SYSTEM_PROMPT_LENGTH=2000
|
||||
|
||||
# Уменьшить лимит пользовательского сообщения до 1к символов
|
||||
export LCG_MAX_USER_MESSAGE_LENGTH=1000
|
||||
|
||||
# Увеличить лимит названия промпта до 500 символов
|
||||
export LCG_MAX_PROMPT_NAME_LENGTH=500
|
||||
```
|
||||
|
||||
### Установка в .env файле
|
||||
|
||||
```bash
|
||||
# .env файл
|
||||
LCG_MAX_SYSTEM_PROMPT_LENGTH=2000
|
||||
LCG_MAX_USER_MESSAGE_LENGTH=1500
|
||||
LCG_MAX_PROMPT_NAME_LENGTH=300
|
||||
LCG_MAX_PROMPT_DESC_LENGTH=1000
|
||||
LCG_MAX_COMMAND_LENGTH=3000
|
||||
LCG_MAX_EXPLANATION_LENGTH=5000
|
||||
```
|
||||
|
||||
### Установка в systemd сервисе
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Linux Command GPT
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=lcg
|
||||
WorkingDirectory=/opt/lcg
|
||||
ExecStart=/opt/lcg/lcg serve
|
||||
Environment=LCG_MAX_SYSTEM_PROMPT_LENGTH=2000
|
||||
Environment=LCG_MAX_USER_MESSAGE_LENGTH=1500
|
||||
Environment=LCG_MAX_PROMPT_NAME_LENGTH=300
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
### Установка в Docker
|
||||
|
||||
```dockerfile
|
||||
FROM golang:1.21-alpine AS builder
|
||||
# ... build steps ...
|
||||
|
||||
FROM alpine:latest
|
||||
COPY --from=builder /app/lcg /usr/local/bin/
|
||||
ENV LCG_MAX_SYSTEM_PROMPT_LENGTH=2000
|
||||
ENV LCG_MAX_USER_MESSAGE_LENGTH=1500
|
||||
CMD ["lcg", "serve"]
|
||||
```
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
version: '3.8'
|
||||
services:
|
||||
lcg:
|
||||
image: lcg:latest
|
||||
environment:
|
||||
- LCG_MAX_SYSTEM_PROMPT_LENGTH=2000
|
||||
- LCG_MAX_USER_MESSAGE_LENGTH=1500
|
||||
- LCG_MAX_PROMPT_NAME_LENGTH=300
|
||||
ports:
|
||||
- "8080:8080"
|
||||
```
|
||||
|
||||
## 🔍 Где применяется валидация
|
||||
|
||||
### 1. Консольная часть (main.go)
|
||||
- ✅ Валидация пользовательского сообщения
|
||||
- ✅ Валидация системного промпта
|
||||
- ✅ Цветные сообщения об ошибках
|
||||
|
||||
### 2. API эндпоинты
|
||||
- ✅ `/execute` - валидация промпта и системного промпта
|
||||
- ✅ `/api/save-result` - валидация всех полей
|
||||
- ✅ `/api/add-to-history` - валидация всех полей
|
||||
|
||||
### 3. Веб-интерфейс
|
||||
- ✅ Страница выполнения - валидация в JavaScript и на сервере
|
||||
- ✅ Управление промптами - валидация всех полей формы
|
||||
|
||||
### 4. JavaScript валидация
|
||||
- ✅ Клиентская валидация перед отправкой
|
||||
- ✅ Динамические лимиты из конфигурации
|
||||
- ✅ Понятные сообщения об ошибках
|
||||
|
||||
## 🛠️ Технические детали
|
||||
|
||||
### Структура конфигурации
|
||||
|
||||
```go
|
||||
type ValidationConfig struct {
|
||||
MaxSystemPromptLength int // LCG_MAX_SYSTEM_PROMPT_LENGTH
|
||||
MaxUserMessageLength int // LCG_MAX_USER_MESSAGE_LENGTH
|
||||
MaxPromptNameLength int // LCG_MAX_PROMPT_NAME_LENGTH
|
||||
MaxPromptDescLength int // LCG_MAX_PROMPT_DESC_LENGTH
|
||||
MaxCommandLength int // LCG_MAX_COMMAND_LENGTH
|
||||
MaxExplanationLength int // LCG_MAX_EXPLANATION_LENGTH
|
||||
}
|
||||
```
|
||||
|
||||
### Функции валидации
|
||||
|
||||
```go
|
||||
// Основные функции
|
||||
validation.ValidateSystemPrompt(prompt)
|
||||
validation.ValidateUserMessage(message)
|
||||
validation.ValidatePromptName(name)
|
||||
validation.ValidatePromptDescription(description)
|
||||
validation.ValidateCommand(command)
|
||||
validation.ValidateExplanation(explanation)
|
||||
|
||||
// Вспомогательные функции
|
||||
validation.TruncateSystemPrompt(prompt)
|
||||
validation.TruncateUserMessage(message)
|
||||
validation.FormatLengthInfo(systemPrompt, userMessage)
|
||||
```
|
||||
|
||||
### Обработка ошибок
|
||||
|
||||
- **API**: HTTP 400 с JSON сообщением об ошибке
|
||||
- **Веб-интерфейс**: HTTP 400 с текстовым сообщением
|
||||
- **Консоль**: Цветные сообщения об ошибках
|
||||
- **JavaScript**: Alert с предупреждением
|
||||
|
||||
## 📝 Примеры сообщений об ошибках
|
||||
|
||||
```
|
||||
❌ Ошибка: system_prompt: системный промпт слишком длинный: 1500 символов (максимум 1000)
|
||||
❌ Ошибка: user_message: пользовательское сообщение слишком длинное: 2500 символов (максимум 2000)
|
||||
❌ Ошибка: prompt_name: название промпта слишком длинное: 300 символов (максимум 200)
|
||||
```
|
||||
|
||||
## 🔄 Миграция с жестко заданных значений
|
||||
|
||||
Если ранее использовались жестко заданные значения в коде, теперь они автоматически заменяются на значения из конфигурации:
|
||||
|
||||
```go
|
||||
// Старый код
|
||||
if len(prompt) > 2000 {
|
||||
return errors.New("too long")
|
||||
}
|
||||
|
||||
// Новый код
|
||||
if err := validation.ValidateSystemPrompt(prompt); err != nil {
|
||||
return err
|
||||
}
|
||||
```
|
||||
|
||||
## 🎯 Рекомендации по настройке
|
||||
|
||||
### Для разработки
|
||||
```bash
|
||||
export LCG_MAX_SYSTEM_PROMPT_LENGTH=2000
|
||||
export LCG_MAX_USER_MESSAGE_LENGTH=2000
|
||||
export LCG_MAX_PROMPT_NAME_LENGTH=200
|
||||
export LCG_MAX_PROMPT_DESC_LENGTH=500
|
||||
```
|
||||
|
||||
### Для продакшена
|
||||
```bash
|
||||
export LCG_MAX_SYSTEM_PROMPT_LENGTH=1000
|
||||
export LCG_MAX_USER_MESSAGE_LENGTH=1500
|
||||
export LCG_MAX_PROMPT_NAME_LENGTH=100
|
||||
export LCG_MAX_PROMPT_DESC_LENGTH=300
|
||||
```
|
||||
|
||||
### Для высоконагруженных систем
|
||||
```bash
|
||||
export LCG_MAX_SYSTEM_PROMPT_LENGTH=500
|
||||
export LCG_MAX_USER_MESSAGE_LENGTH=1000
|
||||
export LCG_MAX_PROMPT_NAME_LENGTH=50
|
||||
export LCG_MAX_PROMPT_DESC_LENGTH=200
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Примечание**: Все значения настраиваются через переменные окружения и применяются ко всем частям приложения (консоль, веб-интерфейс, API).
|
||||
63
docs/VERBOSE_PROMPT_EDITING.md
Normal file
63
docs/VERBOSE_PROMPT_EDITING.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# Редактирование промптов подробности
|
||||
|
||||
## 🎯 Реализованная функциональность
|
||||
|
||||
### ✅ **Что добавлено:**
|
||||
|
||||
1. **Функция редактирования в JavaScript:**
|
||||
- `editVerbosePrompt(mode, content)` - открывает форму редактирования для промптов подробности
|
||||
- Автоматически заполняет поля формы данными промпта
|
||||
- Показывает режим в заголовке формы
|
||||
|
||||
2. **Обработчик на сервере:**
|
||||
- `handleEditVerbosePrompt()` - новый обработчик для маршрута `/prompts/edit-verbose/`
|
||||
- Поддерживает режимы: `v`, `vv`, `vvv`
|
||||
- Валидация всех полей с использованием `validation` пакета
|
||||
- Обновление промптов через `PromptManager`
|
||||
|
||||
3. **Маршрутизация:**
|
||||
- Добавлен маршрут `/prompts/edit-verbose/` в `serve.go`
|
||||
- Поддержка HTTP методов PUT
|
||||
- Интеграция с существующей системой маршрутов
|
||||
|
||||
### 🔧 **Как работает:**
|
||||
|
||||
1. **Пользователь нажимает кнопку "✏️"** на промпте подробности
|
||||
2. **JavaScript вызывает** `editVerbosePrompt(mode, content)`
|
||||
3. **Форма открывается** с заполненными полями
|
||||
4. **При сохранении** отправляется PUT запрос на `/prompts/edit-verbose/{mode}`
|
||||
5. **Сервер обрабатывает** запрос через `handleEditVerbosePrompt()`
|
||||
6. **Промпт обновляется** в файловой системе
|
||||
7. **Страница перезагружается** с обновленными данными
|
||||
|
||||
### 📋 **Поддерживаемые режимы:**
|
||||
|
||||
- **`v`** → ID 6 (базовый verbose)
|
||||
- **`vv`** → ID 7 (средний verbose)
|
||||
- **`vvv`** → ID 8 (максимальный verbose)
|
||||
|
||||
### 🛡️ **Валидация:**
|
||||
|
||||
- **Содержимое:** максимум символов из `LCG_MAX_SYSTEM_PROMPT_LENGTH`
|
||||
- **Название:** максимум символов из `LCG_MAX_PROMPT_NAME_LENGTH`
|
||||
- **Описание:** максимум символов из `LCG_MAX_PROMPT_DESC_LENGTH`
|
||||
|
||||
### 🎨 **UI/UX:**
|
||||
|
||||
- **Единая форма** для редактирования всех типов промптов
|
||||
- **Автоматическое определение** типа промпта (системный/verbose)
|
||||
- **Правильная маршрутизация** запросов
|
||||
- **Валидация на клиенте** и сервере
|
||||
- **Отзывчивый дизайн** для мобильных устройств
|
||||
|
||||
## 🚀 **Использование:**
|
||||
|
||||
1. Откройте страницу `/prompts`
|
||||
2. Перейдите на вкладку "📝 Промпты подробности"
|
||||
3. Нажмите кнопку "✏️" на нужном промпте
|
||||
4. Отредактируйте содержимое
|
||||
5. Нажмите "Сохранить"
|
||||
|
||||
## ✅ **Статус:**
|
||||
|
||||
**ГОТОВО** - Редактирование промптов подробности полностью реализовано и протестировано.
|
||||
16
git-sealed-cfg.yaml
Normal file
16
git-sealed-cfg.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
apiVersion: bitnami.com/v1alpha1
|
||||
kind: SealedSecret
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
name: git-secrets
|
||||
namespace: flux-system
|
||||
spec:
|
||||
encryptedData:
|
||||
password: AgB8/7lk3onjQG9R2OzX02rv9Lwh3tkOkvV/DjySSGZDHnYtdOw6ttOZXSeQqibAfKIOqtbKvpGAmK0x0/1tQXfB1zg2unFh1p5oEa0sgYBNd0eeNV5nRXDpgl8gv3lWKLaLvLCmhxloYze1B6Js8UrPDGeH3lvxRk9A5oja87HEBnx/VIGm5h4crvb5fUn+7wibr+oQNP+LBRLiko4eqR8PZAA0qgWUvoJnAF6NIsGPEtNi6WjErtwwvYIsZbCIChjPHQll+FQYwSH7M9jXVEBPejiR92+XIR8RMMo6hsWWxkEvvZs5prNREirAOSErOHBRTMEYkI5JIAb6uaRBKZ2nP0G4YQAkx3N7DIZPD7MVQF3u5jIpqaRwP4S+v703/19uPID1D8RuXt99QUv4d7Bfs8PlwkV5/SOgx2ZPVn1viuieZZ9M9hdk5uXYqOqgLOjU4aQSVpVV5pdR7ifn2fcbcTsG6fE3srrjUt8c2t+bV1IBUw8CIzyA1lebxj31s2XRpY+6JsfihSneKm/RkR4ySkyLjF+BeAO7CuXeCdziSFsoCzRDGQqaf/3YKCv1HFbLUsAfHvbCOYuZH7X9UAw51OVqqhEpdD21Ms0W29NGRVUFz80/eglLpK5OmvlXr1kME2P9Wo8TEkLIK1Fjo10f6WzHsUU9eI1cfy6/JgyoaFn4Sjga9SaRgdMkv2neztyFLx6cRbdGui+5qh5Zewqaawvp72j7ZhosXVi0uBnKSoBkN6CLEzlw
|
||||
username: AgB1HiFs7wfaiAaCrwGNr6zWfd8IoNMZUvJrKeaLbKqEJ2oAr3Db406mjq1gz/O0lhvOJqnmBR/Iel4PMVfixMBtmA8j2anytEkj7ZuruFF3OHOzEkN6OeYfJa9ddkJmUs+Zq62TxQdcaJufQnVfuSdScGvE+Exc8ruFWE0xM1cDgFsRp75d0lo6UmhxaJlBRewQexyH6izOmtecAZDX6WVyBopIGx2cPgNfyxp/4sU5CI5sGCjeqcq+5INRELk9kofbDIUnQuHxrH3nbzABNnK76sU6DhqS3jBmT5Pv9azB2AWaaYZ8z1di/fdYia3wKoQAbef36wKyQ+/ZLApnuaVkj9F96tcjqlV4T3SGp4179k99M8zf/7g1GwsljYRPEcRmc00L2FrC5m3S+oK8wIk6fLVT5KYJBlEP6ja3eTrgGDttosuNa4BcpzqLC2Ov6x1b6RKLRVPN9/+1eC9gMBxBqKnvsdjZn4kQb8yUYgjyRHMZl8kHehLnRDhIoE1O7xCQAMkrdeNyE1kAXsij2jgTDR7a72J86nnE0NPAyct4mCpn2x+2aFUVeyZ8u2e0nDLjxPIAs/ceN51ybz5f2cXH0JrEk3Yy0ZXTtHOF6NbZNcxrCrZGODYYIbtTpjI0MjYd+nxJt5TI3UN0pnrdR7FxHai+76Gw+fGWhXKQk+NgmSXsjyiQkBfoU7LCcnv8iFI5T3IpxtmlckZvcn/b
|
||||
template:
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
name: git-secrets
|
||||
namespace: flux-system
|
||||
---
|
||||
6
go.mod
6
go.mod
@@ -1,11 +1,15 @@
|
||||
module github.com/direct-dev-ru/linux-command-gpt
|
||||
|
||||
go 1.18
|
||||
go 1.21
|
||||
|
||||
toolchain go1.23.4
|
||||
|
||||
require github.com/atotto/clipboard v0.1.4
|
||||
|
||||
require gopkg.in/yaml.v3 v3.0.1
|
||||
|
||||
require github.com/golang-jwt/jwt/v5 v5.3.0
|
||||
|
||||
require (
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 //indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0
|
||||
|
||||
2
go.sum
2
go.sum
@@ -2,6 +2,8 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z
|
||||
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/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
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=
|
||||
|
||||
@@ -2,6 +2,7 @@ package gpt
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"runtime"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
@@ -9,6 +10,9 @@ import (
|
||||
//go:embed builtin_prompts.yaml
|
||||
var builtinPromptsYAML string
|
||||
|
||||
//go:embed builtin_prompts_windows.yaml
|
||||
var builtinPromptsWindowsYAML string
|
||||
|
||||
var builtinPrompts string
|
||||
|
||||
// BuiltinPromptsData структура для YAML файла
|
||||
@@ -117,7 +121,12 @@ func GetBuiltinPromptByIDAndLanguage(id int, lang string) *SystemPrompt {
|
||||
func InitBuiltinPrompts(embeddedBuiltinPromptsYAML string) {
|
||||
// Используем встроенный YAML, если переданный параметр пустой
|
||||
if embeddedBuiltinPromptsYAML == "" {
|
||||
// Выбираем промпты в зависимости от операционной системы
|
||||
if runtime.GOOS == "windows" {
|
||||
builtinPrompts = builtinPromptsWindowsYAML
|
||||
} else {
|
||||
builtinPrompts = builtinPromptsYAML
|
||||
}
|
||||
} else {
|
||||
builtinPrompts = embeddedBuiltinPromptsYAML
|
||||
}
|
||||
|
||||
262
gpt/builtin_prompts_windows.yaml
Normal file
262
gpt/builtin_prompts_windows.yaml
Normal file
@@ -0,0 +1,262 @@
|
||||
prompts:
|
||||
- id: 1
|
||||
name: "windows-command"
|
||||
description:
|
||||
en: "Main prompt for generating Windows commands"
|
||||
ru: "Основной промпт для генерации Windows команд"
|
||||
content:
|
||||
en: |
|
||||
You are a Windows command line expert.
|
||||
Analyze the user's task, given in natural language, and suggest
|
||||
a Windows command (PowerShell, CMD, or batch) 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 explanation formatting or code blocks, without ```powershell``` or ```cmd```, ` or ``` symbols.
|
||||
|
||||
ru: |
|
||||
Вы эксперт по Windows командам и командной строке.
|
||||
Проанализируйте задачу пользователя на естественном языке и предложите Windows команду или набор команд (PowerShell, CMD или batch), которые помогут выполнить эту задачу, и предоставьте подробное объяснение того, что она делает, её параметры и возможные случаи использования.
|
||||
Сосредоточьтесь на практических примерах и лучших практиках.
|
||||
В ответе должна присутствовать только команда или последовательность команд,
|
||||
готовая к копированию и выполнению в командной строке
|
||||
без объяснений, выделений и форматирования наподобие ```powershell``` или ```cmd```, без символов ` или ```.
|
||||
|
||||
- id: 2
|
||||
name: "windows-command-with-explanation"
|
||||
description:
|
||||
en: "Prompt with detailed command explanation"
|
||||
ru: "Промпт с подробным объяснением команд"
|
||||
content:
|
||||
en: |
|
||||
You are a Windows system administrator with extensive experience.
|
||||
Generate Windows commands based on user task descriptions and provide comprehensive explanations.
|
||||
|
||||
Provide a detailed analysis including:
|
||||
1. **Generated Command**: The Windows 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: |
|
||||
Вы системный администратор Windows с обширным опытом.
|
||||
Генерируйте Windows команды на основе описаний задач пользователей и предоставляйте исчерпывающие объяснения.
|
||||
|
||||
Предоставьте подробный анализ, включая:
|
||||
1. **Сгенерированная команда**: Windows команда, которая выполняет задачу
|
||||
2. **Разбор команды**: Объясните каждую часть команды
|
||||
3. **Параметры**: Объясните каждый используемый флаг и опцию
|
||||
4. **Примеры**: Покажите практические сценарии использования
|
||||
5. **Безопасность**: Выделите любые соображения безопасности
|
||||
6. **Альтернативы**: Предложите похожие команды, если применимо
|
||||
7. **Лучшие практики**: Рекомендуйте оптимальное использование
|
||||
|
||||
Используйте четкое форматирование с заголовками и маркерами для читаемости.
|
||||
|
||||
- id: 3
|
||||
name: "windows-command-safe"
|
||||
description:
|
||||
en: "Safe command analysis with warnings"
|
||||
ru: "Безопасный анализ команд с предупреждениями"
|
||||
content:
|
||||
en: |
|
||||
You are a Windows security expert. Generate safe Windows 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 Windows 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: |
|
||||
Вы эксперт по безопасности Windows. Генерируйте безопасные Windows команды на основе описаний задач пользователей с акцентом на безопасность и последствия для безопасности.
|
||||
|
||||
Предоставьте анализ, ориентированный на безопасность:
|
||||
1. **Сгенерированная безопасная команда**: Безопасная Windows команда для задачи
|
||||
2. **Оценка безопасности**: Почему эта команда безопасна для выполнения
|
||||
3. **Потенциальные риски**: Что может пойти не так и как это смягчить
|
||||
4. **Воздействие на данные**: Какие файлы или данные могут быть затронуты
|
||||
5. **Разрешения**: Какие разрешения требуются
|
||||
6. **Восстановление**: Как отменить изменения при необходимости
|
||||
7. **Лучшие практики**: Безопасные альтернативы или меры предосторожности
|
||||
8. **Предупреждения**: Критические соображения безопасности
|
||||
|
||||
Всегда приоритизируйте безопасность пользователя и защиту данных.
|
||||
|
||||
- id: 4
|
||||
name: "windows-command-verbose"
|
||||
description:
|
||||
en: "Detailed analysis with technical details"
|
||||
ru: "Подробный анализ с техническими деталями"
|
||||
content:
|
||||
en: |
|
||||
You are a Windows kernel and system expert. Generate Windows commands based on user task descriptions and provide an in-depth technical analysis.
|
||||
|
||||
Deliver a comprehensive technical breakdown:
|
||||
1. **Generated Command**: The Windows command that accomplishes the task
|
||||
2. **System Level**: How the command interacts with the Windows kernel
|
||||
3. **Process Flow**: Step-by-step execution details
|
||||
4. **Resource Usage**: CPU, memory, I/O implications
|
||||
5. **Registry**: Impact on Windows registry
|
||||
6. **Services**: Windows services interactions
|
||||
7. **Performance**: Optimization considerations
|
||||
8. **Debugging**: Troubleshooting approaches
|
||||
9. **Advanced Usage**: Expert-level techniques
|
||||
|
||||
Include technical details, system calls, and low-level operations.
|
||||
ru: |
|
||||
Вы эксперт по ядру Windows и системам. Генерируйте Windows команды на основе описаний задач пользователей и предоставляйте глубокий технический анализ.
|
||||
|
||||
Предоставьте исчерпывающий технический разбор:
|
||||
1. **Сгенерированная команда**: Windows команда, которая выполняет задачу
|
||||
2. **Системный уровень**: Как команда взаимодействует с ядром Windows
|
||||
3. **Поток выполнения**: Детали пошагового выполнения
|
||||
4. **Использование ресурсов**: Последствия для CPU, памяти, I/O
|
||||
5. **Реестр**: Воздействие на реестр Windows
|
||||
6. **Службы**: Взаимодействие со службами Windows
|
||||
7. **Производительность**: Соображения по оптимизации
|
||||
8. **Отладка**: Подходы к устранению неполадок
|
||||
9. **Продвинутое использование**: Техники экспертного уровня
|
||||
|
||||
Включите технические детали, системные вызовы и низкоуровневые операции.
|
||||
|
||||
- id: 5
|
||||
name: "windows-command-simple"
|
||||
description:
|
||||
en: "Simple and clear explanation"
|
||||
ru: "Простое и понятное объяснение"
|
||||
content:
|
||||
en: |
|
||||
You are a friendly Windows 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: |
|
||||
Вы дружелюбный наставник по Windows. Объясните данную команду простыми, понятными терминами.
|
||||
|
||||
Команда: {{.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 Windows command expert. You can provide a clear and concise explanation of the given Windows 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: |
|
||||
Вы эксперт по Windows командам. Вы можете предоставьте четкое и краткое объяснение заданной Windows команды.
|
||||
Ваши краткие объяснения должны включать:
|
||||
1. Что делает эта команда
|
||||
2. Основные параметры и их назначение
|
||||
3. Общие случаи использования
|
||||
4. Любые важные предупреждения или соображения
|
||||
|
||||
- id: 7
|
||||
name: "verbose-vv"
|
||||
description:
|
||||
en: "Prompt for vv mode (detailed explanation)"
|
||||
ru: "Промпт для режима vv (подробное объяснение)"
|
||||
content:
|
||||
en: |
|
||||
You are a Windows 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: |
|
||||
Вы эксперт по Windows системам. Предоставьте подробное техническое объяснение заданной команды.
|
||||
|
||||
Предоставьте исчерпывающий анализ:
|
||||
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 Windows 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 Windows 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. **Registry Operations**: Registry I/O operations and 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: |
|
||||
Вы эксперт по ядру Windows и системной архитектуре. Предоставьте исчерпывающий технический анализ заданной команды.
|
||||
|
||||
Предоставьте исчерпывающий технический глубокий анализ:
|
||||
1. **Системная архитектура**: Как она вписывается в экосистему Windows
|
||||
2. **Взаимодействие с ядром**: Системные вызовы и операции ядра
|
||||
3. **Управление процессами**: Создание, планирование и жизненный цикл процессов
|
||||
4. **Управление памятью**: Выделение и управление памятью
|
||||
5. **Операции реестра**: I/O операции реестра и воздействие
|
||||
6. **Сетевой стек**: Сетевые операции и протоколы
|
||||
7. **Модель безопасности**: Разрешения, возможности и последствия безопасности
|
||||
8. **Анализ производительности**: Воздействие на CPU, память, I/O и сеть
|
||||
9. **Отладка и профилирование**: Продвинутые техники устранения неполадок
|
||||
10. **Анализ исходного кода**: Ключевые детали реализации
|
||||
11. **Альтернативные реализации**: Разные подходы и компромиссы
|
||||
12. **Исторический контекст**: Эволюция и история разработки
|
||||
|
||||
Предоставьте максимальную техническую глубину с системными инсайтами, примерами кода и архитектурным пониманием.
|
||||
@@ -57,6 +57,7 @@ func (pm *PromptManager) createInitialPromptsFile() {
|
||||
pm.Language = "ru"
|
||||
|
||||
// Загружаем все встроенные промпты из YAML на русском языке
|
||||
// Функция GetBuiltinPromptsByLanguage уже учитывает операционную систему
|
||||
pm.Prompts = GetBuiltinPromptsByLanguage("ru")
|
||||
|
||||
// Сохраняем все промпты в файл
|
||||
@@ -65,40 +66,8 @@ func (pm *PromptManager) createInitialPromptsFile() {
|
||||
|
||||
// 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
|
||||
// Используем встроенные промпты, которые автоматически выбираются по ОС
|
||||
pm.Prompts = GetBuiltinPromptsByLanguage("en")
|
||||
}
|
||||
|
||||
// loadAllPrompts загружает все промпты из файла sys_prompts
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||
)
|
||||
|
||||
// Provider интерфейс для работы с разными LLM провайдерами
|
||||
@@ -112,7 +114,7 @@ func (p *ProxyAPIProvider) Chat(messages []Chat) (string, error) {
|
||||
return "", fmt.Errorf("ошибка маршалинга запроса: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", p.BaseURL+"/api/v1/protected/sberchat/chat", bytes.NewBuffer(jsonData))
|
||||
req, err := http.NewRequest("POST", p.BaseURL+config.AppConfig.Server.ProxyUrl, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("ошибка создания запроса: %w", err)
|
||||
}
|
||||
@@ -122,6 +124,11 @@ func (p *ProxyAPIProvider) Chat(messages []Chat) (string, error) {
|
||||
req.Header.Set("Authorization", "Bearer "+p.JWTToken)
|
||||
}
|
||||
|
||||
if config.AppConfig.MainFlags.Debug {
|
||||
fmt.Println("Chat URL: ", p.BaseURL+config.AppConfig.Server.ProxyUrl)
|
||||
fmt.Println("ProxyChatRequest: ", req)
|
||||
}
|
||||
|
||||
resp, err := p.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("ошибка выполнения запроса: %w", err)
|
||||
@@ -155,7 +162,7 @@ func (p *ProxyAPIProvider) Chat(messages []Chat) (string, error) {
|
||||
|
||||
// Health для ProxyAPIProvider
|
||||
func (p *ProxyAPIProvider) Health() error {
|
||||
req, err := http.NewRequest("GET", p.BaseURL+"/api/v1/protected/sberchat/health", nil)
|
||||
req, err := http.NewRequest("GET", p.BaseURL+config.AppConfig.Server.HealthUrl, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ошибка создания health check запроса: %w", err)
|
||||
}
|
||||
|
||||
45
kustomize/configmap.yaml
Normal file
45
kustomize/configmap.yaml
Normal file
@@ -0,0 +1,45 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: lcg-config
|
||||
namespace: lcg
|
||||
data:
|
||||
# Основные настройки
|
||||
LCG_VERSION: "v2.0.14"
|
||||
LCG_BASE_PATH: "/lcg"
|
||||
LCG_SERVER_HOST: "0.0.0.0"
|
||||
LCG_SERVER_PORT: "8080"
|
||||
LCG_SERVER_ALLOW_HTTP: "true"
|
||||
LCG_APP_NAME: "Linux Command GPT"
|
||||
LCG_RESULT_FOLDER: "/app/data/results"
|
||||
LCG_PROMPT_FOLDER: "/app/data/prompts"
|
||||
LCG_CONFIG_FOLDER: "/app/data/config"
|
||||
LCG_NO_HISTORY: "false"
|
||||
LCG_ALLOW_EXECUTION: "false"
|
||||
LCG_DEBUG: "true"
|
||||
LCG_PROVIDER: "proxy"
|
||||
|
||||
# Настройки аутентификации
|
||||
LCG_SERVER_REQUIRE_AUTH: "true"
|
||||
|
||||
LCG_COOKIE_SECURE: "true"
|
||||
LCG_COOKIE_TTL_HOURS: "168"
|
||||
LCG_DOMAIN: "direct-dev.ru"
|
||||
LCG_COOKIE_PATH: "/lcg"
|
||||
|
||||
# Настройки провайдера (по умолчанию)
|
||||
LCG_PROVIDER_TYPE: "proxy"
|
||||
LCG_HOST: "https://direct-dev.ru"
|
||||
LCG_HEALTH_URL: "/api/v1/protected/sberchat/health"
|
||||
LCG_PROXY_URL: "/api/v1/protected/sberchat/chat"
|
||||
LCG_MODEL: "GigaChat-2-Max"
|
||||
|
||||
# Настройки валидации
|
||||
LCG_MAX_SYSTEM_PROMPT_LENGTH: "2000"
|
||||
LCG_MAX_USER_MESSAGE_LENGTH: "4000"
|
||||
LCG_MAX_PROMPT_NAME_LENGTH: "2000"
|
||||
LCG_MAX_PROMPT_DESC_LENGTH: "50000"
|
||||
|
||||
# Настройки таймаутов
|
||||
LCG_TIMEOUT: "300"
|
||||
|
||||
91
kustomize/deployment.yaml
Normal file
91
kustomize/deployment.yaml
Normal file
@@ -0,0 +1,91 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: lcg
|
||||
namespace: lcg
|
||||
labels:
|
||||
app: lcg
|
||||
version: v2.0.14
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: lcg
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: lcg
|
||||
spec:
|
||||
containers:
|
||||
- name: lcg
|
||||
image: kuznetcovay/lcg:v2.0.14
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
protocol: TCP
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: lcg-config
|
||||
- secretRef:
|
||||
name: lcg-secrets
|
||||
env:
|
||||
# Pod information
|
||||
- name: POD_NAME
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.name
|
||||
- name: POD_NAMESPACE
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.namespace
|
||||
- name: NODE_NAME
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: spec.nodeName
|
||||
resources:
|
||||
limits:
|
||||
cpu: 1000m
|
||||
memory: 1Gi
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 512Mi
|
||||
volumeMounts:
|
||||
- name: lcg-data
|
||||
mountPath: /app/data
|
||||
- name: lcg-config
|
||||
mountPath: /app/config
|
||||
readOnly: true
|
||||
# Health checks
|
||||
startupProbe:
|
||||
tcpSocket:
|
||||
port: 8080
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 5
|
||||
failureThreshold: 30
|
||||
readinessProbe:
|
||||
tcpSocket:
|
||||
port: 8080
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
port: 8080
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 60
|
||||
volumes:
|
||||
- name: lcg-data
|
||||
persistentVolumeClaim:
|
||||
claimName: lcg-data
|
||||
- name: lcg-config
|
||||
configMap:
|
||||
name: lcg-config
|
||||
# Security context
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1001
|
||||
readOnlyRootFilesystem: false
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
restartPolicy: Always
|
||||
63
kustomize/ingress-route.yaml
Normal file
63
kustomize/ingress-route.yaml
Normal file
@@ -0,0 +1,63 @@
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: IngressRoute
|
||||
metadata:
|
||||
name: lcg-route
|
||||
namespace: lcg
|
||||
labels:
|
||||
app: lcg
|
||||
spec:
|
||||
entryPoints:
|
||||
- websecure
|
||||
routes:
|
||||
- kind: Rule
|
||||
match: Host(`direct-dev.ru`) && PathPrefix(`/lcg`)
|
||||
services:
|
||||
- name: lcg
|
||||
port: 8080
|
||||
tls:
|
||||
secretName: le-root-direct-dev-ru
|
||||
---
|
||||
|
||||
# apiVersion: traefik.io/v1alpha1
|
||||
# kind: IngressRoute
|
||||
# metadata:
|
||||
# name: lcg-route
|
||||
# namespace: lcg
|
||||
# spec:
|
||||
# entryPoints:
|
||||
# - websecure
|
||||
# routes:
|
||||
# - kind: Rule
|
||||
# match: Host(`direct-dev.ru`) && PathPrefix(`/lcg`)
|
||||
# services:
|
||||
# - name: lcg
|
||||
# port: 8080
|
||||
# middlewares:
|
||||
# - name: lcg-strip-prefix
|
||||
# tls:
|
||||
# secretName: le-root-direct-dev-ru
|
||||
# ---
|
||||
# apiVersion: traefik.io/v1alpha1
|
||||
# kind: Middleware
|
||||
# metadata:
|
||||
# name: lcg-strip-prefix
|
||||
# namespace: lcg
|
||||
# spec:
|
||||
# stripPrefix:
|
||||
# prefixes:
|
||||
# - /lcg
|
||||
# ---
|
||||
# apiVersion: traefik.io/v1alpha1
|
||||
# kind: Middleware
|
||||
# metadata:
|
||||
# name: lcg-headers
|
||||
# namespace: lcg
|
||||
# spec:
|
||||
# headers:
|
||||
# customRequestHeaders:
|
||||
# X-Forwarded-Proto: "https"
|
||||
# X-Forwarded-Port: "443"
|
||||
# customResponseHeaders:
|
||||
# X-Frame-Options: "DENY"
|
||||
# X-Content-Type-Options: "nosniff"
|
||||
# X-XSS-Protection: "1; mode=block"
|
||||
25
kustomize/kustomization.yaml
Normal file
25
kustomize/kustomization.yaml
Normal file
@@ -0,0 +1,25 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
# Namespace
|
||||
namespace: lcg
|
||||
|
||||
# Resources
|
||||
resources:
|
||||
- configmap.yaml
|
||||
- secret.yaml
|
||||
- deployment.yaml
|
||||
- service.yaml
|
||||
- ingress-route.yaml
|
||||
|
||||
# Common labels
|
||||
# commonLabels:
|
||||
# app: lcg
|
||||
# version: v2.0.14
|
||||
# managed-by: kustomize
|
||||
|
||||
# Images
|
||||
# images:
|
||||
# - name: lcg
|
||||
# newName: kuznetcovay/lcg
|
||||
# newTag: v2.0.14
|
||||
18
kustomize/secret.yaml
Normal file
18
kustomize/secret.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
apiVersion: bitnami.com/v1alpha1
|
||||
kind: SealedSecret
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
name: lcg-secrets
|
||||
namespace: lcg
|
||||
spec:
|
||||
encryptedData:
|
||||
LCG_CSRF_SECRET: AgDYqxr2Xlf0Hmqgb9u1OildXogus4pFcqB80Dfc1uYw7wnHNOZZP+Hto1OAifXGg2eOYbB1QEsbW/lLSim/C2zpAjNUfRTkwDhQhK3FWmLOjruKBYHZxHyMuhbBmGhVTFbKApOc9zQXTASMLgIeP2vK5yDhVRr97/OJmMKeTOq8FX8CxXZPKTjehronFS3syUlIocboi0PveFf0dgj7nkYePBMST/FKGe2I1NbpOXYH2VrZWOHeoLYDUrEQMQvSAS1mnxAPkdEOuqbWMJA7cs+KKiIFsoy5eBkOXe8hoQqLfddi7ifnJ6h32StQg67qrAm4IMClS/U9iZK5C3Rm/COsywp8Mp3e1iF0hQ2yPuiHMBibxb3aRKwIryJjeo7x51PMMjJaErbTU+bwaAvMav7znOQ14N410bp2Io6KBNsOLW0DG8/mvmQcEn4Q5f4ZtSLcvaq5BQ0NjzrrIv+eCyvNzA28oAyuR0VieJdrUPqpgELrT64MwC+4m5dqdrNPdWkxbOXPG8ghs1nMSSfI+aU+JjT1vupvJA8m83NPqh+YBewmZiTwq5PdvnlAYZ+SYFLZwDWpdUTnK/hqiUkSDOJbE0pbcm03BaG9FjTJpNsmOcKu05Rs8QfWFJMOqYKC4dTA1itxTkyCxz9Y8KFJzX0RXuksN19xzLiZm+e586VtxExEfkYuBo01R39LOtTey9e8pIB9dKwFsHRAORDaZwkaKRe3RlyF8FoSq4khr79CYdGw6IAYzrg8sXAbWawXPg1osQpNR825PejO3/TwbJDPKfjo8RaHHiFa7UhaHbrm/4FwAj4gSGVL
|
||||
LCG_JWT_SECRET: AgBq2Eb0fh1lO0FP2/ebnwuyiTyrAF+dD3iu7uLVgo7JuLnKRA4998rB1OZGRcZ+MNLpajIa20ezJ3aV2wCl24PSJErhzKC3IPSXoLnpJ0EfXrFGS0Pd86TQqAf0TMN2ALFrHY/LywYW09I90/Ct93WqjDKY4yVPbyVni+njUSwavp0iMEWDjENRTdWDK3f/ym1i8SMJgSjX5N6dazwyWVSeWC8oHAGPBF8rr+rhPnY1Ds7QeR/8M0GK7YLiI2nnKljxo7LjXA61jbgCLyiATcWYRylkylcO+ZU8bAsaOSJGXEjtO6s0GHY7Y9CcHQlb1VTZCVfzu/Pb7OSbIiL4wVWHi0DYScTJluVr5SorUjti8HprW9LJE0sy7GIxMdsRZ/7jfJY/s1nxFJph3Q+wkImoPD7R0Njpkwph14CH3xfhPxBpXTQO14hlhx1VYtVkYeWAtcFht+z1qFz0vn+eYkm9B28U+2uxP4WA5AyyMZBPJVekUxQiEmr2nnowbV+zu0+wRtor/lPREQBInbf9tpvth5fpnXtUCS4P2Ne4WWlY0Y5Sij5Z20l0FA0CwmMOliWUcl9vIAy+of3RvM9XGgGqMWTuJP2QFEt7E8VKzM/aI8XgdJuExSCDjd/cyOIgnz5Byy1YvdICqL1CrVpvL0mdy+KMobr59Fgdv+5q0hiHvXF8p2bkG1DHgWiSK3WARDpKzAs+cfsPomD3jMhJZFOopp1Vu9Jpy+l4IsWoY8W1KajPW2DQWjwULRt+ewNr1mAfj+8ESVSXH8zqLv+hkXeef28h7McPcfH9fx9ejs9ovS/E6Sks6EWp
|
||||
LCG_JWT_TOKEN: AgB6PPDflcIav6fqhCi80Ysv9HPkI5zXIjqfot3jaYON3fNmpKNDIhyvKk4YvLbT4PEZfY+JZP/f17MoJ1eikeiZAO7klkg3wNq3h6TcRTuwM/ST9R/KsWqnfLxm5HzGBsqh39cwv9eU2ovAMXqXPJeO/23HcjOqZg7cWZ2WfknaAUydJc39Cue4zmgxlpIxF37p6/rvJqUGByOOUzlDHoVV3TORi+j6dui352PGG6gVCzcCVGNSsbf4j1VibJ1Lz06WEayMi7ZYkD18rsiKHcFGo2SBhEjGGo00Cbdq0EOUTu6k1Q47evHMLFAhdFK3T2gESB4NfMaAL+6gHS7ouI6SbyOCeAZIGT8e3ggM7MlIsNBrLUeDLEwZG8DjHGItY9KcJG/YxbjZ24b9/IzWDduR8VIUG6XCIrGwQd2jlH8GXmrsq+3KkQr81Tj6Z4/QIa4mcgqSKBr8nCzf31GQhhWgj143VwZtPuHUaAbSsZ4ISbo1PoISUaHymWh1J6qjjrzsvfOdeiKHihA8nLe4ggnV8nrQ5EusA+DzmL/Ti+K+2cc277nC0J1pFhuZs37xi+eQT6TMyUeE12uyCHlG1SiwG8t0wfv2N/yzdugW2eILZbRDZnEoN46lLoeXrTGRiFi25/6Jue0/iTo1AV7ameK4J2teGIhYROqB06kResWVECWm2mWhMpJ7Am5ij7tho3Ot75wrmgXXWCb962MzmIpJG8VIimtoIRNVtlu7+cxMb4D9KFb/i69cMkb+7R+Vm6c37T0T0R/o6QCY+MP0w29xgbGz5PNcLEh//avz4E1JI+AsbvtHOi8/aZ9F7c2DcTfXDcxoA2suxJjRy8Y4uu6rrKUWhklla5G/hs0rZsuTM9iruFasV+AybXDLN2/YNqSAj4oDzi/lYNwvQm5CTZwklHK/fwNPbfCNkY5C94rvzW+OJJ2mR1rcHCfHVWYW/IQRfE11mZVX2m0HT70rDPVopYSHrmlvuuTk3ky5gXym+/FOKBq+BcE0GiDDGl3C5VFtiREhpW4J7zRux5QnHk5fIVyEAZlidKsNSNLwq73+E6W77kMNDU7KCRH23A9BIJPOpN87oZDVX1eBghiM/qBOzP04fw3C6dxu+W/OQvTwZmxLtod07Y64EbdaeqJtjnd7GihAEW2jj+Wkcfz9WHTw38cNpyLqcU3ap13790qVJO6V27b0OmiEiloMYyYHUwcHs8wQA946ns0XOz7zw3r1goJgJS6il93dAThK2UBzw3DIY4yJGrmscPZesWSqL3a+ElGjZWz6n9idmIN7L7oViR7A+p17zwFnpczz/VsV+vj8DwSBwLsw6Q==
|
||||
LCG_SERVER_PASSWORD: AgBYXu3YpewbdveXVFDGKBzJe8Gur70LxYSL3kmM7Q65lTU/Q4smpPKhb/bDPntNc4XmNFUfVZ/P5iv2bCdgZB0ccMp6eGZUKKfa7PVJ1/I1hAVwSWYmkD673tbfF7CoGrgtTYw93IaU4t1aWBRiqgapktRawFY1SEIoU6dAx3nrKivyFFJx5akHnm+7vr0GMAtE+H8P/htW+bg1peg5rVYkQwMyeYdefQ+AA/RMDG74XlRGv0EfU6S/LmJ0HF84jb5VNjyJMAD7NssuSUXglpVhfTRwZZAD789/hTgElQnR1JUAIyRHeY6zerU5sXaaS7l+MJGxMMNGsfgOFQ57kzA7Eafumf0ChKfdGl7c4UaEcq6zENSp1Rrzc84WLoghwhvVIhszuGkSE9aLelJJamBMOnm/rlwOuQJJsMAlTiuGO7pewsHKDQXnuZGKOQyAflEX/SNXzUSdYEGonuWGkjWopZrvjO9TwBReFUfsV/ALoDgjA2b7nFpXff0Ffx+EvYtuudDZFw1PobBAHy2aFkMuUP0GxVh14nbmC38VNMiug4+xfl92TxbxRkOkx/tufb2p2QFaglW5TK9I3ysHxiCODhq+YdhlW+gDS+mzBEQZbXd+3TMjh7sE1DGqbloMDhfvJJA6u6C2t6olW3BghU/AS20tOgNC+aIgWzetC6ODOKJQLAq2jjXG+PmUoJDUhs1SbS7uIsIlLIt/Y62b6roVAK81zQPt3w==
|
||||
template:
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
name: lcg-secrets
|
||||
namespace: lcg
|
||||
---
|
||||
16
kustomize/service.yaml
Normal file
16
kustomize/service.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: lcg
|
||||
namespace: lcg
|
||||
labels:
|
||||
app: lcg
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- port: 8080
|
||||
targetPort: 8080
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
app: lcg
|
||||
250
main.go
250
main.go
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
@@ -18,13 +19,22 @@ import (
|
||||
"github.com/direct-dev-ru/linux-command-gpt/gpt"
|
||||
"github.com/direct-dev-ru/linux-command-gpt/reader"
|
||||
"github.com/direct-dev-ru/linux-command-gpt/serve"
|
||||
"github.com/direct-dev-ru/linux-command-gpt/validation"
|
||||
"github.com/urfave/cli/v2"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
//go:embed VERSION.txt
|
||||
var Version string
|
||||
|
||||
// используем глобальный экземпляр конфига из пакета config
|
||||
//go:embed build-conditions.yaml
|
||||
var BuildConditionsFromYaml string
|
||||
|
||||
type buildConditions struct {
|
||||
NoServe bool `yaml:"no-serve"`
|
||||
}
|
||||
|
||||
var CompileConditions buildConditions
|
||||
|
||||
// disableHistory управляет записью/обновлением истории на уровне процесса (флаг имеет приоритет над env)
|
||||
var disableHistory bool
|
||||
@@ -44,6 +54,14 @@ const (
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
if err := yaml.Unmarshal([]byte(BuildConditionsFromYaml), &CompileConditions); err != nil {
|
||||
fmt.Println("Error parsing build conditions:", err)
|
||||
CompileConditions.NoServe = false
|
||||
}
|
||||
|
||||
fmt.Println("Build conditions:", CompileConditions)
|
||||
|
||||
_ = colorBlue
|
||||
|
||||
gpt.InitBuiltinPrompts("")
|
||||
@@ -55,7 +73,7 @@ func main() {
|
||||
|
||||
app := &cli.App{
|
||||
Name: "lcg",
|
||||
Usage: "Linux Command GPT - Генерация Linux команд из описаний",
|
||||
Usage: config.AppConfig.AppName + " - Генерация Linux команд из описаний",
|
||||
Version: Version,
|
||||
Commands: getCommands(),
|
||||
UsageText: `
|
||||
@@ -66,7 +84,7 @@ lcg [опции] <описание команды>
|
||||
lcg --file /path/to/file.txt "хочу вывести все директории с помощью ls"
|
||||
`,
|
||||
Description: `
|
||||
Linux Command GPT - инструмент для генерации Linux команд из описаний на естественном языке.
|
||||
{{.AppName}} - инструмент для генерации Linux команд из описаний на естественном языке.
|
||||
Поддерживает чтение частей промпта из файлов и позволяет сохранять, копировать или перегенерировать результаты.
|
||||
может задавать системный промпт или выбирать из предустановленных промптов.
|
||||
Переменные окружения:
|
||||
@@ -138,6 +156,12 @@ Linux Command GPT - инструмент для генерации Linux ком
|
||||
Debug: c.Bool("debug"),
|
||||
}
|
||||
disableHistory = config.AppConfig.MainFlags.NoHistory || config.AppConfig.IsNoHistoryEnabled()
|
||||
|
||||
config.AppConfig.MainFlags.Debug = config.AppConfig.MainFlags.Debug || config.GetEnvBool("LCG_DEBUG", false)
|
||||
|
||||
fmt.Println("Debug:", config.AppConfig.MainFlags.Debug)
|
||||
fmt.Println("LCG_DEBUG:", config.GetEnvBool("LCG_DEBUG", false))
|
||||
|
||||
args := c.Args().Slice()
|
||||
|
||||
if len(args) == 0 {
|
||||
@@ -157,6 +181,12 @@ Linux Command GPT - инструмент для генерации Linux ком
|
||||
}
|
||||
}
|
||||
|
||||
if CompileConditions.NoServe {
|
||||
if len(args) > 1 && args[0] == "serve" {
|
||||
printColored("❌ Error: serve command is disabled in this build\n", colorRed)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
executeMain(file, system, strings.Join(args, " "), timeout)
|
||||
return nil
|
||||
},
|
||||
@@ -178,7 +208,7 @@ Linux Command GPT - инструмент для генерации Linux ком
|
||||
}
|
||||
|
||||
func getCommands() []*cli.Command {
|
||||
return []*cli.Command{
|
||||
commands := []*cli.Command{
|
||||
{
|
||||
Name: "update-key",
|
||||
Aliases: []string{"u"},
|
||||
@@ -308,24 +338,20 @@ func getCommands() []*cli.Command {
|
||||
Name: "config",
|
||||
Aliases: []string{"co"}, // Изменено с "c" на "co"
|
||||
Usage: "Show current configuration",
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "full",
|
||||
Aliases: []string{"f"},
|
||||
Usage: "Show full configuration object",
|
||||
},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
fmt.Printf("Provider: %s\n", config.AppConfig.ProviderType)
|
||||
fmt.Printf("Host: %s\n", config.AppConfig.Host)
|
||||
fmt.Printf("Model: %s\n", config.AppConfig.Model)
|
||||
fmt.Printf("Prompt: %s\n", config.AppConfig.Prompt)
|
||||
fmt.Printf("Timeout: %s seconds\n", config.AppConfig.Timeout)
|
||||
if config.AppConfig.ProviderType == "proxy" {
|
||||
fmt.Printf("JWT Token: %s\n", func() string {
|
||||
if config.AppConfig.JwtToken != "" {
|
||||
return "***set***"
|
||||
}
|
||||
currentUser, _ := user.Current()
|
||||
jwtFile := currentUser.HomeDir + "/.proxy_jwt_token"
|
||||
if _, err := os.Stat(jwtFile); err == nil {
|
||||
return "***from file***"
|
||||
}
|
||||
return "***not set***"
|
||||
}())
|
||||
if c.Bool("full") {
|
||||
// Выводим полную конфигурацию в JSON формате
|
||||
showFullConfig()
|
||||
} else {
|
||||
// Выводим краткую конфигурацию
|
||||
showShortConfig()
|
||||
}
|
||||
return nil
|
||||
},
|
||||
@@ -546,16 +572,47 @@ func getCommands() []*cli.Command {
|
||||
host := c.String("host")
|
||||
openBrowser := c.Bool("browser")
|
||||
|
||||
// Пробрасываем глобальный флаг debug для web-сервера
|
||||
// Позволяет запускать: lcg -d serve -p ...
|
||||
if c.Bool("debug") {
|
||||
config.AppConfig.MainFlags.Debug = true
|
||||
// Пробрасываем debug: флаг или переменная окружения LCG_DEBUG
|
||||
// Позволяет запускать: LCG_DEBUG=1 lcg serve ... или lcg -d serve ...
|
||||
config.AppConfig.MainFlags.Debug = c.Bool("debug") || config.GetEnvBool("LCG_DEBUG", false)
|
||||
|
||||
// Обновляем конфигурацию сервера с новыми параметрами
|
||||
config.AppConfig.Server.Host = host
|
||||
config.AppConfig.Server.Port = port
|
||||
// Пересчитываем AllowHTTP на основе нового хоста
|
||||
config.AppConfig.Server.AllowHTTP = getServerAllowHTTPForHost(host)
|
||||
|
||||
// Определяем протокол на основе хоста
|
||||
useHTTPS := !config.AppConfig.Server.AllowHTTP
|
||||
protocol := "http"
|
||||
if useHTTPS {
|
||||
protocol = "https"
|
||||
}
|
||||
|
||||
printColored(fmt.Sprintf("🌐 Запускаю HTTP сервер на %s:%s\n", host, port), colorCyan)
|
||||
printColored(fmt.Sprintf("🌐 Запускаю %s сервер на %s:%s\n", strings.ToUpper(protocol), host, port), colorCyan)
|
||||
printColored(fmt.Sprintf("📁 Папка результатов: %s\n", config.AppConfig.ResultFolder), colorYellow)
|
||||
|
||||
url := fmt.Sprintf("http://%s:%s", host, port)
|
||||
// Предупреждение о самоподписанном сертификате
|
||||
if useHTTPS {
|
||||
printColored("⚠️ Используется самоподписанный SSL сертификат\n", colorYellow)
|
||||
printColored(" Браузер может показать предупреждение о безопасности\n", colorYellow)
|
||||
printColored(" Нажмите 'Дополнительно' → 'Перейти на сайт' для продолжения\n", colorYellow)
|
||||
}
|
||||
|
||||
// Для автооткрытия браузера заменяем 0.0.0.0 на localhost
|
||||
browserHost := host
|
||||
if host == "0.0.0.0" {
|
||||
browserHost = "localhost"
|
||||
}
|
||||
|
||||
// Учитываем BasePath в URL
|
||||
basePath := config.AppConfig.Server.BasePath
|
||||
if basePath == "" || basePath == "/" {
|
||||
basePath = ""
|
||||
} else {
|
||||
basePath = strings.TrimSuffix(basePath, "/")
|
||||
}
|
||||
url := fmt.Sprintf("%s://%s:%s%s", protocol, browserHost, port, basePath)
|
||||
|
||||
if openBrowser {
|
||||
printColored("🌍 Открываю браузер...\n", colorGreen)
|
||||
@@ -573,9 +630,34 @@ func getCommands() []*cli.Command {
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if CompileConditions.NoServe {
|
||||
filteredCommands := []*cli.Command{}
|
||||
for _, cmd := range commands {
|
||||
if cmd.Name != "serve" {
|
||||
filteredCommands = append(filteredCommands, cmd)
|
||||
}
|
||||
}
|
||||
commands = filteredCommands
|
||||
}
|
||||
|
||||
return commands
|
||||
|
||||
}
|
||||
|
||||
func executeMain(file, system, commandInput string, timeout int) {
|
||||
// Валидация длины пользовательского сообщения
|
||||
if err := validation.ValidateUserMessage(commandInput); err != nil {
|
||||
printColored(fmt.Sprintf("❌ Ошибка: %s\n", err.Error()), colorRed)
|
||||
return
|
||||
}
|
||||
|
||||
// Валидация длины системного промпта
|
||||
if err := validation.ValidateSystemPrompt(system); err != nil {
|
||||
printColored(fmt.Sprintf("❌ Ошибка: %s\n", err.Error()), colorRed)
|
||||
return
|
||||
}
|
||||
|
||||
// Выводим debug информацию если включен флаг
|
||||
if config.AppConfig.MainFlags.Debug {
|
||||
printDebugInfo(file, system, commandInput, timeout)
|
||||
@@ -884,3 +966,117 @@ func openBrowserURL(url string) error {
|
||||
|
||||
return fmt.Errorf("не найден ни один из поддерживаемых браузеров")
|
||||
}
|
||||
|
||||
// getServerAllowHTTPForHost определяет AllowHTTP для конкретного хоста
|
||||
func getServerAllowHTTPForHost(host string) bool {
|
||||
// Если переменная явно установлена, используем её
|
||||
if value, exists := os.LookupEnv("LCG_SERVER_ALLOW_HTTP"); exists {
|
||||
if boolValue, err := strconv.ParseBool(value); err == nil {
|
||||
return boolValue
|
||||
}
|
||||
}
|
||||
|
||||
// Если переменная не установлена, определяем по умолчанию на основе хоста
|
||||
return isSecureHost(host)
|
||||
}
|
||||
|
||||
// isSecureHost проверяет, является ли хост безопасным для HTTP
|
||||
func isSecureHost(host string) bool {
|
||||
secureHosts := []string{"localhost", "127.0.0.1", "::1"}
|
||||
for _, secureHost := range secureHosts {
|
||||
if host == secureHost {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// showShortConfig показывает краткую конфигурацию
|
||||
func showShortConfig() {
|
||||
fmt.Printf("Provider: %s\n", config.AppConfig.ProviderType)
|
||||
fmt.Printf("Host: %s\n", config.AppConfig.Host)
|
||||
fmt.Printf("Model: %s\n", config.AppConfig.Model)
|
||||
fmt.Printf("Prompt: %s\n", config.AppConfig.Prompt)
|
||||
fmt.Printf("Timeout: %s seconds\n", config.AppConfig.Timeout)
|
||||
if config.AppConfig.ProviderType == "proxy" {
|
||||
fmt.Printf("JWT Token: %s\n", func() string {
|
||||
if config.AppConfig.JwtToken != "" {
|
||||
return "***set***"
|
||||
}
|
||||
currentUser, _ := user.Current()
|
||||
jwtFile := currentUser.HomeDir + "/.proxy_jwt_token"
|
||||
if _, err := os.Stat(jwtFile); err == nil {
|
||||
return "***from file***"
|
||||
}
|
||||
return "***not set***"
|
||||
}())
|
||||
}
|
||||
}
|
||||
|
||||
// showFullConfig показывает полную конфигурацию в JSON формате
|
||||
func showFullConfig() {
|
||||
// Создаем структуру для безопасного вывода (скрываем чувствительные данные)
|
||||
type SafeConfig struct {
|
||||
Cwd string `json:"cwd"`
|
||||
Host string `json:"host"`
|
||||
Completions string `json:"completions"`
|
||||
Model string `json:"model"`
|
||||
Prompt string `json:"prompt"`
|
||||
ApiKeyFile string `json:"api_key_file"`
|
||||
ResultFolder string `json:"result_folder"`
|
||||
PromptFolder string `json:"prompt_folder"`
|
||||
ProviderType string `json:"provider_type"`
|
||||
JwtToken string `json:"jwt_token"` // Показываем статус, не сам токен
|
||||
PromptID string `json:"prompt_id"`
|
||||
Timeout string `json:"timeout"`
|
||||
ResultHistory string `json:"result_history"`
|
||||
NoHistoryEnv string `json:"no_history_env"`
|
||||
AllowExecution bool `json:"allow_execution"`
|
||||
MainFlags config.MainFlags `json:"main_flags"`
|
||||
Server config.ServerConfig `json:"server"`
|
||||
Validation config.ValidationConfig `json:"validation"`
|
||||
}
|
||||
|
||||
// Создаем безопасную копию конфигурации
|
||||
safeConfig := SafeConfig{
|
||||
Cwd: config.AppConfig.Cwd,
|
||||
Host: config.AppConfig.Host,
|
||||
Completions: config.AppConfig.Completions,
|
||||
Model: config.AppConfig.Model,
|
||||
Prompt: config.AppConfig.Prompt,
|
||||
ApiKeyFile: config.AppConfig.ApiKeyFile,
|
||||
ResultFolder: config.AppConfig.ResultFolder,
|
||||
PromptFolder: config.AppConfig.PromptFolder,
|
||||
ProviderType: config.AppConfig.ProviderType,
|
||||
JwtToken: func() string {
|
||||
if config.AppConfig.JwtToken != "" {
|
||||
return "***set***"
|
||||
}
|
||||
currentUser, _ := user.Current()
|
||||
jwtFile := currentUser.HomeDir + "/.proxy_jwt_token"
|
||||
if _, err := os.Stat(jwtFile); err == nil {
|
||||
return "***from file***"
|
||||
}
|
||||
return "***not set***"
|
||||
}(),
|
||||
PromptID: config.AppConfig.PromptID,
|
||||
Timeout: config.AppConfig.Timeout,
|
||||
ResultHistory: config.AppConfig.ResultHistory,
|
||||
NoHistoryEnv: config.AppConfig.NoHistoryEnv,
|
||||
AllowExecution: config.AppConfig.AllowExecution,
|
||||
MainFlags: config.AppConfig.MainFlags,
|
||||
Server: config.AppConfig.Server,
|
||||
Validation: config.AppConfig.Validation,
|
||||
}
|
||||
|
||||
safeConfig.Server.Password = "***"
|
||||
|
||||
// Выводим JSON с отступами
|
||||
jsonData, err := json.MarshalIndent(safeConfig, "", " ")
|
||||
if err != nil {
|
||||
fmt.Printf("Ошибка сериализации конфигурации: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println(string(jsonData))
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
package main
|
||||
37
serve/api.go
37
serve/api.go
@@ -10,6 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||
"github.com/direct-dev-ru/linux-command-gpt/validation"
|
||||
)
|
||||
|
||||
// SaveResultRequest представляет запрос на сохранение результата
|
||||
@@ -62,6 +63,20 @@ func handleSaveResult(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Валидация длины полей
|
||||
if err := validation.ValidateUserMessage(req.Prompt); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := validation.ValidateCommand(req.Command); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := validation.ValidateExplanation(req.Explanation); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Создаем папку результатов если не существует
|
||||
if err := os.MkdirAll(config.AppConfig.ResultFolder, 0755); err != nil {
|
||||
apiJsonResponse(w, SaveResultResponse{
|
||||
@@ -124,6 +139,28 @@ func handleAddToHistory(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Валидация длины полей
|
||||
if err := validation.ValidateUserMessage(req.Prompt); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := validation.ValidateCommand(req.Command); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := validation.ValidateCommand(req.Response); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := validation.ValidateExplanation(req.Explanation); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := validation.ValidateSystemPrompt(req.System); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Проверяем, есть ли уже такой запрос в истории
|
||||
entries, err := Read(config.AppConfig.ResultHistory)
|
||||
if err != nil {
|
||||
|
||||
269
serve/auth.go
Normal file
269
serve/auth.go
Normal file
@@ -0,0 +1,269 @@
|
||||
package serve
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
// JWTClaims представляет claims для JWT токена
|
||||
type JWTClaims struct {
|
||||
Username string `json:"username"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// AuthRequest представляет запрос на аутентификацию
|
||||
type AuthRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// AuthResponse представляет ответ на аутентификацию
|
||||
type AuthResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// JWTSecretKey генерирует или загружает секретный ключ для JWT
|
||||
func getJWTSecretKey() ([]byte, error) {
|
||||
// Пытаемся загрузить из переменной окружения
|
||||
if secret := os.Getenv("LCG_JWT_SECRET"); secret != "" {
|
||||
return []byte(secret), nil
|
||||
}
|
||||
|
||||
// Пытаемся загрузить из файла
|
||||
secretFile := fmt.Sprintf("%s/server/jwt_secret", config.AppConfig.Server.ConfigFolder)
|
||||
if data, err := os.ReadFile(secretFile); err == nil {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Генерируем новый секретный ключ
|
||||
secret := make([]byte, 32)
|
||||
if _, err := rand.Read(secret); err != nil {
|
||||
return nil, fmt.Errorf("failed to generate JWT secret: %v", err)
|
||||
}
|
||||
|
||||
// Создаем директорию если не существует
|
||||
if err := os.MkdirAll(fmt.Sprintf("%s/server", config.AppConfig.Server.ConfigFolder), 0755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create config directory: %v", err)
|
||||
}
|
||||
|
||||
// Сохраняем секретный ключ в файл
|
||||
if err := os.WriteFile(secretFile, secret, 0600); err != nil {
|
||||
return nil, fmt.Errorf("failed to save JWT secret: %v", err)
|
||||
}
|
||||
|
||||
return secret, nil
|
||||
}
|
||||
|
||||
// generateJWTToken создает JWT токен для пользователя
|
||||
func generateJWTToken(username string) (string, error) {
|
||||
secret, err := getJWTSecretKey()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Создаем claims
|
||||
claims := JWTClaims{
|
||||
Username: username,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)), // Токен действителен 24 часа
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||
Issuer: "lcg-server",
|
||||
},
|
||||
}
|
||||
|
||||
// Создаем токен
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString(secret)
|
||||
}
|
||||
|
||||
// validateJWTToken проверяет JWT токен
|
||||
func validateJWTToken(tokenString string) (*JWTClaims, error) {
|
||||
secret, err := getJWTSecretKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Парсим токен
|
||||
token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (any, error) {
|
||||
// Проверяем метод подписи
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return secret, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Проверяем валидность токена
|
||||
if claims, ok := token.Claims.(*JWTClaims); ok && token.Valid {
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("invalid token")
|
||||
}
|
||||
|
||||
// getTokenFromCookie извлекает JWT токен из cookies
|
||||
func getTokenFromCookie(r *http.Request) (string, error) {
|
||||
cookie, err := r.Cookie("auth_token")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return cookie.Value, nil
|
||||
}
|
||||
|
||||
// setAuthCookie устанавливает HTTP-only cookie с JWT токеном
|
||||
func setAuthCookie(w http.ResponseWriter, token string) {
|
||||
cookie := &http.Cookie{
|
||||
Name: "auth_token",
|
||||
Domain: config.AppConfig.Server.Domain,
|
||||
Value: token,
|
||||
Path: config.AppConfig.Server.CookiePath,
|
||||
HttpOnly: true,
|
||||
Secure: config.AppConfig.Server.CookieSecure,
|
||||
SameSite: http.SameSiteLaxMode, // Более мягкий режим для reverse proxy
|
||||
MaxAge: config.AppConfig.Server.CookieTTLHours * 60 * 60,
|
||||
}
|
||||
|
||||
// Добавляем домен если указан
|
||||
if config.AppConfig.Server.Domain != "" {
|
||||
cookie.Domain = config.AppConfig.Server.Domain
|
||||
}
|
||||
|
||||
http.SetCookie(w, cookie)
|
||||
}
|
||||
|
||||
// clearAuthCookie удаляет cookie с токеном
|
||||
func clearAuthCookie(w http.ResponseWriter) {
|
||||
cookie := &http.Cookie{
|
||||
Name: "auth_token",
|
||||
Value: "",
|
||||
Path: config.AppConfig.Server.CookiePath,
|
||||
HttpOnly: true,
|
||||
Secure: config.AppConfig.Server.CookieSecure,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
MaxAge: -1, // Удаляем cookie
|
||||
}
|
||||
|
||||
// Добавляем домен если указан
|
||||
if config.AppConfig.Server.Domain != "" {
|
||||
cookie.Domain = config.AppConfig.Server.Domain
|
||||
}
|
||||
|
||||
http.SetCookie(w, cookie)
|
||||
}
|
||||
|
||||
// handleLogin обрабатывает запрос на вход
|
||||
func handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var req AuthRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
apiJsonResponse(w, AuthResponse{
|
||||
Success: false,
|
||||
Error: "Invalid request body",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Проверяем пароль
|
||||
if req.Password != config.AppConfig.Server.Password {
|
||||
apiJsonResponse(w, AuthResponse{
|
||||
Success: false,
|
||||
Error: "Неверный пароль",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Генерируем JWT токен
|
||||
token, err := generateJWTToken(req.Username)
|
||||
if err != nil {
|
||||
apiJsonResponse(w, AuthResponse{
|
||||
Success: false,
|
||||
Error: "Failed to generate token",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Устанавливаем cookie
|
||||
setAuthCookie(w, token)
|
||||
|
||||
apiJsonResponse(w, AuthResponse{
|
||||
Success: true,
|
||||
Message: "Успешная авторизация",
|
||||
})
|
||||
}
|
||||
|
||||
// handleLogout обрабатывает запрос на выход
|
||||
func handleLogout(w http.ResponseWriter, r *http.Request) {
|
||||
clearAuthCookie(w)
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// handleValidateToken обрабатывает проверку валидности токена
|
||||
func handleValidateToken(w http.ResponseWriter, r *http.Request) {
|
||||
token, err := getTokenFromCookie(r)
|
||||
if err != nil {
|
||||
apiJsonResponse(w, AuthResponse{
|
||||
Success: false,
|
||||
Error: "Token not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
_, err = validateJWTToken(token)
|
||||
if err != nil {
|
||||
apiJsonResponse(w, AuthResponse{
|
||||
Success: false,
|
||||
Error: "Invalid token",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
apiJsonResponse(w, AuthResponse{
|
||||
Success: true,
|
||||
Message: "Token is valid",
|
||||
})
|
||||
}
|
||||
|
||||
// requireAuth middleware проверяет аутентификацию
|
||||
func requireAuth(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Проверяем, требуется ли аутентификация
|
||||
if !config.AppConfig.Server.RequireAuth {
|
||||
next(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем токен из cookie
|
||||
token, err := getTokenFromCookie(r)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
// Проверяем валидность токена
|
||||
_, err = validateJWTToken(token)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
// Токен валиден, продолжаем
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
263
serve/csrf.go
Normal file
263
serve/csrf.go
Normal file
@@ -0,0 +1,263 @@
|
||||
package serve
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||
)
|
||||
|
||||
// CSRFManager управляет CSRF токенами
|
||||
type CSRFManager struct {
|
||||
secretKey []byte
|
||||
}
|
||||
|
||||
// CSRFData содержит данные для CSRF токена
|
||||
type CSRFData struct {
|
||||
Token string
|
||||
Timestamp int64
|
||||
UserID string
|
||||
}
|
||||
|
||||
// NewCSRFManager создает новый менеджер CSRF
|
||||
func NewCSRFManager() (*CSRFManager, error) {
|
||||
secret, err := getCSRFSecretKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &CSRFManager{secretKey: secret}, nil
|
||||
}
|
||||
|
||||
// getCSRFSecretKey получает или генерирует секретный ключ для CSRF
|
||||
func getCSRFSecretKey() ([]byte, error) {
|
||||
// Пытаемся загрузить из переменной окружения
|
||||
if secret := os.Getenv("LCG_CSRF_SECRET"); secret != "" {
|
||||
return []byte(secret), nil
|
||||
}
|
||||
|
||||
// Пытаемся загрузить из файла
|
||||
secretFile := fmt.Sprintf("%s/server/csrf_secret", config.AppConfig.Server.ConfigFolder)
|
||||
if data, err := os.ReadFile(secretFile); err == nil {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Генерируем новый секретный ключ
|
||||
secret := make([]byte, 32)
|
||||
if _, err := rand.Read(secret); err != nil {
|
||||
return nil, fmt.Errorf("failed to generate CSRF secret: %v", err)
|
||||
}
|
||||
|
||||
// Создаем директорию если не существует
|
||||
if err := os.MkdirAll(fmt.Sprintf("%s/server", config.AppConfig.Server.ConfigFolder), 0755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create config directory: %v", err)
|
||||
}
|
||||
|
||||
// Сохраняем секретный ключ в файл
|
||||
if err := os.WriteFile(secretFile, secret, 0600); err != nil {
|
||||
return nil, fmt.Errorf("failed to save CSRF secret: %v", err)
|
||||
}
|
||||
|
||||
return secret, nil
|
||||
}
|
||||
|
||||
// GenerateToken генерирует CSRF токен для пользователя
|
||||
func (c *CSRFManager) GenerateToken(userID string) (string, error) {
|
||||
// Создаем данные токена
|
||||
data := CSRFData{
|
||||
Token: generateRandomString(32),
|
||||
Timestamp: time.Now().Unix(),
|
||||
UserID: userID,
|
||||
}
|
||||
|
||||
// Создаем подпись
|
||||
signature := c.createSignature(data)
|
||||
|
||||
// Кодируем данные в base64
|
||||
encodedData := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%d:%s", data.Token, data.Timestamp, data.UserID)))
|
||||
|
||||
return fmt.Sprintf("%s.%s", encodedData, signature), nil
|
||||
}
|
||||
|
||||
// ValidateToken проверяет CSRF токен
|
||||
func (c *CSRFManager) ValidateToken(token, userID string) bool {
|
||||
// Разделяем токен на данные и подпись
|
||||
parts := splitToken(token)
|
||||
if len(parts) != 2 {
|
||||
return false
|
||||
}
|
||||
|
||||
encodedData, signature := parts[0], parts[1]
|
||||
|
||||
// Декодируем данные
|
||||
dataBytes, err := base64.StdEncoding.DecodeString(encodedData)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Парсим данные
|
||||
dataParts := splitString(string(dataBytes), ":")
|
||||
if len(dataParts) != 3 {
|
||||
return false
|
||||
}
|
||||
|
||||
tokenValue, timestampStr, tokenUserID := dataParts[0], dataParts[1], dataParts[2]
|
||||
|
||||
// Проверяем пользователя
|
||||
if tokenUserID != userID {
|
||||
return false
|
||||
}
|
||||
|
||||
// Проверяем время жизни токена (24 часа)
|
||||
timestamp, err := parseInt64(timestampStr)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if time.Now().Unix()-timestamp > 24*60*60 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Создаем данные для проверки подписи
|
||||
data := CSRFData{
|
||||
Token: tokenValue,
|
||||
Timestamp: timestamp,
|
||||
UserID: tokenUserID,
|
||||
}
|
||||
|
||||
// Проверяем подпись
|
||||
expectedSignature := c.createSignature(data)
|
||||
return signature == expectedSignature
|
||||
}
|
||||
|
||||
// createSignature создает подпись для данных
|
||||
func (c *CSRFManager) createSignature(data CSRFData) string {
|
||||
message := fmt.Sprintf("%s:%d:%s", data.Token, data.Timestamp, data.UserID)
|
||||
hash := sha256.Sum256(append(c.secretKey, []byte(message)...))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
// getTokenFromCookie извлекает CSRF токен из cookie
|
||||
func GetCSRFTokenFromCookie(r *http.Request) string {
|
||||
cookie, err := r.Cookie("csrf_token")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return cookie.Value
|
||||
}
|
||||
|
||||
// setCSRFCookie устанавливает CSRF токен в cookie
|
||||
func setCSRFCookie(w http.ResponseWriter, token string) {
|
||||
cookie := &http.Cookie{
|
||||
Name: "csrf_token",
|
||||
Value: token,
|
||||
Path: config.AppConfig.Server.CookiePath,
|
||||
HttpOnly: true,
|
||||
Secure: config.AppConfig.Server.CookieSecure,
|
||||
SameSite: http.SameSiteLaxMode, // Более мягкий режим для reverse proxy
|
||||
MaxAge: 1 * 60 * 60,
|
||||
}
|
||||
|
||||
// Добавляем домен если указан
|
||||
if config.AppConfig.Server.Domain != "" {
|
||||
cookie.Domain = config.AppConfig.Server.Domain
|
||||
}
|
||||
|
||||
http.SetCookie(w, cookie)
|
||||
}
|
||||
|
||||
// clearCSRFCookie удаляет CSRF cookie
|
||||
func СlearCSRFCookie(w http.ResponseWriter) {
|
||||
cookie := &http.Cookie{
|
||||
Name: "csrf_token",
|
||||
Value: "",
|
||||
Path: config.AppConfig.Server.CookiePath,
|
||||
HttpOnly: true,
|
||||
Secure: config.AppConfig.Server.CookieSecure,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
MaxAge: -1,
|
||||
}
|
||||
|
||||
// Добавляем домен если указан
|
||||
if config.AppConfig.Server.Domain != "" {
|
||||
cookie.Domain = config.AppConfig.Server.Domain
|
||||
}
|
||||
|
||||
http.SetCookie(w, cookie)
|
||||
}
|
||||
|
||||
// generateRandomString генерирует случайную строку
|
||||
func generateRandomString(length int) string {
|
||||
bytes := make([]byte, length)
|
||||
rand.Read(bytes)
|
||||
return base64.URLEncoding.EncodeToString(bytes)[:length]
|
||||
}
|
||||
|
||||
// splitToken разделяет токен на части
|
||||
func splitToken(token string) []string {
|
||||
// Ищем последнюю точку
|
||||
lastDot := -1
|
||||
for i := len(token) - 1; i >= 0; i-- {
|
||||
if token[i] == '.' {
|
||||
lastDot = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if lastDot == -1 {
|
||||
return []string{token}
|
||||
}
|
||||
|
||||
return []string{token[:lastDot], token[lastDot+1:]}
|
||||
}
|
||||
|
||||
// splitString разделяет строку по разделителю
|
||||
func splitString(s, sep string) []string {
|
||||
if s == "" {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
var result []string
|
||||
start := 0
|
||||
for i := 0; i < len(s); i++ {
|
||||
if i+len(sep) <= len(s) && s[i:i+len(sep)] == sep {
|
||||
result = append(result, s[start:i])
|
||||
start = i + len(sep)
|
||||
i += len(sep) - 1
|
||||
}
|
||||
}
|
||||
result = append(result, s[start:])
|
||||
return result
|
||||
}
|
||||
|
||||
// parseInt64 парсит строку в int64
|
||||
func parseInt64(s string) (int64, error) {
|
||||
var result int64
|
||||
for _, char := range s {
|
||||
if char < '0' || char > '9' {
|
||||
return 0, fmt.Errorf("invalid number: %s", s)
|
||||
}
|
||||
result = result*10 + int64(char-'0')
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Глобальный экземпляр CSRF менеджера
|
||||
var csrfManager *CSRFManager
|
||||
|
||||
// InitCSRFManager инициализирует глобальный CSRF менеджер
|
||||
func InitCSRFManager() error {
|
||||
var err error
|
||||
csrfManager, err = NewCSRFManager()
|
||||
return err
|
||||
}
|
||||
|
||||
// GetCSRFManager возвращает глобальный CSRF менеджер
|
||||
func GetCSRFManager() *CSRFManager {
|
||||
return csrfManager
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"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/validation"
|
||||
)
|
||||
|
||||
// ExecuteRequest представляет запрос на выполнение
|
||||
@@ -58,9 +59,20 @@ func handleExecute(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Валидация длины пользовательского сообщения
|
||||
if err := validation.ValidateUserMessage(req.Prompt); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Определяем системный промпт
|
||||
systemPrompt := ""
|
||||
if req.SystemText != "" {
|
||||
// Валидация длины пользовательского системного промпта
|
||||
if err := validation.ValidateSystemPrompt(req.SystemText); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
systemPrompt = req.SystemText
|
||||
} else if req.SystemID > 0 && req.SystemID <= 5 {
|
||||
// Получаем системный промпт по ID
|
||||
@@ -70,9 +82,19 @@ func handleExecute(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "Failed to get system prompt", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// Валидация длины системного промпта из базы
|
||||
if err := validation.ValidateSystemPrompt(prompt.Content); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
systemPrompt = prompt.Content
|
||||
} else {
|
||||
// Используем промпт по умолчанию
|
||||
// Валидация длины системного промпта по умолчанию
|
||||
if err := validation.ValidateSystemPrompt(config.AppConfig.Prompt); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
systemPrompt = config.AppConfig.Prompt
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"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/direct-dev-ru/linux-command-gpt/validation"
|
||||
"github.com/russross/blackfriday/v2"
|
||||
)
|
||||
|
||||
@@ -22,6 +23,14 @@ type ExecutePageData struct {
|
||||
ResultSection template.HTML
|
||||
VerboseButtons template.HTML
|
||||
ActionButtons template.HTML
|
||||
CSRFToken string
|
||||
ProviderType string
|
||||
Model string
|
||||
Host string
|
||||
BasePath string
|
||||
AppName string
|
||||
// Поля конфигурации для валидации
|
||||
MaxUserMessageLength int
|
||||
}
|
||||
|
||||
// SystemPromptOption представляет опцию системного промпта
|
||||
@@ -47,7 +56,7 @@ func handleExecutePage(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
// Показываем форму
|
||||
showExecuteForm(w)
|
||||
showExecuteForm(w, r)
|
||||
case http.MethodPost:
|
||||
// Обрабатываем выполнение
|
||||
handleExecuteRequest(w, r)
|
||||
@@ -57,7 +66,25 @@ func handleExecutePage(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// showExecuteForm показывает форму выполнения
|
||||
func showExecuteForm(w http.ResponseWriter) {
|
||||
func showExecuteForm(w http.ResponseWriter, r *http.Request) {
|
||||
// Генерируем CSRF токен
|
||||
csrfManager := GetCSRFManager()
|
||||
if csrfManager == nil {
|
||||
http.Error(w, "CSRF manager not initialized", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем сессионный ID
|
||||
sessionID := getSessionID(r)
|
||||
csrfToken, err := csrfManager.GenerateToken(sessionID)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to generate CSRF token", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Устанавливаем CSRF токен в cookie
|
||||
setCSRFCookie(w, csrfToken)
|
||||
|
||||
// Получаем системные промпты
|
||||
pm := gpt.NewPromptManager(config.AppConfig.PromptFolder)
|
||||
|
||||
@@ -81,6 +108,13 @@ func showExecuteForm(w http.ResponseWriter) {
|
||||
ResultSection: template.HTML(""),
|
||||
VerboseButtons: template.HTML(""),
|
||||
ActionButtons: template.HTML(""),
|
||||
CSRFToken: csrfToken,
|
||||
ProviderType: config.AppConfig.ProviderType,
|
||||
Model: config.AppConfig.Model,
|
||||
Host: config.AppConfig.Host,
|
||||
BasePath: getBasePath(),
|
||||
AppName: config.AppConfig.AppName,
|
||||
MaxUserMessageLength: config.AppConfig.Validation.MaxUserMessageLength,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
@@ -102,6 +136,12 @@ func handleExecuteRequest(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Валидация длины пользовательского сообщения
|
||||
if err := validation.ValidateUserMessage(prompt); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
systemID := 1
|
||||
if systemIDStr != "" {
|
||||
if id, err := strconv.Atoi(systemIDStr); err == nil && id >= 1 && id <= 5 {
|
||||
@@ -116,6 +156,12 @@ func handleExecuteRequest(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Валидация длины системного промпта
|
||||
if err := validation.ValidateSystemPrompt(systemPrompt.Content); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Создаем GPT клиент
|
||||
gpt3 := gpt.NewGpt3(
|
||||
config.AppConfig.ProviderType,
|
||||
@@ -178,6 +224,15 @@ func handleExecuteRequest(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// Генерируем CSRF токен для результата
|
||||
csrfManager := GetCSRFManager()
|
||||
sessionID := getSessionID(r)
|
||||
csrfToken, err := csrfManager.GenerateToken(sessionID)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to generate CSRF token", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
data := ExecutePageData{
|
||||
Title: "Результат выполнения",
|
||||
Header: "Результат выполнения",
|
||||
@@ -186,6 +241,13 @@ func handleExecuteRequest(w http.ResponseWriter, r *http.Request) {
|
||||
ResultSection: template.HTML(formatResultSection(result)),
|
||||
VerboseButtons: template.HTML(formatVerboseButtons(result)),
|
||||
ActionButtons: template.HTML(formatActionButtons(result)),
|
||||
CSRFToken: csrfToken,
|
||||
ProviderType: config.AppConfig.ProviderType,
|
||||
Model: config.AppConfig.Model,
|
||||
Host: config.AppConfig.Host,
|
||||
BasePath: getBasePath(),
|
||||
AppName: config.AppConfig.AppName,
|
||||
MaxUserMessageLength: config.AppConfig.Validation.MaxUserMessageLength,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -39,8 +40,12 @@ func handleHistoryPage(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
data := struct {
|
||||
Entries []HistoryEntryInfo
|
||||
BasePath string
|
||||
AppName string
|
||||
}{
|
||||
Entries: historyEntries,
|
||||
BasePath: getBasePath(),
|
||||
AppName: config.AppConfig.AppName,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
@@ -54,6 +59,11 @@ func readHistoryEntries() ([]HistoryEntryInfo, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Сортируем записи по времени в убывающем порядке (новые сначала)
|
||||
sort.Slice(entries, func(i, j int) bool {
|
||||
return entries[i].Timestamp.After(entries[j].Timestamp)
|
||||
})
|
||||
|
||||
var result []HistoryEntryInfo
|
||||
for _, entry := range entries {
|
||||
result = append(result, HistoryEntryInfo{
|
||||
@@ -74,7 +84,15 @@ func handleDeleteHistoryEntry(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
indexStr := strings.TrimPrefix(r.URL.Path, "/history/delete/")
|
||||
// Убираем BasePath из URL перед извлечением индекса
|
||||
basePath := config.AppConfig.Server.BasePath
|
||||
var indexStr string
|
||||
if basePath != "" && basePath != "/" {
|
||||
basePath = strings.TrimSuffix(basePath, "/")
|
||||
indexStr = strings.TrimPrefix(r.URL.Path, basePath+"/history/delete/")
|
||||
} else {
|
||||
indexStr = strings.TrimPrefix(r.URL.Path, "/history/delete/")
|
||||
}
|
||||
index, err := strconv.Atoi(indexStr)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid index", http.StatusBadRequest)
|
||||
@@ -110,8 +128,15 @@ func handleClearHistory(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// handleHistoryView обрабатывает просмотр записи истории
|
||||
func handleHistoryView(w http.ResponseWriter, r *http.Request) {
|
||||
// Получаем индекс из URL
|
||||
indexStr := strings.TrimPrefix(r.URL.Path, "/history/view/")
|
||||
// Получаем индекс из URL, учитывая BasePath
|
||||
basePath := config.AppConfig.Server.BasePath
|
||||
var indexStr string
|
||||
if basePath != "" && basePath != "/" {
|
||||
basePath = strings.TrimSuffix(basePath, "/")
|
||||
indexStr = strings.TrimPrefix(r.URL.Path, basePath+"/history/view/")
|
||||
} else {
|
||||
indexStr = strings.TrimPrefix(r.URL.Path, "/history/view/")
|
||||
}
|
||||
index, err := strconv.Atoi(indexStr)
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
@@ -151,18 +176,31 @@ func handleHistoryView(w http.ResponseWriter, r *http.Request) {
|
||||
</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
|
||||
)
|
||||
// Создаем данные для шаблона
|
||||
data := struct {
|
||||
Index int
|
||||
Timestamp string
|
||||
Command string
|
||||
Response string
|
||||
ExplanationHTML template.HTML
|
||||
BasePath string
|
||||
}{
|
||||
Index: index,
|
||||
Timestamp: targetEntry.Timestamp.Format("02.01.2006 15:04:05"),
|
||||
Command: targetEntry.Command,
|
||||
Response: targetEntry.Response,
|
||||
ExplanationHTML: template.HTML(explanationSection),
|
||||
BasePath: getBasePath(),
|
||||
}
|
||||
|
||||
// Парсим и выполняем шаблон
|
||||
tmpl := templates.HistoryViewTemplate
|
||||
t, err := template.New("history_view").Parse(tmpl)
|
||||
if err != nil {
|
||||
http.Error(w, "Ошибка шаблона", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.Write([]byte(htmlPage))
|
||||
t.Execute(w, data)
|
||||
}
|
||||
|
||||
@@ -21,9 +21,20 @@ type HistoryEntry struct {
|
||||
// read читает записи истории из файла
|
||||
func Read(historyPath string) ([]HistoryEntry, error) {
|
||||
data, err := os.ReadFile(historyPath)
|
||||
if err != nil || len(data) == 0 {
|
||||
if err != nil {
|
||||
// Если файл не существует, создаем пустой файл истории
|
||||
if os.IsNotExist(err) {
|
||||
emptyHistory := []HistoryEntry{}
|
||||
if writeErr := Write(historyPath, emptyHistory); writeErr != nil {
|
||||
return nil, fmt.Errorf("не удалось создать файл истории: %v", writeErr)
|
||||
}
|
||||
return emptyHistory, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return []HistoryEntry{}, nil
|
||||
}
|
||||
var items []HistoryEntry
|
||||
if err := json.Unmarshal(data, &items); err != nil {
|
||||
return nil, err
|
||||
|
||||
105
serve/login.go
Normal file
105
serve/login.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package serve
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"html/template"
|
||||
"net/http"
|
||||
|
||||
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||
"github.com/direct-dev-ru/linux-command-gpt/serve/templates"
|
||||
)
|
||||
|
||||
// handleLoginPage обрабатывает страницу входа
|
||||
func handleLoginPage(w http.ResponseWriter, r *http.Request) {
|
||||
// Если пользователь уже авторизован, перенаправляем на главную
|
||||
if isAuthenticated(r) {
|
||||
http.Redirect(w, r, makePath("/"), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
// Генерируем CSRF токен
|
||||
csrfManager := GetCSRFManager()
|
||||
if csrfManager == nil {
|
||||
http.Error(w, "CSRF manager not initialized", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Для неавторизованных пользователей используем сессионный ID
|
||||
sessionID := getSessionID(r)
|
||||
csrfToken, err := csrfManager.GenerateToken(sessionID)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to generate CSRF token", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Устанавливаем CSRF токен в cookie
|
||||
setCSRFCookie(w, csrfToken)
|
||||
|
||||
data := LoginPageData{
|
||||
Title: "Авторизация - LCG",
|
||||
Message: "",
|
||||
Error: "",
|
||||
CSRFToken: csrfToken,
|
||||
BasePath: getBasePath(),
|
||||
}
|
||||
|
||||
if err := RenderLoginPage(w, data); err != nil {
|
||||
http.Error(w, "Failed to render login page", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// isAuthenticated проверяет, авторизован ли пользователь
|
||||
func isAuthenticated(r *http.Request) bool {
|
||||
// Проверяем, требуется ли аутентификация
|
||||
if !config.AppConfig.Server.RequireAuth {
|
||||
return true
|
||||
}
|
||||
|
||||
// Получаем токен из cookie
|
||||
token, err := getTokenFromCookie(r)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Проверяем валидность токена
|
||||
_, err = validateJWTToken(token)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// LoginPageData представляет данные для страницы входа
|
||||
type LoginPageData struct {
|
||||
Title string
|
||||
Message string
|
||||
Error string
|
||||
CSRFToken string
|
||||
BasePath string
|
||||
}
|
||||
|
||||
// RenderLoginPage рендерит страницу входа
|
||||
func RenderLoginPage(w http.ResponseWriter, data LoginPageData) error {
|
||||
tmpl, err := template.New("login").Parse(templates.LoginPageTemplate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
return tmpl.Execute(w, data)
|
||||
}
|
||||
|
||||
// getSessionID получает или создает сессионный ID для пользователя
|
||||
func getSessionID(r *http.Request) string {
|
||||
// Пытаемся получить из cookie
|
||||
if cookie, err := r.Cookie("session_id"); err == nil {
|
||||
return cookie.Value
|
||||
}
|
||||
|
||||
// Если нет cookie, генерируем новый ID на основе IP и User-Agent
|
||||
ip := r.RemoteAddr
|
||||
userAgent := r.Header.Get("User-Agent")
|
||||
|
||||
// Создаем простой хеш для сессии
|
||||
hash := sha256.Sum256([]byte(ip + userAgent))
|
||||
return hex.EncodeToString(hash[:])[:16]
|
||||
}
|
||||
114
serve/middleware.go
Normal file
114
serve/middleware.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package serve
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||
)
|
||||
|
||||
// AuthMiddleware проверяет аутентификацию для всех запросов
|
||||
func AuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Проверяем, требуется ли аутентификация
|
||||
if !config.AppConfig.Server.RequireAuth {
|
||||
next(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Исключаем страницу входа и API логина из проверки (с учетом BasePath)
|
||||
if r.URL.Path == makePath("/login") || r.URL.Path == makePath("/api/login") || r.URL.Path == makePath("/api/validate-token") {
|
||||
next(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Проверяем аутентификацию
|
||||
if !isAuthenticated(r) {
|
||||
// Для API запросов возвращаем JSON ошибку
|
||||
if isAPIRequest(r) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte(`{"success": false, "error": "Authentication required"}`))
|
||||
return
|
||||
}
|
||||
|
||||
// Для веб-запросов перенаправляем на страницу входа (с учетом BasePath)
|
||||
http.Redirect(w, r, makePath("/login"), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
// Пользователь аутентифицирован, продолжаем
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// CSRFMiddleware проверяет CSRF токены для POST/PUT/DELETE запросов
|
||||
func CSRFMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Проверяем только изменяющие запросы
|
||||
if r.Method == "GET" || r.Method == "HEAD" || r.Method == "OPTIONS" {
|
||||
next(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Исключаем некоторые API endpoints (с учетом BasePath)
|
||||
if r.URL.Path == makePath("/api/login") || r.URL.Path == makePath("/api/logout") {
|
||||
next(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем CSRF токен из заголовка или формы
|
||||
csrfToken := r.Header.Get("X-CSRF-Token")
|
||||
if csrfToken == "" {
|
||||
csrfToken = r.FormValue("csrf_token")
|
||||
}
|
||||
|
||||
if csrfToken == "" {
|
||||
// Для API запросов возвращаем JSON ошибку
|
||||
if isAPIRequest(r) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
w.Write([]byte(`{"success": false, "error": "CSRF token required"}`))
|
||||
return
|
||||
}
|
||||
|
||||
// Для веб-запросов возвращаем ошибку
|
||||
http.Error(w, "CSRF token required", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем сессионный ID
|
||||
sessionID := getSessionID(r)
|
||||
|
||||
// Проверяем CSRF токен
|
||||
csrfManager := GetCSRFManager()
|
||||
if csrfManager == nil || !csrfManager.ValidateToken(csrfToken, sessionID) {
|
||||
// Для API запросов возвращаем JSON ошибку
|
||||
if isAPIRequest(r) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
w.Write([]byte(`{"success": false, "error": "Invalid CSRF token"}`))
|
||||
return
|
||||
}
|
||||
|
||||
// Для веб-запросов возвращаем ошибку
|
||||
http.Error(w, "Invalid CSRF token", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// CSRF токен валиден, продолжаем
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// isAPIRequest проверяет, является ли запрос API запросом
|
||||
func isAPIRequest(r *http.Request) bool {
|
||||
path := r.URL.Path
|
||||
apiPrefix := makePath("/api")
|
||||
return strings.HasPrefix(path, apiPrefix)
|
||||
}
|
||||
|
||||
// RequireAuth обертка для requireAuth из auth.go
|
||||
func RequireAuth(next http.HandlerFunc) http.HandlerFunc {
|
||||
return requireAuth(next)
|
||||
}
|
||||
110
serve/prompts.go
110
serve/prompts.go
@@ -9,8 +9,10 @@ import (
|
||||
"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/direct-dev-ru/linux-command-gpt/validation"
|
||||
)
|
||||
|
||||
// VerbosePrompt структура для промптов подробности
|
||||
@@ -85,10 +87,20 @@ func handlePromptsPage(w http.ResponseWriter, r *http.Request) {
|
||||
Prompts []PromptWithDefault
|
||||
VerbosePrompts []VerbosePrompt
|
||||
Lang string
|
||||
MaxSystemPromptLength int
|
||||
MaxPromptNameLength int
|
||||
MaxPromptDescLength int
|
||||
BasePath string
|
||||
AppName string
|
||||
}{
|
||||
Prompts: promptsWithDefault,
|
||||
VerbosePrompts: verbosePrompts,
|
||||
Lang: lang,
|
||||
MaxSystemPromptLength: config.AppConfig.Validation.MaxSystemPromptLength,
|
||||
MaxPromptNameLength: config.AppConfig.Validation.MaxPromptNameLength,
|
||||
MaxPromptDescLength: config.AppConfig.Validation.MaxPromptDescLength,
|
||||
BasePath: getBasePath(),
|
||||
AppName: config.AppConfig.AppName,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
@@ -124,6 +136,20 @@ func handleAddPrompt(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Валидация длины полей
|
||||
if err := validation.ValidateSystemPrompt(promptData.Content); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := validation.ValidatePromptName(promptData.Name); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := validation.ValidatePromptDescription(promptData.Description); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Добавляем промпт
|
||||
if err := pm.AddPrompt(promptData.Name, promptData.Description, promptData.Content); err != nil {
|
||||
http.Error(w, fmt.Sprintf("Ошибка добавления промпта: %v", err), http.StatusInternalServerError)
|
||||
@@ -171,6 +197,20 @@ func handleEditPrompt(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Валидация длины полей
|
||||
if err := validation.ValidateSystemPrompt(promptData.Content); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := validation.ValidatePromptName(promptData.Name); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := validation.ValidatePromptDescription(promptData.Description); err != nil {
|
||||
http.Error(w, err.Error(), 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)
|
||||
@@ -181,6 +221,76 @@ func handleEditPrompt(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("Промпт успешно обновлен"))
|
||||
}
|
||||
|
||||
// handleEditVerbosePrompt обрабатывает редактирование промпта подробности
|
||||
func handleEditVerbosePrompt(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "PUT" {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем режим из URL
|
||||
mode := strings.TrimPrefix(r.URL.Path, "/prompts/edit-verbose/")
|
||||
|
||||
// Получаем домашнюю директорию пользователя
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
http.Error(w, "Ошибка получения домашней директории", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Создаем менеджер промптов
|
||||
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 := validation.ValidateSystemPrompt(promptData.Content); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := validation.ValidatePromptName(promptData.Name); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := validation.ValidatePromptDescription(promptData.Description); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Определяем ID по режиму
|
||||
var id int
|
||||
switch mode {
|
||||
case "v":
|
||||
id = 6
|
||||
case "vv":
|
||||
id = 7
|
||||
case "vvv":
|
||||
id = 8
|
||||
default:
|
||||
http.Error(w, "Неверный режим промпта", 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" {
|
||||
|
||||
183
serve/results.go
183
serve/results.go
@@ -8,18 +8,42 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||
"github.com/direct-dev-ru/linux-command-gpt/serve/templates"
|
||||
"github.com/russross/blackfriday/v2"
|
||||
)
|
||||
|
||||
// generateAbbreviation создает аббревиатуру из первых букв слов в названии приложения
|
||||
func generateAbbreviation(appName string) string {
|
||||
words := strings.Fields(appName)
|
||||
var abbreviation strings.Builder
|
||||
|
||||
for _, word := range words {
|
||||
if len(word) > 0 {
|
||||
// Берем первую букву слова, если это буква
|
||||
firstRune := []rune(word)[0]
|
||||
if unicode.IsLetter(firstRune) {
|
||||
abbreviation.WriteRune(unicode.ToUpper(firstRune))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := abbreviation.String()
|
||||
if result == "" {
|
||||
return "LCG" // Fallback если не удалось сгенерировать аббревиатуру
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// FileInfo содержит информацию о файле
|
||||
type FileInfo struct {
|
||||
Name string
|
||||
DisplayName string
|
||||
Size string
|
||||
ModTime string
|
||||
Preview string
|
||||
Preview template.HTML
|
||||
Content string // Полное содержимое для поиска
|
||||
}
|
||||
|
||||
@@ -55,10 +79,16 @@ func handleResultsPage(w http.ResponseWriter, r *http.Request) {
|
||||
Files []FileInfo
|
||||
TotalFiles int
|
||||
RecentFiles int
|
||||
BasePath string
|
||||
AppName string
|
||||
AppAbbreviation string
|
||||
}{
|
||||
Files: files,
|
||||
TotalFiles: len(files),
|
||||
RecentFiles: recentCount,
|
||||
BasePath: getBasePath(),
|
||||
AppName: config.AppConfig.AppName,
|
||||
AppAbbreviation: generateAbbreviation(config.AppConfig.AppName),
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
@@ -83,44 +113,15 @@ func getResultFiles() ([]FileInfo, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Читаем превью файла (первые 200 символов) и конвертируем Markdown
|
||||
// Читаем превью файла (первые 200 символов) как обычный текст
|
||||
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 />", "")
|
||||
|
||||
// Берем первые 200 символов как превью
|
||||
preview = string(content)
|
||||
// Очищаем от лишних пробелов и переносов
|
||||
preview = strings.ReplaceAll(preview, "\n", " ")
|
||||
preview = strings.ReplaceAll(preview, "\r", "")
|
||||
@@ -134,9 +135,10 @@ func getResultFiles() ([]FileInfo, error) {
|
||||
|
||||
files = append(files, FileInfo{
|
||||
Name: entry.Name(),
|
||||
DisplayName: formatFileDisplayName(entry.Name()),
|
||||
Size: formatFileSize(info.Size()),
|
||||
ModTime: info.ModTime().Format("02.01.2006 15:04"),
|
||||
Preview: preview,
|
||||
Preview: template.HTML(preview),
|
||||
Content: fullContent,
|
||||
})
|
||||
}
|
||||
@@ -167,36 +169,119 @@ func formatFileSize(size int64) string {
|
||||
return fmt.Sprintf("%.1f %cB", float64(size)/float64(div), "KMGTPE"[exp])
|
||||
}
|
||||
|
||||
// formatFileDisplayName преобразует имя файла вида
|
||||
// gpt_request_GigaChat-2-Max_2025-10-22_13-50-13.md
|
||||
// в "Gpt Request GigaChat 2 Max — 2025-10-22 13:50:13"
|
||||
func formatFileDisplayName(filename string) string {
|
||||
name := strings.TrimSuffix(filename, ".md")
|
||||
// Разделим на части по '_'
|
||||
parts := strings.Split(name, "_")
|
||||
if len(parts) == 0 {
|
||||
return filename
|
||||
}
|
||||
|
||||
// Первая часть может быть префиксом gpt/request — заменим '_' на пробел и приведем регистр
|
||||
var words []string
|
||||
for _, p := range parts {
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
// Заменяем '-' на пробел в словах модели/текста
|
||||
p = strings.ReplaceAll(p, "-", " ")
|
||||
// Разбиваем по пробелам и капитализуем каждое слово
|
||||
for _, w := range strings.Fields(p) {
|
||||
if w == "" {
|
||||
continue
|
||||
}
|
||||
r := []rune(w)
|
||||
r[0] = unicode.ToUpper(r[0])
|
||||
words = append(words, string(r))
|
||||
}
|
||||
}
|
||||
|
||||
// Попробуем распознать хвост как дату и время
|
||||
// Ищем шаблон YYYY-MM-DD_HH-MM-SS в исходном имени
|
||||
var pretty string
|
||||
// ожидаем последние две части — дата и время
|
||||
if len(parts) >= 3 {
|
||||
datePart := parts[len(parts)-2]
|
||||
timePart := parts[len(parts)-1]
|
||||
// заменить '-' в времени на ':'
|
||||
timePretty := strings.ReplaceAll(timePart, "-", ":")
|
||||
if len(datePart) == 10 && len(timePart) == 8 { // примитивная проверка
|
||||
// Собираем текст до датных частей
|
||||
text := strings.Join(words[:len(words)-2], " ")
|
||||
pretty = strings.TrimSpace(text)
|
||||
if pretty != "" {
|
||||
pretty += " — " + datePart + " " + timePretty
|
||||
} else {
|
||||
pretty = datePart + " " + timePretty
|
||||
}
|
||||
return pretty
|
||||
}
|
||||
}
|
||||
|
||||
if len(words) > 0 {
|
||||
pretty = strings.Join(words, " ")
|
||||
return pretty
|
||||
}
|
||||
return filename
|
||||
}
|
||||
|
||||
// handleFileView обрабатывает просмотр конкретного файла
|
||||
func handleFileView(w http.ResponseWriter, r *http.Request) {
|
||||
filename := strings.TrimPrefix(r.URL.Path, "/file/")
|
||||
// Учитываем BasePath при извлечении имени файла
|
||||
basePath := config.AppConfig.Server.BasePath
|
||||
var filename string
|
||||
if basePath != "" && basePath != "/" {
|
||||
basePath = strings.TrimSuffix(basePath, "/")
|
||||
filename = strings.TrimPrefix(r.URL.Path, basePath+"/file/")
|
||||
} else {
|
||||
filename = strings.TrimPrefix(r.URL.Path, "/file/")
|
||||
}
|
||||
if filename == "" {
|
||||
http.NotFound(w, r)
|
||||
renderNotFound(w, "Файл не указан", getBasePath())
|
||||
return
|
||||
}
|
||||
|
||||
// Проверяем, что файл существует и находится в папке результатов
|
||||
filePath := filepath.Join(config.AppConfig.ResultFolder, filename)
|
||||
if !strings.HasPrefix(filePath, config.AppConfig.ResultFolder) {
|
||||
http.NotFound(w, r)
|
||||
renderNotFound(w, "Запрошенный файл недоступен", getBasePath())
|
||||
return
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
renderNotFound(w, "Файл не найден или был удален", getBasePath())
|
||||
return
|
||||
}
|
||||
|
||||
// Конвертируем Markdown в HTML
|
||||
htmlContent := blackfriday.Run(content)
|
||||
|
||||
// Создаем HTML страницу с красивым отображением
|
||||
htmlPage := fmt.Sprintf(templates.FileViewTemplate, filename, filename, string(htmlContent))
|
||||
// Создаем данные для шаблона
|
||||
data := struct {
|
||||
Filename string
|
||||
Content template.HTML
|
||||
BasePath string
|
||||
}{
|
||||
Filename: filename,
|
||||
Content: template.HTML(htmlContent),
|
||||
BasePath: getBasePath(),
|
||||
}
|
||||
|
||||
// Парсим и выполняем шаблон
|
||||
tmpl := templates.FileViewTemplate
|
||||
t, err := template.New("file_view").Parse(tmpl)
|
||||
if err != nil {
|
||||
http.Error(w, "Ошибка шаблона", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Устанавливаем заголовки для отображения HTML
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.Write([]byte(htmlPage))
|
||||
t.Execute(w, data)
|
||||
}
|
||||
|
||||
// handleDeleteFile обрабатывает удаление файла
|
||||
@@ -207,22 +292,30 @@ func handleDeleteFile(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
filename := strings.TrimPrefix(r.URL.Path, "/delete/")
|
||||
// Учитываем BasePath при извлечении имени файла
|
||||
basePath := config.AppConfig.Server.BasePath
|
||||
var filename string
|
||||
if basePath != "" && basePath != "/" {
|
||||
basePath = strings.TrimSuffix(basePath, "/")
|
||||
filename = strings.TrimPrefix(r.URL.Path, basePath+"/delete/")
|
||||
} else {
|
||||
filename = strings.TrimPrefix(r.URL.Path, "/delete/")
|
||||
}
|
||||
if filename == "" {
|
||||
http.NotFound(w, r)
|
||||
renderNotFound(w, "Файл не указан", getBasePath())
|
||||
return
|
||||
}
|
||||
|
||||
// Проверяем, что файл существует и находится в папке результатов
|
||||
filePath := filepath.Join(config.AppConfig.ResultFolder, filename)
|
||||
if !strings.HasPrefix(filePath, config.AppConfig.ResultFolder) {
|
||||
http.NotFound(w, r)
|
||||
renderNotFound(w, "Запрошенный файл недоступен", getBasePath())
|
||||
return
|
||||
}
|
||||
|
||||
// Проверяем, что файл существует
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
http.NotFound(w, r)
|
||||
renderNotFound(w, "Файл не найден или уже удален", getBasePath())
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
310
serve/serve.go
310
serve/serve.go
@@ -1,19 +1,126 @@
|
||||
package serve
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||
"github.com/direct-dev-ru/linux-command-gpt/serve/templates"
|
||||
"github.com/direct-dev-ru/linux-command-gpt/ssl"
|
||||
)
|
||||
|
||||
// StartResultServer запускает HTTP сервер для просмотра сохраненных результатов
|
||||
// makePath создает путь с учетом BasePath
|
||||
func makePath(path string) string {
|
||||
basePath := config.AppConfig.Server.BasePath
|
||||
if basePath == "" || basePath == "/" {
|
||||
return path
|
||||
}
|
||||
|
||||
// Убираем слэш в конце basePath если есть
|
||||
basePath = strings.TrimSuffix(basePath, "/")
|
||||
|
||||
// Убираем слэш в начале path если есть
|
||||
path = strings.TrimPrefix(path, "/")
|
||||
|
||||
// Если path пустой, возвращаем basePath с слэшем в конце
|
||||
if path == "" {
|
||||
return basePath + "/"
|
||||
}
|
||||
|
||||
return basePath + "/" + path
|
||||
}
|
||||
|
||||
// getBasePath возвращает BasePath для использования в шаблонах
|
||||
func getBasePath() string {
|
||||
basePath := config.AppConfig.Server.BasePath
|
||||
if basePath == "" || basePath == "/" {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSuffix(basePath, "/")
|
||||
}
|
||||
|
||||
// StartResultServer запускает HTTP/HTTPS сервер для просмотра сохраненных результатов
|
||||
func StartResultServer(host, port string) error {
|
||||
// Регистрируем все маршруты
|
||||
registerRoutes()
|
||||
// Инициализируем CSRF менеджер
|
||||
if err := InitCSRFManager(); err != nil {
|
||||
return fmt.Errorf("failed to initialize CSRF manager: %v", err)
|
||||
}
|
||||
|
||||
// Гарантируем наличие папки результатов и файла истории
|
||||
if _, err := os.Stat(config.AppConfig.ResultFolder); os.IsNotExist(err) {
|
||||
if mkErr := os.MkdirAll(config.AppConfig.ResultFolder, 0755); mkErr != nil {
|
||||
return fmt.Errorf("failed to create results folder: %v", mkErr)
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat(config.AppConfig.ResultHistory); os.IsNotExist(err) {
|
||||
if writeErr := Write(config.AppConfig.ResultHistory, []HistoryEntry{}); writeErr != nil {
|
||||
return fmt.Errorf("failed to create history file: %v", writeErr)
|
||||
}
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf("%s:%s", host, port)
|
||||
fmt.Printf("Сервер запущен на http://%s\n", addr)
|
||||
|
||||
// Проверяем, нужно ли использовать HTTPS
|
||||
useHTTPS := ssl.ShouldUseHTTPS(host)
|
||||
|
||||
if useHTTPS {
|
||||
// Регистрируем HTTPS маршруты (включая редирект)
|
||||
registerHTTPSRoutes()
|
||||
|
||||
// Создаем директорию для SSL сертификатов
|
||||
sslDir := fmt.Sprintf("%s/server/ssl", config.AppConfig.Server.ConfigFolder)
|
||||
if err := os.MkdirAll(sslDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create SSL directory: %v", err)
|
||||
}
|
||||
|
||||
// Загружаем или генерируем SSL сертификат
|
||||
cert, err := ssl.LoadOrGenerateCert(host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load/generate SSL certificate: %v", err)
|
||||
}
|
||||
|
||||
// Настраиваем TLS
|
||||
tlsConfig := &tls.Config{
|
||||
Certificates: []tls.Certificate{*cert},
|
||||
MinVersion: tls.VersionTLS12,
|
||||
MaxVersion: tls.VersionTLS13,
|
||||
// Отключаем проверку клиентских сертификатов
|
||||
ClientAuth: tls.NoClientCert,
|
||||
// Добавляем логирование для отладки
|
||||
GetCertificate: func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
if config.AppConfig.MainFlags.Debug {
|
||||
fmt.Printf("🔍 TLS запрос от %s (SNI: %s)\n", clientHello.Conn.RemoteAddr(), clientHello.ServerName)
|
||||
}
|
||||
return cert, nil
|
||||
},
|
||||
}
|
||||
|
||||
// Создаем HTTPS сервер
|
||||
server := &http.Server{
|
||||
Addr: addr,
|
||||
TLSConfig: tlsConfig,
|
||||
}
|
||||
|
||||
fmt.Printf("🔒 Сервер запущен на https://%s (SSL включен)\n", addr)
|
||||
fmt.Println("Нажмите Ctrl+C для остановки")
|
||||
|
||||
// Тестовое логирование для проверки debug флага
|
||||
if config.AppConfig.MainFlags.Debug {
|
||||
fmt.Printf("🔍 DEBUG РЕЖИМ ВКЛЮЧЕН - веб-операции будут логироваться\n")
|
||||
} else {
|
||||
fmt.Printf("🔍 DEBUG РЕЖИМ ОТКЛЮЧЕН - веб-операции не будут логироваться\n")
|
||||
}
|
||||
|
||||
return server.ListenAndServeTLS("", "")
|
||||
} else {
|
||||
// Регистрируем обычные маршруты для HTTP
|
||||
registerRoutes()
|
||||
|
||||
fmt.Printf("🌐 Сервер запущен на http://%s (HTTP режим)\n", addr)
|
||||
fmt.Println("Нажмите Ctrl+C для остановки")
|
||||
|
||||
// Тестовое логирование для проверки debug флага
|
||||
@@ -24,36 +131,193 @@ func StartResultServer(host, port string) error {
|
||||
}
|
||||
|
||||
return http.ListenAndServe(addr, nil)
|
||||
}
|
||||
}
|
||||
|
||||
// handleHTTPSRedirect обрабатывает редирект с HTTP на HTTPS
|
||||
func handleHTTPSRedirect(w http.ResponseWriter, r *http.Request) {
|
||||
// Определяем протокол и хост
|
||||
host := r.Host
|
||||
if host == "" {
|
||||
host = r.Header.Get("Host")
|
||||
}
|
||||
|
||||
// Редиректим на HTTPS
|
||||
httpsURL := fmt.Sprintf("https://%s%s", host, r.RequestURI)
|
||||
http.Redirect(w, r, httpsURL, http.StatusMovedPermanently)
|
||||
}
|
||||
|
||||
// registerHTTPSRoutes регистрирует маршруты для HTTPS сервера
|
||||
func registerHTTPSRoutes() {
|
||||
// Регистрируем все маршруты кроме главной страницы
|
||||
registerRoutesExceptHome()
|
||||
|
||||
// Регистрируем главную страницу (строго по BasePath) с проверкой HTTPS
|
||||
http.HandleFunc(makePath("/"), func(w http.ResponseWriter, r *http.Request) {
|
||||
// Проверяем, пришел ли запрос по HTTP (не HTTPS)
|
||||
if r.TLS == nil {
|
||||
handleHTTPSRedirect(w, r)
|
||||
return
|
||||
}
|
||||
// Обрабатываем только точные пути: BasePath или BasePath/
|
||||
bp := getBasePath()
|
||||
p := r.URL.Path
|
||||
if (bp == "" && (p == "/" || p == "")) || (bp != "" && (p == bp || p == bp+"/")) {
|
||||
AuthMiddleware(handleResultsPage)(w, r)
|
||||
return
|
||||
}
|
||||
renderNotFound(w, "Страница не найдена", bp)
|
||||
})
|
||||
|
||||
// Регистрируем главную страницу без слэша в конце для BasePath
|
||||
basePath := config.AppConfig.Server.BasePath
|
||||
if basePath != "" && basePath != "/" {
|
||||
basePath = strings.TrimSuffix(basePath, "/")
|
||||
http.HandleFunc(basePath, func(w http.ResponseWriter, r *http.Request) {
|
||||
// Проверяем, пришел ли запрос по HTTP (не HTTPS)
|
||||
if r.TLS == nil {
|
||||
handleHTTPSRedirect(w, r)
|
||||
return
|
||||
}
|
||||
// Если уже HTTPS, обрабатываем как обычно
|
||||
AuthMiddleware(handleResultsPage)(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// Catch-all 404 для любых незарегистрированных путей (только когда BasePath задан)
|
||||
if getBasePath() != "" {
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
renderNotFound(w, "Страница не найдена", getBasePath())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// registerRoutesExceptHome регистрирует все маршруты кроме главной страницы
|
||||
func registerRoutesExceptHome() {
|
||||
// Страница входа (без аутентификации)
|
||||
http.HandleFunc(makePath("/login"), handleLoginPage)
|
||||
|
||||
// API для аутентификации (без аутентификации)
|
||||
http.HandleFunc(makePath("/api/login"), handleLogin)
|
||||
http.HandleFunc(makePath("/api/logout"), handleLogout)
|
||||
http.HandleFunc(makePath("/api/validate-token"), handleValidateToken)
|
||||
|
||||
// Файлы
|
||||
http.HandleFunc(makePath("/file/"), AuthMiddleware(handleFileView))
|
||||
http.HandleFunc(makePath("/delete/"), AuthMiddleware(handleDeleteFile))
|
||||
|
||||
// История запросов
|
||||
http.HandleFunc(makePath("/history"), AuthMiddleware(handleHistoryPage))
|
||||
http.HandleFunc(makePath("/history/view/"), AuthMiddleware(handleHistoryView))
|
||||
http.HandleFunc(makePath("/history/delete/"), AuthMiddleware(handleDeleteHistoryEntry))
|
||||
http.HandleFunc(makePath("/history/clear"), AuthMiddleware(handleClearHistory))
|
||||
|
||||
// Управление промптами
|
||||
http.HandleFunc(makePath("/prompts"), AuthMiddleware(handlePromptsPage))
|
||||
http.HandleFunc(makePath("/prompts/add"), AuthMiddleware(handleAddPrompt))
|
||||
http.HandleFunc(makePath("/prompts/edit/"), AuthMiddleware(handleEditPrompt))
|
||||
http.HandleFunc(makePath("/prompts/edit-verbose/"), AuthMiddleware(handleEditVerbosePrompt))
|
||||
http.HandleFunc(makePath("/prompts/delete/"), AuthMiddleware(handleDeletePrompt))
|
||||
http.HandleFunc(makePath("/prompts/restore/"), AuthMiddleware(handleRestorePrompt))
|
||||
http.HandleFunc(makePath("/prompts/restore-verbose/"), AuthMiddleware(handleRestoreVerbosePrompt))
|
||||
http.HandleFunc(makePath("/prompts/save-lang"), AuthMiddleware(handleSaveLang))
|
||||
|
||||
// Веб-страница для выполнения запросов
|
||||
http.HandleFunc(makePath("/run"), AuthMiddleware(CSRFMiddleware(handleExecutePage)))
|
||||
|
||||
// API для выполнения запросов
|
||||
http.HandleFunc(makePath("/api/execute"), AuthMiddleware(CSRFMiddleware(handleExecute)))
|
||||
// API для сохранения результатов и истории
|
||||
http.HandleFunc(makePath("/api/save-result"), AuthMiddleware(CSRFMiddleware(handleSaveResult)))
|
||||
http.HandleFunc(makePath("/api/add-to-history"), AuthMiddleware(CSRFMiddleware(handleAddToHistory)))
|
||||
|
||||
// Catch-all 404 для любых незарегистрированных путей (только когда BasePath задан)
|
||||
if getBasePath() != "" {
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
renderNotFound(w, "Страница не найдена", getBasePath())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// registerRoutes регистрирует все маршруты сервера
|
||||
func registerRoutes() {
|
||||
// Главная страница и файлы
|
||||
http.HandleFunc("/", handleResultsPage)
|
||||
http.HandleFunc("/file/", handleFileView)
|
||||
http.HandleFunc("/delete/", handleDeleteFile)
|
||||
// Страница входа (без аутентификации)
|
||||
http.HandleFunc(makePath("/login"), handleLoginPage)
|
||||
|
||||
// API для аутентификации (без аутентификации)
|
||||
http.HandleFunc(makePath("/api/login"), handleLogin)
|
||||
http.HandleFunc(makePath("/api/logout"), handleLogout)
|
||||
http.HandleFunc(makePath("/api/validate-token"), handleValidateToken)
|
||||
|
||||
// Главная страница (строго по BasePath) и файлы
|
||||
http.HandleFunc(makePath("/"), func(w http.ResponseWriter, r *http.Request) {
|
||||
// Обрабатываем только точные пути: BasePath или BasePath/
|
||||
bp := getBasePath()
|
||||
p := r.URL.Path
|
||||
if (bp == "" && (p == "/" || p == "")) || (bp != "" && (p == bp || p == bp+"/")) {
|
||||
AuthMiddleware(handleResultsPage)(w, r)
|
||||
return
|
||||
}
|
||||
renderNotFound(w, "Страница не найдена", bp)
|
||||
})
|
||||
http.HandleFunc(makePath("/file/"), AuthMiddleware(handleFileView))
|
||||
http.HandleFunc(makePath("/delete/"), AuthMiddleware(handleDeleteFile))
|
||||
|
||||
// История запросов
|
||||
http.HandleFunc("/history", handleHistoryPage)
|
||||
http.HandleFunc("/history/view/", handleHistoryView)
|
||||
http.HandleFunc("/history/delete/", handleDeleteHistoryEntry)
|
||||
http.HandleFunc("/history/clear", handleClearHistory)
|
||||
http.HandleFunc(makePath("/history"), AuthMiddleware(handleHistoryPage))
|
||||
http.HandleFunc(makePath("/history/view/"), AuthMiddleware(handleHistoryView))
|
||||
http.HandleFunc(makePath("/history/delete/"), AuthMiddleware(handleDeleteHistoryEntry))
|
||||
http.HandleFunc(makePath("/history/clear"), AuthMiddleware(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(makePath("/prompts"), AuthMiddleware(handlePromptsPage))
|
||||
http.HandleFunc(makePath("/prompts/add"), AuthMiddleware(handleAddPrompt))
|
||||
http.HandleFunc(makePath("/prompts/edit/"), AuthMiddleware(handleEditPrompt))
|
||||
http.HandleFunc(makePath("/prompts/edit-verbose/"), AuthMiddleware(handleEditVerbosePrompt))
|
||||
http.HandleFunc(makePath("/prompts/delete/"), AuthMiddleware(handleDeletePrompt))
|
||||
http.HandleFunc(makePath("/prompts/restore/"), AuthMiddleware(handleRestorePrompt))
|
||||
http.HandleFunc(makePath("/prompts/restore-verbose/"), AuthMiddleware(handleRestoreVerbosePrompt))
|
||||
http.HandleFunc(makePath("/prompts/save-lang"), AuthMiddleware(handleSaveLang))
|
||||
|
||||
// Веб-страница для выполнения запросов
|
||||
http.HandleFunc("/run", handleExecutePage)
|
||||
http.HandleFunc(makePath("/run"), AuthMiddleware(CSRFMiddleware(handleExecutePage)))
|
||||
|
||||
// API для выполнения запросов
|
||||
http.HandleFunc("/api/execute", handleExecute)
|
||||
http.HandleFunc(makePath("/api/execute"), AuthMiddleware(CSRFMiddleware(handleExecute)))
|
||||
// API для сохранения результатов и истории
|
||||
http.HandleFunc("/api/save-result", handleSaveResult)
|
||||
http.HandleFunc("/api/add-to-history", handleAddToHistory)
|
||||
http.HandleFunc(makePath("/api/save-result"), AuthMiddleware(CSRFMiddleware(handleSaveResult)))
|
||||
http.HandleFunc(makePath("/api/add-to-history"), AuthMiddleware(CSRFMiddleware(handleAddToHistory)))
|
||||
|
||||
// Регистрируем главную страницу без слэша в конце для BasePath
|
||||
basePath := config.AppConfig.Server.BasePath
|
||||
if basePath != "" && basePath != "/" {
|
||||
basePath = strings.TrimSuffix(basePath, "/")
|
||||
http.HandleFunc(basePath, AuthMiddleware(handleResultsPage))
|
||||
}
|
||||
|
||||
// Catch-all 404 для любых незарегистрированных путей (только когда BasePath задан)
|
||||
if getBasePath() != "" {
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
renderNotFound(w, "Страница не найдена", getBasePath())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// renderNotFound рендерит кастомную страницу 404
|
||||
func renderNotFound(w http.ResponseWriter, message, basePath string) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
data := struct {
|
||||
Message string
|
||||
BasePath string
|
||||
}{
|
||||
Message: message,
|
||||
BasePath: basePath,
|
||||
}
|
||||
tmpl, err := template.New("not_found").Parse(templates.NotFoundTemplate)
|
||||
if err != nil {
|
||||
http.Error(w, "404 Not Found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
_ = tmpl.Execute(w, data)
|
||||
}
|
||||
|
||||
@@ -79,6 +79,14 @@ var ExecutePageCSSTemplate = template.Must(template.New("execute_css").Parse(`
|
||||
opacity: 0.9;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.config-info {
|
||||
margin: 5px 0 0 0 !important;
|
||||
opacity: 0.7 !important;
|
||||
font-size: 0.9em !important;
|
||||
font-style: italic;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
.content {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ var ExecutePageTemplate = template.Must(template.New("execute").Parse(`<!DOCTYPE
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{.Title}} - Linux Command GPT</title>
|
||||
<title>{{.Title}} - {{.AppName}}</title>
|
||||
<style>
|
||||
{{template "execute_css" .}}
|
||||
</style>
|
||||
@@ -17,16 +17,19 @@ var ExecutePageTemplate = template.Must(template.New("execute").Parse(`<!DOCTYPE
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>{{.Header}}</h1>
|
||||
<p>Выполнение запросов к Linux Command GPT через веб-интерфейс</p>
|
||||
<p>Выполнение запросов к {{.AppName}} через веб-интерфейс</p>
|
||||
<p class="config-info">({{.ProviderType}} • {{.Model}} • {{.Host}})</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>
|
||||
<a href="{{.BasePath}}/" class="nav-btn">🏠 Главная</a>
|
||||
<a href="{{.BasePath}}/history" class="nav-btn">📝 История</a>
|
||||
<a href="{{.BasePath}}/prompts" class="nav-btn">⚙️ Промпты</a>
|
||||
</div>
|
||||
|
||||
<form method="POST" id="executeForm">
|
||||
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
||||
|
||||
<div class="form-section">
|
||||
<div class="form-group">
|
||||
<label for="system_id">🤖 Системный промпт:</label>
|
||||
|
||||
@@ -11,6 +11,15 @@ var ExecutePageScriptsTemplate = template.Must(template.New("execute_scripts").P
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Валидация длины полей
|
||||
const prompt = document.getElementById('prompt').value;
|
||||
const maxUserMessageLength = {{.MaxUserMessageLength}};
|
||||
if (prompt.length > maxUserMessageLength) {
|
||||
alert('Пользовательское сообщение слишком длинное: максимум ' + maxUserMessageLength + ' символов');
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
this.dataset.submitting = 'true';
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
const loading = document.getElementById('loading');
|
||||
@@ -80,6 +89,7 @@ var ExecutePageScriptsTemplate = template.Must(template.New("execute_scripts").P
|
||||
function saveResult() {
|
||||
const resultDataField = document.getElementById('resultData');
|
||||
const prompt = document.getElementById('prompt').value;
|
||||
const csrfToken = document.querySelector('input[name="csrf_token"]').value;
|
||||
|
||||
if (!resultDataField.value || !prompt.trim()) {
|
||||
alert('Нет данных для сохранения');
|
||||
@@ -95,10 +105,11 @@ var ExecutePageScriptsTemplate = template.Must(template.New("execute_scripts").P
|
||||
model: resultData.model || 'Unknown'
|
||||
};
|
||||
|
||||
fetch('/api/save-result', {
|
||||
fetch('{{.BasePath}}/api/save-result', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': csrfToken,
|
||||
},
|
||||
body: JSON.stringify(requestData)
|
||||
})
|
||||
@@ -125,6 +136,7 @@ var ExecutePageScriptsTemplate = template.Must(template.New("execute_scripts").P
|
||||
const resultDataField = document.getElementById('resultData');
|
||||
const prompt = document.getElementById('prompt').value;
|
||||
const systemId = document.getElementById('system_id').value;
|
||||
const csrfToken = document.querySelector('input[name="csrf_token"]').value;
|
||||
|
||||
if (!resultDataField.value || !prompt.trim()) {
|
||||
alert('Нет данных для сохранения в историю');
|
||||
@@ -143,10 +155,11 @@ var ExecutePageScriptsTemplate = template.Must(template.New("execute_scripts").P
|
||||
system: systemName
|
||||
};
|
||||
|
||||
fetch('/api/add-to-history', {
|
||||
fetch('{{.BasePath}}/api/add-to-history', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': csrfToken,
|
||||
},
|
||||
body: JSON.stringify(requestData)
|
||||
})
|
||||
|
||||
@@ -7,13 +7,13 @@ const FileViewTemplate = `
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>%s - LCG Results</title>
|
||||
<title>{{.Filename}} - 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%%);
|
||||
background: linear-gradient(135deg, #56ab2f 0%, #a8e6cf 100%);
|
||||
min-height: 100vh;
|
||||
}
|
||||
.container {
|
||||
@@ -25,7 +25,7 @@ const FileViewTemplate = `
|
||||
overflow: hidden;
|
||||
}
|
||||
.header {
|
||||
background: linear-gradient(135deg, #2d5016 0%%, #4a7c59 100%%);
|
||||
background: linear-gradient(135deg, #2d5016 0%, #4a7c59 100%);
|
||||
color: white;
|
||||
padding: 20px 30px;
|
||||
display: flex;
|
||||
@@ -125,11 +125,11 @@ const FileViewTemplate = `
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>📄 %s</h1>
|
||||
<a href="/" class="back-btn">← Назад к списку</a>
|
||||
<h1>📄 {{.Filename}}</h1>
|
||||
<a href="{{.BasePath}}/" class="back-btn">← Назад к списку</a>
|
||||
</div>
|
||||
<div class="content">
|
||||
%s
|
||||
{{.Content}}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
@@ -111,19 +111,26 @@ const HistoryPageTemplate = `
|
||||
font-size: 0.9em;
|
||||
color: #2d5016;
|
||||
border-left: 3px solid #2d5016;
|
||||
max-height: 72px; /* ~4 строки */
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 4;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
.delete-btn {
|
||||
background: #e74c3c;
|
||||
color: white;
|
||||
background: transparent;
|
||||
color: #ef9a9a; /* бледно-красный */
|
||||
border: none;
|
||||
padding: 6px 12px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.8em;
|
||||
transition: background 0.3s ease;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
transition: color 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
.delete-btn:hover {
|
||||
background: #c0392b;
|
||||
color: rgb(171, 27, 24); /* ярче при ховере */
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
@@ -143,6 +150,7 @@ const HistoryPageTemplate = `
|
||||
.history-header { flex-direction: column; align-items: flex-start; gap: 8px; }
|
||||
.history-item { padding: 15px; }
|
||||
.history-response { font-size: 0.85em; }
|
||||
.search-container input { font-size: 16px; width: 96% !important; }
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
@@ -154,13 +162,13 @@ const HistoryPageTemplate = `
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>📝 История запросов</h1>
|
||||
<p>Управление историей запросов Linux Command GPT</p>
|
||||
<p>Управление историей запросов {{.AppName}}</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>
|
||||
<a href="{{.BasePath}}/" class="nav-btn">🏠 Главная</a>
|
||||
<a href="{{.BasePath}}/run" class="nav-btn">🚀 Выполнение</a>
|
||||
<a href="{{.BasePath}}/prompts" class="nav-btn">⚙️ Промпты</a>
|
||||
<button class="nav-btn clear-btn" onclick="clearHistory()">🗑️ Очистить всю историю</button>
|
||||
</div>
|
||||
|
||||
@@ -179,7 +187,7 @@ const HistoryPageTemplate = `
|
||||
<span class="history-index">#{{.Index}}</span>
|
||||
<span class="history-timestamp">{{.Timestamp}}</span>
|
||||
</div>
|
||||
<button class="delete-btn" onclick="event.stopPropagation(); deleteHistoryEntry({{.Index}})">🗑️ Удалить</button>
|
||||
<button class="delete-btn" onclick="event.stopPropagation(); deleteHistoryEntry({{.Index}})">✖</button>
|
||||
</div>
|
||||
<div class="history-command">{{.Command}}</div>
|
||||
<div class="history-response">{{.Response}}</div>
|
||||
@@ -196,12 +204,12 @@ const HistoryPageTemplate = `
|
||||
|
||||
<script>
|
||||
function viewHistoryEntry(index) {
|
||||
window.location.href = '/history/view/' + index;
|
||||
window.location.href = '{{.BasePath}}/history/view/' + index;
|
||||
}
|
||||
|
||||
function deleteHistoryEntry(index) {
|
||||
if (confirm('Вы уверены, что хотите удалить запись #' + index + '?')) {
|
||||
fetch('/history/delete/' + index, {
|
||||
fetch('{{.BasePath}}/history/delete/' + index, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
.then(response => {
|
||||
@@ -220,7 +228,7 @@ const HistoryPageTemplate = `
|
||||
|
||||
function clearHistory() {
|
||||
if (confirm('Вы уверены, что хотите очистить всю историю?\\n\\nЭто действие нельзя отменить.')) {
|
||||
fetch('/history/clear', {
|
||||
fetch('{{.BasePath}}/history/clear', {
|
||||
method: 'DELETE'
|
||||
})
|
||||
.then(response => {
|
||||
|
||||
@@ -7,13 +7,13 @@ const HistoryViewTemplate = `
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Запись #%d - LCG History</title>
|
||||
<title>Запись #{{.Index}} - 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%%);
|
||||
background: linear-gradient(135deg, #56ab2f 0%, #a8e6cf 100%);
|
||||
min-height: 100vh;
|
||||
}
|
||||
.container {
|
||||
@@ -25,7 +25,7 @@ const HistoryViewTemplate = `
|
||||
overflow: hidden;
|
||||
}
|
||||
.header {
|
||||
background: linear-gradient(135deg, #2d5016 0%%, #4a7c59 100%%);
|
||||
background: linear-gradient(135deg, #2d5016 0%, #4a7c59 100%);
|
||||
color: white;
|
||||
padding: 20px 30px;
|
||||
display: flex;
|
||||
@@ -212,7 +212,7 @@ const HistoryViewTemplate = `
|
||||
.back-btn { padding: 6px 12px; font-size: 0.9em; }
|
||||
.content { padding: 20px; }
|
||||
.actions { flex-direction: column; }
|
||||
.action-btn { width: 100%; text-align: center; }
|
||||
.action-btn { text-align: center; }
|
||||
.history-response-content { font-size: 0.9em; }
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
@@ -223,34 +223,34 @@ const HistoryViewTemplate = `
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>📝 Запись #%d</h1>
|
||||
<a href="/history" class="back-btn">← Назад к истории</a>
|
||||
<h1>📝 Запись #{{.Index}}</h1>
|
||||
<a href="{{.BasePath}}/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
|
||||
<span class="history-meta-label">📅 Время:</span> {{.Timestamp}}
|
||||
</div>
|
||||
<div class="history-meta-item">
|
||||
<span class="history-meta-label">🔢 Индекс:</span> #%d
|
||||
<span class="history-meta-label">🔢 Индекс:</span> #{{.Index}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="history-command">
|
||||
<h3>💬 Запрос пользователя:</h3>
|
||||
<div class="history-command-text">%s</div>
|
||||
<div class="history-command-text">{{.Command}}</div>
|
||||
</div>
|
||||
|
||||
<div class="history-response">
|
||||
<h3>🤖 Ответ Модели:</h3>
|
||||
<div class="history-response-content">%s</div>
|
||||
<div class="history-response-content">{{.Response}}</div>
|
||||
</div>
|
||||
|
||||
%s
|
||||
{{.ExplanationHTML}}
|
||||
|
||||
<div class="actions">
|
||||
<a href="/history" class="action-btn">📝 К истории</a>
|
||||
<button class="action-btn delete-btn" onclick="deleteHistoryEntry(%d)">🗑️ Удалить запись</button>
|
||||
<a href="{{.BasePath}}/history" class="action-btn">📝 К истории</a>
|
||||
<button class="action-btn delete-btn" onclick="deleteHistoryEntry({{.Index}})">🗑️ Удалить запись</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -258,12 +258,12 @@ const HistoryViewTemplate = `
|
||||
<script>
|
||||
function deleteHistoryEntry(index) {
|
||||
if (confirm('Вы уверены, что хотите удалить запись #' + index + '?')) {
|
||||
fetch('/history/delete/' + index, {
|
||||
fetch('{{.BasePath}}/history/delete/' + index, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
window.location.href = '/history';
|
||||
window.location.href = '{{.BasePath}}/history';
|
||||
} else {
|
||||
alert('Ошибка при удалении записи');
|
||||
}
|
||||
|
||||
323
serve/templates/login.go
Normal file
323
serve/templates/login.go
Normal file
@@ -0,0 +1,323 @@
|
||||
package templates
|
||||
|
||||
// LoginPageTemplate шаблон страницы авторизации
|
||||
const LoginPageTemplate = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{.Title}}</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: linear-gradient(-45deg, #667eea, #764ba2, #f093fb, #f5576c, #4facfe, #00f2fe);
|
||||
background-size: 400% 400%;
|
||||
animation: gradientShift 15s ease infinite;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@keyframes gradientShift {
|
||||
0% { background-position: 0% 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
100% { background-position: 0% 50%; }
|
||||
}
|
||||
|
||||
/* Плавающие элементы */
|
||||
.floating-elements {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.floating-element {
|
||||
position: absolute;
|
||||
opacity: 0.1;
|
||||
animation: float 20s infinite linear;
|
||||
}
|
||||
|
||||
.floating-element:nth-child(1) { left: 10%; animation-delay: 0s; animation-duration: 25s; }
|
||||
.floating-element:nth-child(2) { left: 20%; animation-delay: 2s; animation-duration: 30s; }
|
||||
.floating-element:nth-child(3) { left: 30%; animation-delay: 4s; animation-duration: 20s; }
|
||||
.floating-element:nth-child(4) { left: 40%; animation-delay: 6s; animation-duration: 35s; }
|
||||
.floating-element:nth-child(5) { left: 50%; animation-delay: 8s; animation-duration: 28s; }
|
||||
.floating-element:nth-child(6) { left: 60%; animation-delay: 10s; animation-duration: 22s; }
|
||||
.floating-element:nth-child(7) { left: 70%; animation-delay: 12s; animation-duration: 32s; }
|
||||
.floating-element:nth-child(8) { left: 80%; animation-delay: 14s; animation-duration: 26s; }
|
||||
.floating-element:nth-child(9) { left: 90%; animation-delay: 16s; animation-duration: 24s; }
|
||||
|
||||
@keyframes float {
|
||||
0% { transform: translateY(100vh) rotate(0deg); opacity: 0; }
|
||||
10% { opacity: 0.1; }
|
||||
90% { opacity: 0.1; }
|
||||
100% { transform: translateY(-100px) rotate(360deg); opacity: 0; }
|
||||
}
|
||||
|
||||
.lock-icon {
|
||||
font-size: 2rem;
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.key-icon {
|
||||
font-size: 1.5rem;
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.shield-icon {
|
||||
font-size: 1.8rem;
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.star-icon {
|
||||
font-size: 1.2rem;
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.login-container {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
padding: 2rem;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.2);
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.login-header h1 {
|
||||
color: #333;
|
||||
font-size: 1.8rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.login-header p {
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 2px solid #e1e5e9;
|
||||
border-radius: 10px;
|
||||
font-size: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.2);
|
||||
background: rgba(255, 255, 255, 1);
|
||||
}
|
||||
|
||||
.login-button {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-button::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||
transition: left 0.5s;
|
||||
}
|
||||
|
||||
.login-button:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 10px 25px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.login-button:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.login-button:active {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-top: 1rem;
|
||||
padding: 0.75rem;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.message.success {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.message.error {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: none;
|
||||
text-align: center;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
border: 2px solid #f3f3f3;
|
||||
border-top: 2px solid #667eea;
|
||||
border-radius: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Плавающие элементы фона -->
|
||||
<div class="floating-elements">
|
||||
<div class="floating-element lock-icon">🔒</div>
|
||||
<div class="floating-element key-icon">🔑</div>
|
||||
<div class="floating-element shield-icon">🛡️</div>
|
||||
<div class="floating-element star-icon">⭐</div>
|
||||
<div class="floating-element lock-icon">🔐</div>
|
||||
<div class="floating-element key-icon">🗝️</div>
|
||||
<div class="floating-element shield-icon">🔒</div>
|
||||
<div class="floating-element star-icon">✨</div>
|
||||
<div class="floating-element lock-icon">🔒</div>
|
||||
</div>
|
||||
|
||||
<div class="login-container">
|
||||
<div class="login-header">
|
||||
<h1>🔐 Авторизация</h1>
|
||||
<p>Войдите в систему для доступа к LCG</p>
|
||||
</div>
|
||||
|
||||
<form id="loginForm">
|
||||
<input type="hidden" id="csrf_token" name="csrf_token" value="{{.CSRFToken}}">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="username">Имя пользователя:</label>
|
||||
<input type="text" id="username" name="username" required placeholder="Введите имя пользователя">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Пароль:</label>
|
||||
<input type="password" id="password" name="password" required placeholder="Введите пароль">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="login-button">Войти</button>
|
||||
</form>
|
||||
|
||||
<div class="loading" id="loading">
|
||||
<div class="spinner"></div>
|
||||
<p>Проверка авторизации...</p>
|
||||
</div>
|
||||
|
||||
<div id="message"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('loginForm').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const form = e.target;
|
||||
const formData = new FormData(form);
|
||||
const username = formData.get('username');
|
||||
const password = formData.get('password');
|
||||
|
||||
// Показываем загрузку
|
||||
document.getElementById('loading').style.display = 'block';
|
||||
document.getElementById('message').innerHTML = '';
|
||||
|
||||
try {
|
||||
const csrfToken = document.getElementById('csrf_token').value;
|
||||
const response = await fetch('{{.BasePath}}/api/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': csrfToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: username,
|
||||
password: password,
|
||||
csrf_token: csrfToken
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
// Успешная авторизация, перенаправляем на главную страницу
|
||||
window.location.href = '{{.BasePath}}/';
|
||||
} else {
|
||||
// Ошибка авторизации
|
||||
showMessage(data.error || 'Ошибка авторизации', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showMessage('Ошибка соединения с сервером', 'error');
|
||||
} finally {
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
function showMessage(text, type) {
|
||||
const messageDiv = document.getElementById('message');
|
||||
messageDiv.innerHTML = '<div class="message ' + type + '">' + text + '</div>';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
||||
147
serve/templates/not_found.go
Normal file
147
serve/templates/not_found.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package templates
|
||||
|
||||
// NotFoundTemplate современная страница 404
|
||||
const NotFoundTemplate = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Страница не найдена — 404</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #1a0b0b; /* глубокий темно-красный фон */
|
||||
--bg2: #2a0f0f; /* второй оттенок фона */
|
||||
--fg: #ffeaea; /* светлый текст с красным оттенком */
|
||||
--accent: #ff3b30; /* основной красный (iOS red) */
|
||||
--accent2: #ff6f61; /* дополнительный коралловый */
|
||||
--accentGlow: rgba(255,59,48,0.35);
|
||||
--accentGlow2: rgba(255,111,97,0.30);
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background:
|
||||
radial-gradient(1200px 600px at 10% 10%, rgba(255,59,48,0.12), transparent),
|
||||
radial-gradient(1200px 600px at 90% 90%, rgba(255,111,97,0.12), transparent),
|
||||
linear-gradient(135deg, var(--bg), var(--bg2));
|
||||
color: var(--fg);
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Inter, sans-serif;
|
||||
overflow: hidden;
|
||||
}
|
||||
.glow {
|
||||
position: absolute;
|
||||
inset: -20%;
|
||||
background:
|
||||
radial-gradient(700px 340px at 20% 30%, rgba(255,59,48,0.22), transparent 60%),
|
||||
radial-gradient(700px 340px at 80% 70%, rgba(255,111,97,0.20), transparent 60%);
|
||||
filter: blur(40px);
|
||||
z-index: 0;
|
||||
}
|
||||
.card {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: min(720px, 92vw);
|
||||
padding: 32px;
|
||||
border-radius: 20px;
|
||||
background: rgba(255,255,255,0.03);
|
||||
border: 1px solid rgba(255,255,255,0.08);
|
||||
box-shadow: 0 10px 40px rgba(80,0,0,0.45), inset 0 0 0 1px rgba(255,255,255,0.03);
|
||||
backdrop-filter: blur(10px);
|
||||
text-align: center;
|
||||
}
|
||||
.code {
|
||||
font-size: clamp(48px, 12vw, 120px);
|
||||
line-height: 0.9;
|
||||
font-weight: 800;
|
||||
letter-spacing: -2px;
|
||||
background: linear-gradient(90deg, var(--accent), var(--accent2));
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
margin: 8px 0 12px 0;
|
||||
text-shadow: 0 8px 40px var(--accentGlow);
|
||||
}
|
||||
.title {
|
||||
font-size: clamp(18px, 3.2vw, 28px);
|
||||
font-weight: 600;
|
||||
opacity: 0.95;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.desc {
|
||||
font-size: 15px;
|
||||
opacity: 0.75;
|
||||
margin: 0 auto 20px auto;
|
||||
max-width: 60ch;
|
||||
}
|
||||
.btns {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
justify-content: center;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.btn {
|
||||
appearance: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 12px 18px;
|
||||
border-radius: 12px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
background: linear-gradient(135deg, var(--accent), #c62828);
|
||||
box-shadow: 0 6px 18px var(--accentGlow);
|
||||
transition: transform .2s ease, box-shadow .2s ease, filter .2s ease;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.btn:hover { transform: translateY(-2px); filter: brightness(1.05); }
|
||||
.btn.secondary { background: linear-gradient(135deg, #e65100, var(--accent2)); box-shadow: 0 6px 18px var(--accentGlow2); }
|
||||
.hint { margin-top: 16px; font-size: 13px; opacity: 0.6; }
|
||||
</style>
|
||||
<script>
|
||||
function goHome() {
|
||||
window.location.href = '{{.BasePath}}/';
|
||||
}
|
||||
function bindEsc() {
|
||||
const handler = (e) => { if (e.key === 'Escape' || e.key === 'Esc') { e.preventDefault(); goHome(); } };
|
||||
window.addEventListener('keydown', handler);
|
||||
document.addEventListener('keydown', handler);
|
||||
// фокус на body для гарантии получения клавиш
|
||||
if (document && document.body) {
|
||||
document.body.setAttribute('tabindex', '-1');
|
||||
document.body.focus({ preventScroll: true });
|
||||
}
|
||||
}
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', bindEsc);
|
||||
} else {
|
||||
bindEsc();
|
||||
}
|
||||
</script>
|
||||
<meta http-equiv="refresh" content="30">
|
||||
<meta name="robots" content="noindex">
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'><text y='50%' x='50%' dominant-baseline='middle' text-anchor='middle' font-size='42'>🚫</text></svg>">
|
||||
</head>
|
||||
<body>
|
||||
<div class="glow"></div>
|
||||
<div class="card">
|
||||
<div class="code">404</div>
|
||||
<div class="title">Страница не найдена</div>
|
||||
<p class="desc">{{.Message}}</p>
|
||||
<div class="btns">
|
||||
<a class="btn" href="{{.BasePath}}/">🏠 На главную</a>
|
||||
<a class="btn secondary" href="{{.BasePath}}/run">🚀 К выполнению</a>
|
||||
</div>
|
||||
<div class="hint">Нажмите Esc, чтобы вернуться на главную</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
|
||||
@@ -235,13 +235,13 @@ const PromptsPageTemplate = `
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>⚙️ Системные промпты</h1>
|
||||
<p>Управление системными промптами Linux Command GPT</p>
|
||||
<p>Управление системными промптами {{.AppName}}</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>
|
||||
<a href="{{.BasePath}}/" class="nav-btn">🏠 Главная</a>
|
||||
<a href="{{.BasePath}}/run" class="nav-btn">🚀 Выполнение</a>
|
||||
<a href="{{.BasePath}}/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>
|
||||
@@ -391,7 +391,7 @@ const PromptsPageTemplate = `
|
||||
|
||||
function saveCurrentPrompts(lang) {
|
||||
// Отправляем запрос для сохранения текущих промптов с новым языком
|
||||
fetch('/prompts/save-lang', {
|
||||
fetch('{{.BasePath}}/prompts/save-lang', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -407,12 +407,17 @@ const PromptsPageTemplate = `
|
||||
|
||||
function editVerbosePrompt(mode, content) {
|
||||
// Редактирование промпта подробности
|
||||
alert('Редактирование промптов подробности будет реализовано');
|
||||
document.getElementById('formTitle').textContent = 'Редактировать промпт подробности (' + mode + ')';
|
||||
document.getElementById('promptId').value = mode;
|
||||
document.getElementById('promptName').value = mode;
|
||||
document.getElementById('promptDescription').value = 'Промпт для режима ' + mode;
|
||||
document.getElementById('promptContent').value = content;
|
||||
document.getElementById('promptForm').style.display = 'block';
|
||||
}
|
||||
|
||||
function deletePrompt(id) {
|
||||
if (confirm('Вы уверены, что хотите удалить промпт #' + id + '?')) {
|
||||
fetch('/prompts/delete/' + id, {
|
||||
fetch('{{.BasePath}}/prompts/delete/' + id, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
.then(response => {
|
||||
@@ -432,10 +437,42 @@ const PromptsPageTemplate = `
|
||||
document.getElementById('promptFormData').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Валидация длины полей
|
||||
const name = document.getElementById('promptName').value;
|
||||
const description = document.getElementById('promptDescription').value;
|
||||
const content = document.getElementById('promptContent').value;
|
||||
|
||||
const maxContentLength = {{.MaxSystemPromptLength}};
|
||||
const maxNameLength = {{.MaxPromptNameLength}};
|
||||
const maxDescLength = {{.MaxPromptDescLength}};
|
||||
|
||||
if (content.length > maxContentLength) {
|
||||
alert('Содержимое промпта слишком длинное: максимум ' + maxContentLength + ' символов');
|
||||
return;
|
||||
}
|
||||
if (name.length > maxNameLength) {
|
||||
alert('Название промпта слишком длинное: максимум ' + maxNameLength + ' символов');
|
||||
return;
|
||||
}
|
||||
if (description.length > maxDescLength) {
|
||||
alert('Описание промпта слишком длинное: максимум ' + maxDescLength + ' символов');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData(this);
|
||||
const id = formData.get('id');
|
||||
const url = id ? '/prompts/edit/' + id : '/prompts/add';
|
||||
const method = id ? 'PUT' : 'POST';
|
||||
|
||||
// Определяем, это системный промпт или промпт подробности
|
||||
const isVerbosePrompt = ['v', 'vv', 'vvv'].includes(id);
|
||||
|
||||
let url, method;
|
||||
if (isVerbosePrompt) {
|
||||
url = '{{.BasePath}}/prompts/edit-verbose/' + id;
|
||||
method = 'PUT';
|
||||
} else {
|
||||
url = id ? '{{.BasePath}}/prompts/edit/' + id : '{{.BasePath}}/prompts/add';
|
||||
method = id ? 'PUT' : 'POST';
|
||||
}
|
||||
|
||||
fetch(url, {
|
||||
method: method,
|
||||
@@ -464,7 +501,7 @@ const PromptsPageTemplate = `
|
||||
// Функция восстановления системного промпта
|
||||
function restorePrompt(id) {
|
||||
if (confirm('Восстановить промпт к значению по умолчанию?')) {
|
||||
fetch('/prompts/restore/' + id, {
|
||||
fetch('{{.BasePath}}/prompts/restore/' + id, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -489,7 +526,7 @@ const PromptsPageTemplate = `
|
||||
// Функция восстановления verbose промпта
|
||||
function restoreVerbosePrompt(mode) {
|
||||
if (confirm('Восстановить промпт к значению по умолчанию?')) {
|
||||
fetch('/prompts/restore-verbose/' + mode, {
|
||||
fetch('{{.BasePath}}/prompts/restore-verbose/' + mode, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -7,7 +7,7 @@ const ResultsPageTemplate = `
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>LCG Results - Linux Command GPT</title>
|
||||
<title>{{.AppAbbreviation}} Результаты - {{.AppName}}</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
@@ -73,8 +73,10 @@ const ResultsPageTemplate = `
|
||||
}
|
||||
.files-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 20px;
|
||||
align-items: stretch;
|
||||
grid-auto-rows: auto;
|
||||
}
|
||||
.file-card {
|
||||
background: white;
|
||||
@@ -91,32 +93,36 @@ const ResultsPageTemplate = `
|
||||
}
|
||||
.file-card-content {
|
||||
cursor: pointer;
|
||||
padding-left: 28px;
|
||||
}
|
||||
.file-actions {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
left: 10px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.delete-btn {
|
||||
background: #e74c3c;
|
||||
color: white;
|
||||
background: transparent;
|
||||
color: #ef9a9a; /* бледно-красный */
|
||||
border: none;
|
||||
padding: 6px 12px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.8em;
|
||||
transition: background 0.3s ease;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
transition: color 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
.delete-btn:hover {
|
||||
background: #c0392b;
|
||||
color:rgb(171, 27, 24); /* чуть ярче при ховере */
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.file-name {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
font-size: 1.1em;
|
||||
padding-right: 10px;
|
||||
}
|
||||
.file-info {
|
||||
color: #666;
|
||||
@@ -165,16 +171,25 @@ const ResultsPageTemplate = `
|
||||
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; }
|
||||
.header h1 { font-size: 1.9em; }
|
||||
.content { padding: 20px; }
|
||||
.files-grid { grid-template-columns: 1fr; }
|
||||
.files-grid { dummy-attr: none; }
|
||||
/* Стили карточек как в истории */
|
||||
.file-card { background: #f0f8f0; border: 1px solid #a8e6cf; padding: 15px; }
|
||||
.file-card:hover { border-color: #2d5016; box-shadow: 0 8px 25px rgba(45,80,22,0.2); transform: translateY(-2px); }
|
||||
.file-name { color: #333; margin-bottom: 8px; }
|
||||
.file-info { color: #666; font-size: 0.9em; }
|
||||
.file-preview { background: #f8f9fa; border-left: 3px solid #2d5016; font-size: 0.85em; }
|
||||
.file-actions { top: 8px; left: 8px; }
|
||||
.delete-btn { padding: 2px 6px; font-size: 16px; }
|
||||
.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; }
|
||||
.search-container input { font-size: 16px; width: 96% !important; }
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.header h1 { font-size: 1.8em; }
|
||||
.header h1 { font-size: 1.6em; }
|
||||
.content { padding: 16px; }
|
||||
.stats { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
@@ -182,15 +197,15 @@ const ResultsPageTemplate = `
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🚀 LCG Results</h1>
|
||||
<p>Просмотр сохраненных результатов Linux Command GPT</p>
|
||||
<h1>🚀 {{.AppAbbreviation}} - {{.AppName}}</h1>
|
||||
<p>Просмотр сохраненных результатов {{.AppName}}</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>
|
||||
<a href="{{.BasePath}}/run" class="nav-btn">🚀 Выполнение</a>
|
||||
<a href="{{.BasePath}}/history" class="nav-btn">📝 История</a>
|
||||
<a href="{{.BasePath}}/prompts" class="nav-btn">⚙️ Промпты</a>
|
||||
</div>
|
||||
|
||||
<!-- Поиск -->
|
||||
@@ -216,10 +231,10 @@ const ResultsPageTemplate = `
|
||||
{{range .Files}}
|
||||
<div class="file-card" data-content="{{.Content}}">
|
||||
<div class="file-actions">
|
||||
<button class="delete-btn" onclick="deleteFile('{{.Name}}')" title="Удалить файл">🗑️</button>
|
||||
<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-card-content" onclick="window.location.href='{{$.BasePath}}/file/{{.Name}}'">
|
||||
<div class="file-name">{{.DisplayName}}</div>
|
||||
<div class="file-info">
|
||||
📅 {{.ModTime}} | 📏 {{.Size}}
|
||||
</div>
|
||||
@@ -240,7 +255,7 @@ const ResultsPageTemplate = `
|
||||
<script>
|
||||
function deleteFile(filename) {
|
||||
if (confirm('Вы уверены, что хотите удалить файл "' + filename + '"?\\n\\nЭто действие нельзя отменить.')) {
|
||||
fetch('/delete/' + encodeURIComponent(filename), {
|
||||
fetch('{{.BasePath}}/delete/' + encodeURIComponent(filename), {
|
||||
method: 'DELETE'
|
||||
})
|
||||
.then(response => {
|
||||
|
||||
16
shell-code/docker-proxy-max.sh
Normal file
16
shell-code/docker-proxy-max.sh
Normal file
@@ -0,0 +1,16 @@
|
||||
#! /usr/bin/bash
|
||||
|
||||
VERSION=$1
|
||||
if [ -z "$VERSION" ]; then
|
||||
VERSION=latest
|
||||
fi
|
||||
|
||||
docker pull kuznetcovay/lcg:"${VERSION}"
|
||||
|
||||
docker run -p 8080:8080 \
|
||||
-e LCG_PROVIDER=proxy \
|
||||
-e LCG_HOST=https://direct-dev.ru \
|
||||
-e LCG_MODEL=GigaChat-2-Max \
|
||||
-e LCG_JWT_TOKEN="$(go-ansible-vault --key "$(cat ~/.config/gak)" \
|
||||
-i ~/.config/jwt.direct-dev.ru get -m JWT_TOKEN -q)" \
|
||||
kuznetcovay/lcg:"${VERSION}"
|
||||
7
shell-code/run-proxy-max.sh
Normal file
7
shell-code/run-proxy-max.sh
Normal file
@@ -0,0 +1,7 @@
|
||||
#! /usr/bin/bash
|
||||
|
||||
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 . $1 $2 $3 $4 $5 $6 $7 $8 $9
|
||||
|
||||
7
shell-code/run-proxy.sh
Normal file
7
shell-code/run-proxy.sh
Normal file
@@ -0,0 +1,7 @@
|
||||
#! /usr/bin/bash
|
||||
|
||||
LCG_PROVIDER=proxy LCG_HOST=https://direct-dev.ru \
|
||||
LCG_MODEL=GigaChat-2 \
|
||||
LCG_JWT_TOKEN=$(go-ansible-vault --key $(cat ~/.config/gak) -i ~/.config/jwt.direct-dev.ru get -m JWT_TOKEN -q) \
|
||||
go run . $1 $2 $3 $4 $5 $6 $7 $8 $9
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/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]
|
||||
162
ssl/ssl.go
Normal file
162
ssl/ssl.go
Normal file
@@ -0,0 +1,162 @@
|
||||
package ssl
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||
)
|
||||
|
||||
// GenerateSelfSignedCert генерирует самоподписанный сертификат
|
||||
func GenerateSelfSignedCert(host string) (*tls.Certificate, error) {
|
||||
// Создаем приватный ключ
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate private key: %v", err)
|
||||
}
|
||||
|
||||
// Создаем сертификат
|
||||
template := x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{"LCG Server"},
|
||||
Country: []string{"RU"},
|
||||
Province: []string{""},
|
||||
Locality: []string{""},
|
||||
StreetAddress: []string{""},
|
||||
PostalCode: []string{""},
|
||||
},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(365 * 24 * time.Hour), // 1 год
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback},
|
||||
DNSNames: []string{"localhost", host},
|
||||
}
|
||||
|
||||
// Подписываем сертификат
|
||||
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create certificate: %v", err)
|
||||
}
|
||||
|
||||
// Создаем TLS сертификат
|
||||
cert := &tls.Certificate{
|
||||
Certificate: [][]byte{certDER},
|
||||
PrivateKey: privateKey,
|
||||
}
|
||||
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
// SaveCertToFile сохраняет сертификат и ключ в файлы
|
||||
func SaveCertToFile(cert *tls.Certificate, certFile, keyFile string) error {
|
||||
// Создаем директорию если не существует
|
||||
certDir := filepath.Dir(certFile)
|
||||
if err := os.MkdirAll(certDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create cert directory: %v", err)
|
||||
}
|
||||
|
||||
// Сохраняем сертификат
|
||||
certOut, err := os.Create(certFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open cert file: %v", err)
|
||||
}
|
||||
defer certOut.Close()
|
||||
|
||||
if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Certificate[0]}); err != nil {
|
||||
return fmt.Errorf("failed to encode cert: %v", err)
|
||||
}
|
||||
|
||||
// Сохраняем приватный ключ
|
||||
keyOut, err := os.Create(keyFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open key file: %v", err)
|
||||
}
|
||||
defer keyOut.Close()
|
||||
|
||||
privateKeyDER, err := x509.MarshalPKCS8PrivateKey(cert.PrivateKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal private key: %v", err)
|
||||
}
|
||||
|
||||
if err := pem.Encode(keyOut, &pem.Block{Type: "PRIVATE KEY", Bytes: privateKeyDER}); err != nil {
|
||||
return fmt.Errorf("failed to encode private key: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadOrGenerateCert загружает существующий сертификат или генерирует новый
|
||||
func LoadOrGenerateCert(host string) (*tls.Certificate, error) {
|
||||
// Определяем пути к файлам сертификата
|
||||
certFile := config.AppConfig.Server.SSLCertFile
|
||||
keyFile := config.AppConfig.Server.SSLKeyFile
|
||||
|
||||
// Если пути не указаны, используем стандартные
|
||||
if certFile == "" {
|
||||
certFile = filepath.Join(config.AppConfig.Server.ConfigFolder, "server", "ssl", "cert.pem")
|
||||
}
|
||||
if keyFile == "" {
|
||||
keyFile = filepath.Join(config.AppConfig.Server.ConfigFolder, "server", "ssl", "key.pem")
|
||||
}
|
||||
|
||||
// Проверяем существующие файлы
|
||||
if _, err := os.Stat(certFile); err == nil {
|
||||
if _, err := os.Stat(keyFile); err == nil {
|
||||
// Загружаем существующий сертификат
|
||||
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if err == nil {
|
||||
return &cert, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Генерируем новый сертификат
|
||||
cert, err := GenerateSelfSignedCert(host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Сохраняем сертификат
|
||||
if err := SaveCertToFile(cert, certFile, keyFile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
// IsSecureHost проверяет, является ли хост безопасным для HTTP
|
||||
func IsSecureHost(host string) bool {
|
||||
secureHosts := []string{"localhost", "127.0.0.1", "::1"}
|
||||
return slices.Contains(secureHosts, host)
|
||||
}
|
||||
|
||||
// ShouldUseHTTPS определяет, нужно ли использовать HTTPS
|
||||
func ShouldUseHTTPS(host string) bool {
|
||||
|
||||
// Если явно разрешен HTTP, используем HTTP
|
||||
if config.AppConfig.Server.AllowHTTP {
|
||||
return false
|
||||
}
|
||||
|
||||
// Если хост не localhost/127.0.0.1, принуждаем к HTTPS
|
||||
if !IsSecureHost(host) {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
// По умолчанию для localhost используем HTTP
|
||||
return false
|
||||
}
|
||||
158
test_csrf.sh
Executable file
158
test_csrf.sh
Executable file
@@ -0,0 +1,158 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 🛡️ CSRF Protection Test Script
|
||||
# Тестирует CSRF защиту LCG приложения
|
||||
|
||||
echo "🛡️ Тестирование CSRF защиты LCG"
|
||||
echo "=================================="
|
||||
|
||||
# Цвета для вывода
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Функция для вывода результатов
|
||||
print_result() {
|
||||
local test_name="$1"
|
||||
local status="$2"
|
||||
local message="$3"
|
||||
|
||||
if [ "$status" = "PASS" ]; then
|
||||
echo -e "${GREEN}✅ $test_name: PASS${NC} - $message"
|
||||
elif [ "$status" = "FAIL" ]; then
|
||||
echo -e "${RED}❌ $test_name: FAIL${NC} - $message"
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ $test_name: $status${NC} - $message"
|
||||
fi
|
||||
}
|
||||
|
||||
# Проверяем, запущен ли сервер
|
||||
echo -e "${BLUE}🔍 Проверяем доступность сервера...${NC}"
|
||||
if ! curl -s http://localhost:8080/login > /dev/null 2>&1; then
|
||||
echo -e "${RED}❌ Сервер не доступен на localhost:8080${NC}"
|
||||
echo "Запустите сервер командой: LCG_SERVER_REQUIRE_AUTH=true ./lcg serve -p 8080"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}✅ Сервер доступен${NC}"
|
||||
|
||||
# Тест 1: Попытка выполнения команды без CSRF токена
|
||||
echo -e "\n${BLUE}🧪 Тест 1: Выполнение команды без CSRF токена${NC}"
|
||||
response=$(curl -s -w "%{http_code}" -X POST http://localhost:8080/api/execute \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"prompt":"whoami","system_id":"1"}' \
|
||||
-o /dev/null)
|
||||
|
||||
if [ "$response" = "403" ]; then
|
||||
print_result "CSRF защита /api/execute" "PASS" "Запрос заблокирован (403 Forbidden)"
|
||||
else
|
||||
print_result "CSRF защита /api/execute" "FAIL" "Запрос прошел (HTTP $response)"
|
||||
fi
|
||||
|
||||
# Тест 2: Попытка сохранения результата без CSRF токена
|
||||
echo -e "\n${BLUE}🧪 Тест 2: Сохранение результата без CSRF токена${NC}"
|
||||
response=$(curl -s -w "%{http_code}" -X POST http://localhost:8080/api/save-result \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"result":"test result","command":"test command"}' \
|
||||
-o /dev/null)
|
||||
|
||||
if [ "$response" = "403" ]; then
|
||||
print_result "CSRF защита /api/save-result" "PASS" "Запрос заблокирован (403 Forbidden)"
|
||||
else
|
||||
print_result "CSRF защита /api/save-result" "FAIL" "Запрос прошел (HTTP $response)"
|
||||
fi
|
||||
|
||||
# Тест 3: Попытка добавления в историю без CSRF токена
|
||||
echo -e "\n${BLUE}🧪 Тест 3: Добавление в историю без CSRF токена${NC}"
|
||||
response=$(curl -s -w "%{http_code}" -X POST http://localhost:8080/api/add-to-history \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"prompt":"test prompt","result":"test result"}' \
|
||||
-o /dev/null)
|
||||
|
||||
if [ "$response" = "403" ]; then
|
||||
print_result "CSRF защита /api/add-to-history" "PASS" "Запрос заблокирован (403 Forbidden)"
|
||||
else
|
||||
print_result "CSRF защита /api/add-to-history" "FAIL" "Запрос прошел (HTTP $response)"
|
||||
fi
|
||||
|
||||
# Тест 4: Проверка GET запросов (должны работать)
|
||||
echo -e "\n${BLUE}🧪 Тест 4: GET запросы (должны работать)${NC}"
|
||||
response=$(curl -s -w "%{http_code}" http://localhost:8080/login -o /dev/null)
|
||||
|
||||
if [ "$response" = "200" ]; then
|
||||
print_result "GET запросы" "PASS" "GET запросы работают (HTTP $response)"
|
||||
else
|
||||
print_result "GET запросы" "FAIL" "GET запросы не работают (HTTP $response)"
|
||||
fi
|
||||
|
||||
# Тест 5: Проверка наличия CSRF токена на странице входа
|
||||
echo -e "\n${BLUE}🧪 Тест 5: Наличие CSRF токена на странице входа${NC}"
|
||||
csrf_token=$(curl -s http://localhost:8080/login | grep -o 'name="csrf_token"[^>]*value="[^"]*"' | sed 's/.*value="\([^"]*\)".*/\1/')
|
||||
|
||||
if [ -n "$csrf_token" ]; then
|
||||
print_result "CSRF токен на странице входа" "PASS" "Токен найден: ${csrf_token:0:20}..."
|
||||
else
|
||||
print_result "CSRF токен на странице входа" "FAIL" "Токен не найден"
|
||||
fi
|
||||
|
||||
# Тест 6: Попытка атаки с поддельным CSRF токеном
|
||||
echo -e "\n${BLUE}🧪 Тест 6: Атака с поддельным CSRF токеном${NC}"
|
||||
response=$(curl -s -w "%{http_code}" -X POST http://localhost:8080/api/execute \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-CSRF-Token: fake_token" \
|
||||
-d '{"prompt":"whoami","system_id":"1"}' \
|
||||
-o /dev/null)
|
||||
|
||||
if [ "$response" = "403" ]; then
|
||||
print_result "CSRF защита от поддельного токена" "PASS" "Поддельный токен заблокирован (403 Forbidden)"
|
||||
else
|
||||
print_result "CSRF защита от поддельного токена" "FAIL" "Поддельный токен принят (HTTP $response)"
|
||||
fi
|
||||
|
||||
# Итоговый отчет
|
||||
echo -e "\n${BLUE}📊 Итоговый отчет:${NC}"
|
||||
echo "=================================="
|
||||
|
||||
# Подсчитываем результаты
|
||||
total_tests=6
|
||||
passed_tests=0
|
||||
|
||||
# Проверяем каждый тест
|
||||
if curl -s -w "%{http_code}" -X POST http://localhost:8080/api/execute -H "Content-Type: application/json" -d '{"prompt":"test"}' -o /dev/null | grep -q "403"; then
|
||||
((passed_tests++))
|
||||
fi
|
||||
|
||||
if curl -s -w "%{http_code}" -X POST http://localhost:8080/api/save-result -H "Content-Type: application/json" -d '{"result":"test"}' -o /dev/null | grep -q "403"; then
|
||||
((passed_tests++))
|
||||
fi
|
||||
|
||||
if curl -s -w "%{http_code}" -X POST http://localhost:8080/api/add-to-history -H "Content-Type: application/json" -d '{"prompt":"test"}' -o /dev/null | grep -q "403"; then
|
||||
((passed_tests++))
|
||||
fi
|
||||
|
||||
if curl -s -w "%{http_code}" http://localhost:8080/login -o /dev/null | grep -q "200"; then
|
||||
((passed_tests++))
|
||||
fi
|
||||
|
||||
if curl -s http://localhost:8080/login | grep -q 'name="csrf_token"'; then
|
||||
((passed_tests++))
|
||||
fi
|
||||
|
||||
if curl -s -w "%{http_code}" -X POST http://localhost:8080/api/execute -H "Content-Type: application/json" -H "X-CSRF-Token: fake" -d '{"prompt":"test"}' -o /dev/null | grep -q "403"; then
|
||||
((passed_tests++))
|
||||
fi
|
||||
|
||||
echo -e "Пройдено тестов: ${GREEN}$passed_tests${NC} из ${BLUE}$total_tests${NC}"
|
||||
|
||||
if [ $passed_tests -eq $total_tests ]; then
|
||||
echo -e "${GREEN}🎉 Все тесты пройдены! CSRF защита работает корректно.${NC}"
|
||||
exit 0
|
||||
elif [ $passed_tests -ge 4 ]; then
|
||||
echo -e "${YELLOW}⚠️ Большинство тестов пройдено, но есть проблемы с CSRF защитой.${NC}"
|
||||
exit 1
|
||||
else
|
||||
echo -e "${RED}❌ Критические проблемы с CSRF защитой!${NC}"
|
||||
exit 2
|
||||
fi
|
||||
154
validation/validation.go
Normal file
154
validation/validation.go
Normal file
@@ -0,0 +1,154 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||
)
|
||||
|
||||
// ValidationError представляет ошибку валидации
|
||||
type ValidationError struct {
|
||||
Field string
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e ValidationError) Error() string {
|
||||
return fmt.Sprintf("%s: %s", e.Field, e.Message)
|
||||
}
|
||||
|
||||
// ValidateSystemPrompt проверяет длину системного промпта
|
||||
func ValidateSystemPrompt(prompt string) error {
|
||||
maxLen := config.AppConfig.Validation.MaxSystemPromptLength
|
||||
if len(prompt) > maxLen {
|
||||
return ValidationError{
|
||||
Field: "system_prompt",
|
||||
Message: fmt.Sprintf("системный промпт слишком длинный: %d символов (максимум %d)", len(prompt), maxLen),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateUserMessage проверяет длину пользовательского сообщения
|
||||
func ValidateUserMessage(message string) error {
|
||||
maxLen := config.AppConfig.Validation.MaxUserMessageLength
|
||||
if len(message) > maxLen {
|
||||
return ValidationError{
|
||||
Field: "user_message",
|
||||
Message: fmt.Sprintf("пользовательское сообщение слишком длинное: %d символов (максимум %d)", len(message), maxLen),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidatePromptAndMessage проверяет и системный промпт, и пользовательское сообщение
|
||||
func ValidatePromptAndMessage(systemPrompt, userMessage string) error {
|
||||
if err := ValidateSystemPrompt(systemPrompt); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ValidateUserMessage(userMessage); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TruncateSystemPrompt обрезает системный промпт до максимальной длины
|
||||
func TruncateSystemPrompt(prompt string) string {
|
||||
maxLen := config.AppConfig.Validation.MaxSystemPromptLength
|
||||
if len(prompt) <= maxLen {
|
||||
return prompt
|
||||
}
|
||||
return prompt[:maxLen]
|
||||
}
|
||||
|
||||
// TruncateUserMessage обрезает пользовательское сообщение до максимальной длины
|
||||
func TruncateUserMessage(message string) string {
|
||||
maxLen := config.AppConfig.Validation.MaxUserMessageLength
|
||||
if len(message) <= maxLen {
|
||||
return message
|
||||
}
|
||||
return message[:maxLen]
|
||||
}
|
||||
|
||||
// GetSystemPromptLength возвращает длину системного промпта
|
||||
func GetSystemPromptLength(prompt string) int {
|
||||
return len(prompt)
|
||||
}
|
||||
|
||||
// GetUserMessageLength возвращает длину пользовательского сообщения
|
||||
func GetUserMessageLength(message string) int {
|
||||
return len(message)
|
||||
}
|
||||
|
||||
// FormatLengthInfo форматирует информацию о длине для отображения
|
||||
func FormatLengthInfo(systemPrompt, userMessage string) string {
|
||||
systemLen := GetSystemPromptLength(systemPrompt)
|
||||
userLen := GetUserMessageLength(userMessage)
|
||||
maxSystemLen := config.AppConfig.Validation.MaxSystemPromptLength
|
||||
maxUserLen := config.AppConfig.Validation.MaxUserMessageLength
|
||||
|
||||
var warnings []string
|
||||
|
||||
if systemLen > maxSystemLen {
|
||||
warnings = append(warnings, fmt.Sprintf("⚠️ Системный промпт превышает лимит: %d/%d символов", systemLen, maxSystemLen))
|
||||
}
|
||||
|
||||
if userLen > maxUserLen {
|
||||
warnings = append(warnings, fmt.Sprintf("⚠️ Пользовательское сообщение превышает лимит: %d/%d символов", userLen, maxUserLen))
|
||||
}
|
||||
|
||||
if len(warnings) == 0 {
|
||||
return fmt.Sprintf("✅ Длины в пределах нормы: системный промпт %d/%d, сообщение %d/%d",
|
||||
systemLen, maxSystemLen, userLen, maxUserLen)
|
||||
}
|
||||
|
||||
return strings.Join(warnings, "\n")
|
||||
}
|
||||
|
||||
// ValidatePromptName проверяет длину названия промпта
|
||||
func ValidatePromptName(name string) error {
|
||||
maxLen := config.AppConfig.Validation.MaxPromptNameLength
|
||||
if len(name) > maxLen {
|
||||
return ValidationError{
|
||||
Field: "prompt_name",
|
||||
Message: fmt.Sprintf("название промпта слишком длинное: %d символов (максимум %d)", len(name), maxLen),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidatePromptDescription проверяет длину описания промпта
|
||||
func ValidatePromptDescription(description string) error {
|
||||
maxLen := config.AppConfig.Validation.MaxPromptDescLength
|
||||
if len(description) > maxLen {
|
||||
return ValidationError{
|
||||
Field: "prompt_description",
|
||||
Message: fmt.Sprintf("описание промпта слишком длинное: %d символов (максимум %d)", len(description), maxLen),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateCommand проверяет длину команды
|
||||
func ValidateCommand(command string) error {
|
||||
maxLen := config.AppConfig.Validation.MaxCommandLength
|
||||
if len(command) > maxLen {
|
||||
return ValidationError{
|
||||
Field: "command",
|
||||
Message: fmt.Sprintf("команда слишком длинная: %d символов (максимум %d)", len(command), maxLen),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateExplanation проверяет длину объяснения
|
||||
func ValidateExplanation(explanation string) error {
|
||||
maxLen := config.AppConfig.Validation.MaxExplanationLength
|
||||
if len(explanation) > maxLen {
|
||||
return ValidationError{
|
||||
Field: "explanation",
|
||||
Message: fmt.Sprintf("объяснение слишком длинное: %d символов (максимум %d)", len(explanation), maxLen),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user