Compare commits

...

8 Commits

14 changed files with 151 additions and 22 deletions

View File

@@ -1,6 +1,10 @@
# Используем готовый образ Ollama # Используем готовый образ Ollama
FROM localhost/ollama_packed:latest FROM localhost/ollama_packed:latest
RUN apt-get update && apt-get install -y --no-install-recommends bash && apt-get install -y --no-install-recommends curl \
&& apt-get install -y --no-install-recommends jq && apt-get install -y --no-install-recommends wget
# Устанавливаем bash если его нет (базовый образ ollama может быть на разных дистрибутивах) # Устанавливаем bash если его нет (базовый образ ollama может быть на разных дистрибутивах)
RUN if ! command -v bash >/dev/null 2>&1; then \ RUN if ! command -v bash >/dev/null 2>&1; then \
if command -v apk >/dev/null 2>&1; then \ if command -v apk >/dev/null 2>&1; then \
@@ -72,6 +76,8 @@ ENV LCG_SERVER_HOST=0.0.0.0
ENV LCG_SERVER_PORT=8080 ENV LCG_SERVER_PORT=8080
ENV LCG_DOMAIN="remote.ollama-server.ru" ENV LCG_DOMAIN="remote.ollama-server.ru"
ENV LCG_COOKIE_PATH="/lcg" ENV LCG_COOKIE_PATH="/lcg"
# ENV LCG_FORCE_NO_CSRF=true
# ENV LCG_SERVER_ALLOW_HTTP=true # ENV LCG_SERVER_ALLOW_HTTP=true
# ENV OLLAMA_HOST=127.0.0.1 # ENV OLLAMA_HOST=127.0.0.1
# ENV OLLAMA_PORT=11434 # ENV OLLAMA_PORT=11434

View File

@@ -52,7 +52,7 @@ run: ## Запустить контейнер (Docker)
-v lcg-results:/app/data/results \ -v lcg-results:/app/data/results \
-v lcg-prompts:/app/data/prompts \ -v lcg-prompts:/app/data/prompts \
-v lcg-config:/app/data/config \ -v lcg-config:/app/data/config \
${IMAGE_NAME}:${IMAGE_TAG} ${IMAGE_NAME}:${IMAGE_TAG} ollama serve
@echo "Контейнер ${CONTAINER_NAME} запущен" @echo "Контейнер ${CONTAINER_NAME} запущен"
run-podman: ## Запустить контейнер (Podman) run-podman: ## Запустить контейнер (Podman)
@@ -69,7 +69,7 @@ run-podman: ## Запустить контейнер (Podman)
-v lcg-results:/app/data/results \ -v lcg-results:/app/data/results \
-v lcg-prompts:/app/data/prompts \ -v lcg-prompts:/app/data/prompts \
-v lcg-config:/app/data/config \ -v lcg-config:/app/data/config \
${IMAGE_NAME}:${IMAGE_TAG} ${IMAGE_NAME}:${IMAGE_TAG} ollama serve
@echo "Контейнер ${CONTAINER_NAME} запущен" @echo "Контейнер ${CONTAINER_NAME} запущен"
run-podman-nodemon: ## Запустить контейнер (Podman) без -d run-podman-nodemon: ## Запустить контейнер (Podman) без -d
@@ -86,7 +86,7 @@ run-podman-nodemon: ## Запустить контейнер (Podman) без -d
-v lcg-results:/app/data/results \ -v lcg-results:/app/data/results \
-v lcg-prompts:/app/data/prompts \ -v lcg-prompts:/app/data/prompts \
-v lcg-config:/app/data/config \ -v lcg-config:/app/data/config \
${IMAGE_NAME}:${IMAGE_TAG} ${IMAGE_NAME}:${IMAGE_TAG} ollama serve
@echo "Контейнер ${CONTAINER_NAME} запущен" @echo "Контейнер ${CONTAINER_NAME} запущен"
stop: ## Остановить контейнер (Docker) stop: ## Остановить контейнер (Docker)

View File

@@ -65,7 +65,8 @@ export LCG_CONFIG_FOLDER="${LCG_CONFIG_FOLDER:-/app/data/config}"
export LCG_SERVER_HOST="${LCG_SERVER_HOST:-0.0.0.0}" export LCG_SERVER_HOST="${LCG_SERVER_HOST:-0.0.0.0}"
export LCG_SERVER_PORT="${LCG_SERVER_PORT:-8080}" export LCG_SERVER_PORT="${LCG_SERVER_PORT:-8080}"
export LCG_SERVER_ALLOW_HTTP="${LCG_SERVER_ALLOW_HTTP:-false}" export LCG_SERVER_ALLOW_HTTP="${LCG_SERVER_ALLOW_HTTP:-false}"
export LCG_FORCE_NO_CSRF="${LCG_FORCE_NO_CSRF:-true}" export LCG_FORCE_NO_CSRF="${LCG_FORCE_NO_CSRF:-false}"
export LCG_CSRF_DEBUG_FILE="${LCG_CSRF_DEBUG_FILE:-/app/data/csrf-debug.log}"
if [ "$LCG_FORCE_NO_CSRF" = "true" ]; then if [ "$LCG_FORCE_NO_CSRF" = "true" ]; then
info "CSRF проверка отключена через LCG_FORCE_NO_CSRF" info "CSRF проверка отключена через LCG_FORCE_NO_CSRF"

View File

@@ -1 +1 @@
v.2.0.21 v2.0.28

View File

@@ -27,6 +27,7 @@ type Config struct {
ResultHistory string ResultHistory string
NoHistoryEnv string NoHistoryEnv string
AllowExecution bool AllowExecution bool
Think bool
Query string Query string
MainFlags MainFlags MainFlags MainFlags
Server ServerConfig Server ServerConfig

View File

@@ -1 +1 @@
v.2.0.21 v2.0.28

View File

@@ -49,6 +49,14 @@ type Gpt3Request struct {
Options Gpt3Options `json:"options"` Options Gpt3Options `json:"options"`
} }
type Gpt3ThinkRequest struct {
Model string `json:"model"`
Stream bool `json:"stream"`
Think bool `json:"think"`
Messages []Chat `json:"messages"`
Options Gpt3Options `json:"options"`
}
type Gpt3Options struct { type Gpt3Options struct {
Temperature float64 `json:"temperature"` Temperature float64 `json:"temperature"`
} }

View File

@@ -200,12 +200,26 @@ func (p *ProxyAPIProvider) Health() error {
// Chat для OllamaProvider // Chat для OllamaProvider
func (o *OllamaProvider) Chat(messages []Chat) (string, error) { func (o *OllamaProvider) Chat(messages []Chat) (string, error) {
payload := Gpt3Request{
think := config.AppConfig.Think
var payload interface{}
if think {
payload = Gpt3Request{
Model: o.Model, Model: o.Model,
Messages: messages, Messages: messages,
Stream: false, Stream: false,
Options: Gpt3Options{o.Temperature}, Options: Gpt3Options{o.Temperature},
} }
} else {
payload = Gpt3ThinkRequest{
Model: o.Model,
Messages: messages,
Stream: false,
Think: false,
Options: Gpt3Options{o.Temperature},
}
}
jsonData, err := json.Marshal(payload) jsonData, err := json.Marshal(payload)
if err != nil { if err != nil {

View File

@@ -5,7 +5,7 @@ metadata:
namespace: lcg namespace: lcg
data: data:
# Основные настройки # Основные настройки
LCG_VERSION: "v.2.0.21" LCG_VERSION: "v2.0.28"
LCG_BASE_PATH: "/lcg" LCG_BASE_PATH: "/lcg"
LCG_SERVER_HOST: "0.0.0.0" LCG_SERVER_HOST: "0.0.0.0"
LCG_SERVER_PORT: "8080" LCG_SERVER_PORT: "8080"

View File

@@ -5,7 +5,7 @@ metadata:
namespace: lcg namespace: lcg
labels: labels:
app: lcg app: lcg
version: v.2.0.21 version: v2.0.28
spec: spec:
replicas: 1 replicas: 1
selector: selector:
@@ -18,7 +18,7 @@ spec:
spec: spec:
containers: containers:
- name: lcg - name: lcg
image: kuznetcovay/lcg:v.2.0.21 image: kuznetcovay/lcg:v2.0.28
imagePullPolicy: Always imagePullPolicy: Always
ports: ports:
- containerPort: 8080 - containerPort: 8080

View File

@@ -15,11 +15,11 @@ resources:
# Common labels # Common labels
# commonLabels: # commonLabels:
# app: lcg # app: lcg
# version: v.2.0.21 # version: v2.0.28
# managed-by: kustomize # managed-by: kustomize
# Images # Images
# images: # images:
# - name: lcg # - name: lcg
# newName: kuznetcovay/lcg # newName: kuznetcovay/lcg
# newTag: v.2.0.21 # newTag: v2.0.28

18
main.go
View File

@@ -109,6 +109,7 @@ lcg [опции] <описание команды>
LCG_PROXY_URL URL прокси для proxy провайдера (по умолчанию: /api/v1/protected/sberchat/chat) LCG_PROXY_URL URL прокси для proxy провайдера (по умолчанию: /api/v1/protected/sberchat/chat)
LCG_API_KEY_FILE Файл с API ключом (по умолчанию: .openai_api_key) LCG_API_KEY_FILE Файл с API ключом (по умолчанию: .openai_api_key)
LCG_APP_NAME Название приложения (по умолчанию: Linux Command GPT) LCG_APP_NAME Название приложения (по умолчанию: Linux Command GPT)
LCG_ALLOW_THINK только для ollama: разрешить модели отправлять свои размышления ("1" или "true" = разрешено, пусто = запрещено). Имеет смысл для моделей, которые поддерживают эти действия: qwen3, deepseek.
Настройки истории и выполнения: Настройки истории и выполнения:
LCG_NO_HISTORY Отключить запись истории ("1" или "true" = отключено, пусто = включено) LCG_NO_HISTORY Отключить запись истории ("1" или "true" = отключено, пусто = включено)
@@ -163,12 +164,18 @@ lcg [опции] <описание команды>
Usage: "Disable writing/updating command history (overrides LCG_NO_HISTORY)", Usage: "Disable writing/updating command history (overrides LCG_NO_HISTORY)",
Value: false, Value: false,
}, },
&cli.BoolFlag{
Name: "think",
Aliases: []string{"T"},
Usage: "Разрешить модели отправлять свои размышления",
Value: false,
},
&cli.StringFlag{ &cli.StringFlag{
Name: "query", Name: "query",
Aliases: []string{"Q"}, Aliases: []string{"Q"},
Usage: "Query to send to the model", Usage: "Query to send to the model",
DefaultText: "Hello? what day is it today?", DefaultText: "Привет! Порадуй меня случайной Linux командой ...",
Value: "Hello? what day is it today?", Value: "Привет! Порадуй меня случайной Linux командой ...",
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: "sys", Name: "sys",
@@ -216,7 +223,10 @@ lcg [опции] <описание команды>
if c.IsSet("model") { if c.IsSet("model") {
config.AppConfig.Model = model config.AppConfig.Model = model
} }
config.AppConfig.Think = false
if c.IsSet("think") {
config.AppConfig.Think = c.Bool("think")
}
promptID := c.Int("prompt-id") promptID := c.Int("prompt-id")
timeout := c.Int("timeout") timeout := c.Int("timeout")
@@ -1018,7 +1028,7 @@ func printDebugInfo(file, system, commandInput string, timeout int) {
fmt.Printf("📁 Файл: %s\n", file) fmt.Printf("📁 Файл: %s\n", file)
fmt.Printf("🤖 Системный промпт: %s\n", system) fmt.Printf("🤖 Системный промпт: %s\n", system)
fmt.Printf("💬 Запрос: %s\n", commandInput) fmt.Printf("💬 Запрос: %s\n", commandInput)
fmt.Printf("⏱️ Таймаут: %d сек\n", timeout) fmt.Printf("⏱️ Таймаут: %d сек\n", timeout)
fmt.Printf("🌐 Провайдер: %s\n", config.AppConfig.ProviderType) fmt.Printf("🌐 Провайдер: %s\n", config.AppConfig.ProviderType)
fmt.Printf("🏠 Хост: %s\n", config.AppConfig.Host) fmt.Printf("🏠 Хост: %s\n", config.AppConfig.Host)
fmt.Printf("🧠 Модель: %s\n", config.AppConfig.Model) fmt.Printf("🧠 Модель: %s\n", config.AppConfig.Model)

View File

@@ -8,6 +8,8 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
"path/filepath"
"sync"
"time" "time"
"github.com/direct-dev-ru/linux-command-gpt/config" "github.com/direct-dev-ru/linux-command-gpt/config"
@@ -20,10 +22,72 @@ const (
CSRFTokenLifetimeSeconds = CSRFTokenLifetimeHours * 60 * 60 CSRFTokenLifetimeSeconds = CSRFTokenLifetimeHours * 60 * 60
) )
// csrfDebugPrint выводит отладочную информацию только если включен debug режим var (
func csrfDebugPrint(format string, args ...interface{}) { // csrfDebugFile файл для отладочного вывода CSRF
csrfDebugFile *os.File
// csrfDebugFileMutex мьютекс для безопасной записи в файл
csrfDebugFileMutex sync.Mutex
)
// initCSRFDebugFile инициализирует файл для отладочного вывода CSRF
func initCSRFDebugFile() error {
debugFile := os.Getenv("LCG_CSRF_DEBUG_FILE")
if debugFile == "" {
return nil // Файл не указан, ничего не делаем
}
// Создаем директорию для файла, если нужно
dir := filepath.Dir(debugFile)
if dir != "." && dir != "" {
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("failed to create directory for CSRF debug file %s: %v", dir, err)
}
}
// Создаем/перезаписываем файл
file, err := os.Create(debugFile)
if err != nil {
return fmt.Errorf("failed to create CSRF debug file %s: %v", debugFile, err)
}
csrfDebugFileMutex.Lock()
// Закрываем старый файл, если был открыт
if csrfDebugFile != nil {
csrfDebugFile.Close()
}
csrfDebugFile = file
csrfDebugFileMutex.Unlock()
// Записываем заголовок
header := fmt.Sprintf("=== CSRF Debug Log Started at %s ===\n", time.Now().Format(time.RFC3339))
if _, err := csrfDebugFile.WriteString(header); err != nil {
return fmt.Errorf("failed to write header to CSRF debug file: %v", err)
}
if err := csrfDebugFile.Sync(); err != nil {
return fmt.Errorf("failed to sync CSRF debug file: %v", err)
}
return nil
}
// csrfDebugPrint выводит отладочную информацию
// Если установлен LCG_CSRF_DEBUG_FILE - всегда пишет в файл (независимо от debug режима)
// Если включен debug режим - также пишет в консоль
func csrfDebugPrint(format string, args ...any) {
message := fmt.Sprintf(format, args...)
// Записываем в файл, если он установлен
csrfDebugFileMutex.Lock()
if csrfDebugFile != nil {
csrfDebugFile.WriteString(message)
// Синхронизируем сразу для отладки (может быть медленно, но гарантирует запись)
csrfDebugFile.Sync()
}
csrfDebugFileMutex.Unlock()
// Записываем в консоль, если включен debug режим
if config.AppConfig.MainFlags.Debug { if config.AppConfig.MainFlags.Debug {
fmt.Printf(format, args...) fmt.Print(message)
} }
} }
@@ -336,6 +400,11 @@ var csrfManager *CSRFManager
// InitCSRFManager инициализирует глобальный CSRF менеджер // InitCSRFManager инициализирует глобальный CSRF менеджер
func InitCSRFManager() error { func InitCSRFManager() error {
// Инициализируем файл для отладки CSRF, если указан LCG_CSRF_DEBUG_FILE
if err := initCSRFDebugFile(); err != nil {
return fmt.Errorf("failed to initialize CSRF debug file: %v", err)
}
var err error var err error
csrfManager, err = NewCSRFManager() csrfManager, err = NewCSRFManager()
return err return err

View File

@@ -138,19 +138,39 @@ func CSRFMiddleware(next http.HandlerFunc) http.HandlerFunc {
// Получаем CSRF токен из cookie для сравнения // Получаем CSRF токен из cookie для сравнения
csrfTokenFromCookie := GetCSRFTokenFromCookie(r) csrfTokenFromCookie := GetCSRFTokenFromCookie(r)
valid := true
if csrfTokenFromCookie != "" { if csrfTokenFromCookie != "" {
csrfDebugPrint("[CSRF MIDDLEWARE] CSRF токен из cookie (первые 50 символов): %s...\n", csrfDebugPrint("[CSRF MIDDLEWARE] CSRF токен из cookie (первые 50 символов): %s...\n",
safeSubstring(csrfTokenFromCookie, 0, 50)) safeSubstring(csrfTokenFromCookie, 0, 50))
if csrfTokenFromCookie != csrfToken { if csrfTokenFromCookie != csrfToken {
csrfDebugPrint("[CSRF MIDDLEWARE] ⚠️ ВНИМАНИЕ: Токен из cookie отличается от токена в запросе!\n") csrfDebugPrint("[CSRF MIDDLEWARE] ⚠️ ВНИМАНИЕ: Токен из cookie отличается от токена в запросе!\n")
valid = false
} else { } else {
csrfDebugPrint("[CSRF MIDDLEWARE] ✅ Токен из cookie совпадает с токеном в запросе\n") csrfDebugPrint("[CSRF MIDDLEWARE] ✅ Токен из cookie совпадает с токеном в запросе\n")
valid = true
} }
} else { } else {
csrfDebugPrint("[CSRF MIDDLEWARE] ⚠️ ВНИМАНИЕ: CSRF токен не найден в cookie!\n") csrfDebugPrint("[CSRF MIDDLEWARE] ⚠️ ВНИМАНИЕ: CSRF токен не найден в cookie!\n")
valid = false
} }
// Проверяем CSRF токен // Проверяем CSRF токен
if !valid {
csrfDebugPrint("[CSRF MIDDLEWARE] ❌ ОШИБКА: Валидация CSRF токена не прошла!\n")
// Для 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 OR Empty CSRF token", http.StatusForbidden)
return
}
csrfManager := GetCSRFManager() csrfManager := GetCSRFManager()
if csrfManager == nil { if csrfManager == nil {
csrfDebugPrint("[CSRF MIDDLEWARE] ❌ ОШИБКА: CSRF менеджер не инициализирован!\n") csrfDebugPrint("[CSRF MIDDLEWARE] ❌ ОШИБКА: CSRF менеджер не инициализирован!\n")
@@ -165,7 +185,7 @@ func CSRFMiddleware(next http.HandlerFunc) http.HandlerFunc {
} }
csrfDebugPrint("[CSRF MIDDLEWARE] Вызов ValidateToken с токеном и sessionID: %s\n", sessionID) csrfDebugPrint("[CSRF MIDDLEWARE] Вызов ValidateToken с токеном и sessionID: %s\n", sessionID)
valid := csrfManager.ValidateToken(csrfToken, sessionID) valid = csrfManager.ValidateToken(csrfToken, sessionID)
csrfDebugPrint("[CSRF MIDDLEWARE] Результат ValidateToken: %t\n", valid) csrfDebugPrint("[CSRF MIDDLEWARE] Результат ValidateToken: %t\n", valid)
if !valid { if !valid {