This commit is contained in:
+83
-9
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
@@ -10,20 +11,25 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"kb-editor/internal/aifallback"
|
||||
"kb-editor/internal/store"
|
||||
)
|
||||
|
||||
type appConfig struct {
|
||||
Mode string `json:"mode"`
|
||||
Title string `json:"title"`
|
||||
Subtitle string `json:"subtitle"`
|
||||
Writable bool `json:"writable"`
|
||||
Mode string `json:"mode"`
|
||||
Title string `json:"title"`
|
||||
Subtitle string `json:"subtitle"`
|
||||
Writable bool `json:"writable"`
|
||||
AIFallbackEnabled bool `json:"ai_fallback_enabled"`
|
||||
AIFallbackTimeoutSeconds int `json:"ai_fallback_timeout_seconds,omitempty"`
|
||||
AIFallbackModel string `json:"ai_fallback_model,omitempty"`
|
||||
}
|
||||
|
||||
type app struct {
|
||||
store *store.Store
|
||||
web fs.FS
|
||||
config appConfig
|
||||
ai *aifallback.Service
|
||||
}
|
||||
|
||||
func newApp(s *store.Store, web fs.FS, configs ...appConfig) *app {
|
||||
@@ -34,6 +40,11 @@ func newApp(s *store.Store, web fs.FS, configs ...appConfig) *app {
|
||||
return &app{store: s, web: web, config: cfg}
|
||||
}
|
||||
|
||||
func (a *app) withAI(service *aifallback.Service) *app {
|
||||
a.ai = service
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *app) routes() http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("GET /api/health", a.handleHealth)
|
||||
@@ -42,6 +53,8 @@ func (a *app) routes() http.Handler {
|
||||
mux.HandleFunc("GET /api/search", a.handleSearch)
|
||||
mux.HandleFunc("GET /api/facets", a.handleFacets)
|
||||
mux.HandleFunc("GET /api/items/{key}", a.handleGet)
|
||||
mux.HandleFunc("POST /api/ai/fallback", a.handleAIFallback)
|
||||
mux.HandleFunc("GET /api/staging/{key}", a.handleStagingGet)
|
||||
|
||||
if a.config.Writable {
|
||||
mux.HandleFunc("PUT /api/items/{key}", a.handlePut)
|
||||
@@ -71,11 +84,12 @@ func securityHeaders(next http.Handler) http.Handler {
|
||||
|
||||
func (a *app) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
payload := map[string]any{
|
||||
"ok": true,
|
||||
"count": a.store.Count(),
|
||||
"data_dir": a.store.DataDir(),
|
||||
"mode": a.config.Mode,
|
||||
"writable": a.config.Writable,
|
||||
"ok": true,
|
||||
"count": a.store.Count(),
|
||||
"data_dir": a.store.DataDir(),
|
||||
"mode": a.config.Mode,
|
||||
"writable": a.config.Writable,
|
||||
"ai_fallback_enabled": a.config.AIFallbackEnabled && a.ai != nil,
|
||||
}
|
||||
if a.config.Writable {
|
||||
payload["backup_dir"] = a.store.BackupDir()
|
||||
@@ -130,6 +144,66 @@ func (a *app) handleGet(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]any{"document": doc, "meta": meta})
|
||||
}
|
||||
|
||||
type aiFallbackRequest struct {
|
||||
Query string `json:"query"`
|
||||
}
|
||||
|
||||
func (a *app) handleAIFallback(w http.ResponseWriter, r *http.Request) {
|
||||
if !a.config.AIFallbackEnabled || a.ai == nil {
|
||||
writeError(w, http.StatusNotFound, "KI-Fallback ist auf dieser Instanz deaktiviert")
|
||||
return
|
||||
}
|
||||
if !mustJSONContentType(w, r) {
|
||||
return
|
||||
}
|
||||
var req aiFallbackRequest
|
||||
if err := decodeJSON(r, &req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Ungültige Anfrage: "+err.Error())
|
||||
return
|
||||
}
|
||||
query := strings.TrimSpace(req.Query)
|
||||
if len([]rune(query)) < 3 {
|
||||
writeError(w, http.StatusBadRequest, "Suchanfrage ist für den KI-Fallback zu kurz")
|
||||
return
|
||||
}
|
||||
// Server-side guard: AI generation is only permitted when the regular KB has zero hits.
|
||||
check := a.store.Search(store.Query{Q: query, Page: 1, PageSize: 1})
|
||||
if check.Total > 0 {
|
||||
writeJSON(w, http.StatusConflict, map[string]any{
|
||||
"error": "Die Wissensbasis enthält inzwischen passende Treffer; KI-Fallback wurde nicht gestartet",
|
||||
"total": check.Total,
|
||||
})
|
||||
return
|
||||
}
|
||||
result, err := a.ai.Generate(r.Context(), query)
|
||||
if err != nil {
|
||||
if errors.Is(err, context.DeadlineExceeded) || errors.Is(r.Context().Err(), context.DeadlineExceeded) {
|
||||
writeError(w, http.StatusGatewayTimeout, "KI-Fallback hat das Zeitlimit überschritten")
|
||||
return
|
||||
}
|
||||
writeError(w, http.StatusBadGateway, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, result)
|
||||
}
|
||||
|
||||
func (a *app) handleStagingGet(w http.ResponseWriter, r *http.Request) {
|
||||
if !a.config.AIFallbackEnabled || a.ai == nil {
|
||||
writeError(w, http.StatusNotFound, "Staging-Viewer ist auf dieser Instanz deaktiviert")
|
||||
return
|
||||
}
|
||||
result, err := a.ai.GetStaging(r.PathValue("key"))
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
writeError(w, http.StatusNotFound, "Staging-Eintrag nicht gefunden")
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, result)
|
||||
}
|
||||
|
||||
func (a *app) handleReadOnly(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusForbidden, "Diese Instanz läuft im Google-/Viewer-Modus und ist schreibgeschützt")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user