Files
knowledge-governance/cmd/api/main.go
T
姚章浩 021e91eae4 feat(settings): bind internal/external knowledge bases from the UI
Routing targets move from env-only to a UI-managed binding (migration
000034 routing_kb_bindings, single active row):
- GET/PUT /api/v1/routing-bindings + /options: env defaults act as the
  bootstrap fallback; a UI binding wins and takes effect immediately
  (per-request resolution, no restart), and clearing falls back to env
- PUT validates against the connected WeKnora instance (real knowledge
  base list via the scoped API key) and rejects same-KB bindings;
  saves require the admin role
- route/catalog capabilities now resolve through the binding so both
  annotation paths share one source of truth
- system settings > 服务配置 gains a 知识库绑定 panel: dropdown picker
  (or raw ID when WeKnora is unreachable) showing bound name, origin
  (UI vs env default) and last author
- contract tests: OpenAPI, handler matrix (fallback, validation,
  same-KB rejection, capability resolution), frontend fetch/save layer
2026-09-30 10:54:07 +08:00

122 lines
4.2 KiB
Go

package main
import (
"context"
"errors"
"log"
"net/http"
"os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/jackc/pgx/v5/pgxpool"
"knowledge-governance/internal/appconfig"
"knowledge-governance/internal/httpapi"
"knowledge-governance/internal/monitoring"
postgresstore "knowledge-governance/internal/platform/postgres"
"knowledge-governance/internal/platform/weknora"
)
func main() {
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
if err := run(ctx, os.Getenv); err != nil {
log.Fatal(err)
}
}
func run(ctx context.Context, getenv func(string) string) error {
config, err := appconfig.Load(getenv)
if err != nil {
return err
}
pool, err := pgxpool.New(ctx, config.DatabaseURL)
if err != nil {
return err
}
defer pool.Close()
if err := pool.Ping(ctx); err != nil {
return err
}
if err := postgresstore.ApplyMigrations(ctx, pool); err != nil {
return err
}
authStore := postgresstore.NewStore(pool)
if err := bootstrapInitialAdmin(ctx, authStore, time.Now); err != nil {
return err
}
releaseAuthorizationToken := ""
if config.ProductionReleaseRequestsEnabled {
releaseAuthorizationToken, err = weknora.ReadAPIKeyFile(config.ProductionReleaseAuthTokenFile)
if err != nil {
return errors.New("read production release authorization token")
}
}
// P10: with a mounted API key the probe verifies a real knowledge-base read
// (readiness); without one it degrades to the liveness-only HTTP check.
var weknoraReadiness func(context.Context) error
var knowledgeBaseLister httpapi.KnowledgeBaseLister
if strings.TrimSpace(config.TestWeKnoraBaseURL) != "" {
if apiKey, keyErr := weknora.ReadAPIKeyFile(config.TestWeKnoraAPIKeyFile); keyErr == nil {
probeTimeout := config.TestWeKnoraHTTPTimeout
if probeTimeout > 10*time.Second || probeTimeout <= 0 {
probeTimeout = 10 * time.Second
}
if probeClient, clientErr := weknora.NewClient(weknora.ClientConfig{BaseURL: config.TestWeKnoraBaseURL, APIKey: apiKey, Timeout: probeTimeout, AllowDockerHTTP: config.TestWeKnoraAllowDockerHTTP}); clientErr == nil {
knowledgeBaseLister = probeClient
routingKB := config.InternalRoutingKnowledgeBaseID
weknoraReadiness = func(probeCtx context.Context) error {
_, _, err := probeClient.ListKnowledge(probeCtx, routingKB, 1, 1)
return err
}
}
}
}
monitorProvider, err := monitoring.NewRealProvider(monitoring.RealProviderConfig{DB: pool, WeKnoraBaseURL: config.TestWeKnoraBaseURL, MinerUBaseURL: config.MinerUBaseURL, WeKnoraReadiness: weknoraReadiness, WorkerStaleAfter: config.WorkerLivenessStaleAfter})
if err != nil {
return err
}
handler, err := httpapi.NewHandler(httpapi.Config{
Store: postgresstore.NewStore(pool),
Monitor: monitorProvider,
InternalRoutingKnowledgeBaseID: config.InternalRoutingKnowledgeBaseID,
ExternalRoutingKnowledgeBaseID: config.ExternalRoutingKnowledgeBaseID,
RoutingBindings: postgresstore.NewStore(pool),
KnowledgeBases: knowledgeBaseLister,
ProductionReleaseRequestsEnabled: config.ProductionReleaseRequestsEnabled,
ProductionBackupRestoreVerified: config.ProductionBackupRestoreVerified,
ProductionReleaseAuthorizerRef: config.ProductionReleaseAuthorizerRef,
ProductionReleaseAuthorizationToken: releaseAuthorizationToken,
Auth: &httpapi.AuthConfig{Store: authStore, SessionTTL: 12 * time.Hour},
})
if err != nil {
return err
}
server := &http.Server{
Addr: config.AppAddr,
Handler: handler,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 15 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second,
}
errCh := make(chan error, 1)
go func() { errCh <- server.ListenAndServe() }()
log.Printf("governance API listening on %s", config.AppAddr)
select {
case err := <-errCh:
if errors.Is(err, http.ErrServerClosed) {
return nil
}
return err
case <-ctx.Done():
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
return server.Shutdown(shutdownCtx)
}
}