5759c1862e
M0-M7 已完成:核心网关(身份/RBAC/TOTP/OIDC/SAML/Provider/配额/路由/内容策略/审计/定价)+ 资源市场(MCP/Skills/数字员工)。 含 22 个 PostgreSQL 迁移、管理端/门户端前端源码、OpenAPI 契约、部署 compose。 Co-Authored-By: Claude <noreply@anthropic.com>
384 lines
18 KiB
Go
384 lines
18 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"log/slog"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
"time"
|
|
|
|
"aigateway.local/core/internal/apikey"
|
|
"aigateway.local/core/internal/audit"
|
|
"aigateway.local/core/internal/contentpolicy"
|
|
"aigateway.local/core/internal/factcheck"
|
|
"aigateway.local/core/internal/gateway"
|
|
"aigateway.local/core/internal/identity"
|
|
"aigateway.local/core/internal/operations"
|
|
"aigateway.local/core/internal/outbox"
|
|
"aigateway.local/core/internal/platform/cache"
|
|
"aigateway.local/core/internal/platform/config"
|
|
"aigateway.local/core/internal/platform/cryptox"
|
|
"aigateway.local/core/internal/platform/database"
|
|
"aigateway.local/core/internal/platform/health"
|
|
"aigateway.local/core/internal/platform/httpserver"
|
|
"aigateway.local/core/internal/portal"
|
|
"aigateway.local/core/internal/pricing"
|
|
"aigateway.local/core/internal/provider"
|
|
providercontrolplane "aigateway.local/core/internal/provider/controlplane"
|
|
provideropenai "aigateway.local/core/internal/provider/openai"
|
|
providerruntime "aigateway.local/core/internal/provider/runtime"
|
|
"aigateway.local/core/internal/shadow"
|
|
"aigateway.local/core/internal/workbench"
|
|
)
|
|
|
|
var version = "dev"
|
|
|
|
func main() {
|
|
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
logger.Error("invalid configuration", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
if err := cfg.ValidateRuntime(); err != nil {
|
|
logger.Error("invalid runtime configuration", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
|
defer stop()
|
|
|
|
db, err := database.Open(ctx, cfg.Database)
|
|
if err != nil {
|
|
logger.Error("database initialization failed", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
if db != nil {
|
|
defer db.Close()
|
|
}
|
|
criticalRedis, err := cache.Open(cfg.Redis.CriticalURL)
|
|
if err != nil {
|
|
logger.Error("critical redis initialization failed", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
if criticalRedis != nil {
|
|
defer criticalRedis.Close()
|
|
}
|
|
cacheRedis, err := cache.Open(cfg.Redis.CacheURL)
|
|
if err != nil {
|
|
logger.Error("cache redis initialization failed", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
if cacheRedis != nil {
|
|
defer cacheRedis.Close()
|
|
}
|
|
|
|
checker := health.Checker{Timeout: 1500 * time.Millisecond}
|
|
checker.Dependencies = append(checker.Dependencies,
|
|
health.Dependency{Name: "postgres", Required: true, Probe: func(probeCtx context.Context) error {
|
|
if db == nil {
|
|
return errors.New("not configured")
|
|
}
|
|
return db.Ping(probeCtx)
|
|
}},
|
|
health.Dependency{Name: "redis_critical", Required: true, Probe: func(probeCtx context.Context) error {
|
|
if criticalRedis == nil {
|
|
return errors.New("not configured")
|
|
}
|
|
return criticalRedis.Ping(probeCtx).Err()
|
|
}},
|
|
health.Dependency{Name: "redis_cache", Required: false, Probe: func(probeCtx context.Context) error {
|
|
if cacheRedis == nil {
|
|
return errors.New("not configured")
|
|
}
|
|
return cacheRedis.Ping(probeCtx).Err()
|
|
}},
|
|
)
|
|
|
|
adapter, err := provideropenai.New(cfg.Upstream.BaseURL, cfg.Upstream.APIKey)
|
|
if err != nil {
|
|
logger.Error("provider initialization failed", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
var fallbackAdapter provider.Adapter
|
|
if cfg.Upstream.FallbackEnabled {
|
|
fallbackAdapter = adapter
|
|
}
|
|
credentialCipher, err := provider.NewCredentialCipher(
|
|
cfg.Credentials.MasterKey, cfg.Credentials.KEKVersion, cfg.Credentials.KEKKeyring,
|
|
)
|
|
if err != nil {
|
|
logger.Error("credential encryption initialization failed", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
providerRepository := provider.NewRepository(db)
|
|
providerResolver := providerruntime.NewResolver(
|
|
providerRepository, credentialCipher, fallbackAdapter, cfg.Credentials.ProviderRefreshInterval, logger,
|
|
)
|
|
providerResolver.SetNotificationClient(criticalRedis)
|
|
go providerResolver.Run(ctx)
|
|
apiKeyRepository := apikey.NewRepository(db)
|
|
bootstrapAPIKey := ""
|
|
if cfg.Security.BootstrapAPIKeyEnabled {
|
|
bootstrapAPIKey = cfg.Security.BootstrapAPIKey
|
|
logger.Warn("bootstrap API key compatibility is enabled; monitor usage and disable after migration")
|
|
}
|
|
apiKeyAuthenticator := apikey.NewAuthenticator(apiKeyRepository, criticalRedis, bootstrapAPIKey)
|
|
apiKeyAuthenticator.SetLogger(logger)
|
|
proxy := gateway.NewDynamicProxy(providerResolver, apiKeyAuthenticator, cfg.Server.MaxBodyBytes, logger)
|
|
proxy.SetAdmissionController(gateway.NewRedisAdmissionController(criticalRedis))
|
|
proxy.SetTokenQuotaController(gateway.NewRedisTokenQuotaController(criticalRedis))
|
|
proxy.SetResiliencePolicy(gateway.ResiliencePolicy{
|
|
ResponseHeaderTimeout: cfg.Upstream.ResponseHeaderTimeout, MaxRetries: cfg.Upstream.MaxRetries,
|
|
RetryBackoff: cfg.Upstream.RetryBackoff, CircuitThreshold: cfg.Upstream.CircuitThreshold,
|
|
CircuitOpenDuration: cfg.Upstream.CircuitOpenDuration,
|
|
})
|
|
auditRecorder := audit.NewRecorder(db, logger, cfg.Audit.QueueSize, cfg.Audit.BatchSize, cfg.Audit.FlushInterval)
|
|
auditContext, stopAudit := context.WithCancel(context.Background())
|
|
auditStopped := make(chan struct{})
|
|
go func() {
|
|
auditRecorder.Run(auditContext)
|
|
close(auditStopped)
|
|
}()
|
|
proxy.SetAuditRecorder(auditRecorder)
|
|
contentPolicyEngine := contentpolicy.NewEngine(db, cfg.RuntimeData.ContentPolicyRefreshInterval, logger)
|
|
pricingService := pricing.NewService(db, cfg.RuntimeData.PricingRefreshInterval, logger)
|
|
if err := contentPolicyEngine.Reload(ctx); err != nil {
|
|
logger.Error("content policy initialization failed", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
if err := pricingService.Reload(ctx); err != nil {
|
|
logger.Error("model pricing initialization failed", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
go contentPolicyEngine.Run(ctx)
|
|
go pricingService.Run(ctx)
|
|
proxy.SetContentPolicyEngine(contentPolicyEngine)
|
|
proxy.SetPricingService(pricingService)
|
|
identityRepository := identity.NewRepository(db)
|
|
sessionStore := identity.NewSessionStore(criticalRedis, cfg.Auth.SessionTTL)
|
|
loginLimiter := identity.NewLoginLimiter(criticalRedis, cfg.Auth.LoginRateLimitMax, cfg.Auth.LoginRateLimitWindow)
|
|
totpCipher, err := cryptox.NewKeyring(
|
|
cfg.Credentials.MasterKey, cfg.Credentials.KEKVersion, cfg.Credentials.KEKKeyring, "totp-secret",
|
|
)
|
|
if err != nil {
|
|
logger.Error("TOTP encryption initialization failed", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
identityService := identity.NewService(identityRepository, sessionStore, loginLimiter, cfg.Auth, totpCipher)
|
|
idpCipher, err := cryptox.NewKeyring(
|
|
cfg.Credentials.MasterKey, cfg.Credentials.KEKVersion, cfg.Credentials.KEKKeyring, "identity-provider-credentials",
|
|
)
|
|
if err != nil {
|
|
logger.Error("identity provider encryption initialization failed", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
identityService.SetIdentityProviderCipher(idpCipher, cfg.Credentials.AllowPrivateProviderURL)
|
|
identityHandler := identity.NewHTTPHandler(identityService)
|
|
identityManagementHandler := identity.NewManagementHTTPHandler(identityService)
|
|
providerHandler := provider.NewAdminHTTPHandler(
|
|
providerRepository, credentialCipher, identityService, cfg.Credentials.AllowPrivateProviderURL,
|
|
)
|
|
providerOperations := providercontrolplane.NewService(
|
|
providerRepository, credentialCipher, cfg.Credentials.AllowPrivateProviderURL,
|
|
)
|
|
providerHandler.SetOperations(providerOperations)
|
|
providerHandler.SetChangeHook(func(changeCtx context.Context) error {
|
|
reloadErr := providerResolver.Reload(changeCtx)
|
|
notifyErr := providerResolver.Notify(changeCtx)
|
|
if reloadErr != nil || notifyErr != nil {
|
|
logger.Warn("provider change propagation was incomplete", "reload_error", reloadErr, "notify_error", notifyErr)
|
|
}
|
|
return errors.Join(reloadErr, notifyErr)
|
|
})
|
|
apiKeyHandler := apikey.NewAdminHTTPHandler(apiKeyRepository, apiKeyAuthenticator, identityService)
|
|
apiKeyHandler.SetUsageStore(apikey.NewUsageStore(criticalRedis))
|
|
auditHandler := audit.NewAdminHTTPHandler(audit.NewQueryService(db), identityService)
|
|
outboxHandler := outbox.NewAdminHTTPHandler(outbox.NewStore(db), identityService)
|
|
contentPolicyHandler := contentpolicy.NewAdminHTTPHandler(contentpolicy.NewStore(db), contentPolicyEngine, identityService)
|
|
pricingHandler := pricing.NewAdminHTTPHandler(pricingService, identityService)
|
|
factCheckHandler := factcheck.NewAdminHTTPHandler(factcheck.NewService(db), identityService)
|
|
workbenchService := workbench.NewService(db)
|
|
toolCipher, err := cryptox.NewKeyring(
|
|
cfg.Credentials.MasterKey, cfg.Credentials.KEKVersion, cfg.Credentials.KEKKeyring, "tool-request-headers",
|
|
)
|
|
if err != nil {
|
|
logger.Error("tool credential encryption initialization failed", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
notificationCipher, err := cryptox.NewKeyring(
|
|
cfg.Credentials.MasterKey, cfg.Credentials.KEKVersion, cfg.Credentials.KEKKeyring, "notification-signing-secret",
|
|
)
|
|
if err != nil {
|
|
logger.Error("notification encryption initialization failed", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
toolService := workbench.NewToolService(workbenchService, toolCipher, cfg.Credentials.AllowPrivateToolURL)
|
|
notificationService := workbench.NewNotificationService(workbenchService, notificationCipher, cfg.Credentials.AllowPrivateWebhookURL)
|
|
workbenchHandler := workbench.NewAdminHTTPHandler(workbenchService, toolService, notificationService, identityService)
|
|
mcpServerCipher, err := cryptox.NewKeyring(
|
|
cfg.Credentials.MasterKey, cfg.Credentials.KEKVersion, cfg.Credentials.KEKKeyring, "mcp-server-headers",
|
|
)
|
|
if err != nil {
|
|
logger.Error("MCP server credential encryption initialization failed", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
mcpServerService := workbench.NewMCPServerService(workbenchService, mcpServerCipher, cfg.Credentials.AllowPrivateToolURL)
|
|
skillService := workbench.NewSkillService(workbenchService)
|
|
digitalEmployeeService := workbench.NewDigitalEmployeeService(workbenchService, skillService, toolService, mcpServerService)
|
|
marketplaceService := workbench.NewMarketplaceService(workbenchService, mcpServerService, skillService, digitalEmployeeService)
|
|
mcpClient := workbench.NewMCPClient(cfg.Credentials.AllowPrivateToolURL, 60*time.Second)
|
|
marketplaceHandler := workbench.NewMarketplaceAdminHTTPHandler(marketplaceService, mcpServerService, skillService, digitalEmployeeService, mcpClient, identityService)
|
|
applicationKeyCipher, err := cryptox.NewKeyring(
|
|
cfg.Credentials.MasterKey, cfg.Credentials.KEKVersion, cfg.Credentials.KEKKeyring, "application-runtime-key",
|
|
)
|
|
if err != nil {
|
|
logger.Error("application runtime credential initialization failed", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
shadowMiddleware := shadow.New(cfg.Shadow, logger)
|
|
governedGateway := shadowMiddleware.Wrap(proxy)
|
|
workbenchRuntime := workbench.NewRuntimeHTTPHandler(workbenchService, toolService, workbench.NewPostgreSQLRetriever(workbenchService), apiKeyAuthenticator, governedGateway, workbench.MarketplaceDeps{
|
|
MCPServers: mcpServerService,
|
|
Skills: skillService,
|
|
Employees: digitalEmployeeService,
|
|
Market: marketplaceService,
|
|
MCPClient: mcpClient,
|
|
})
|
|
workbenchRuntime.SetLogger(logger)
|
|
// Wire the fact-check engine: the admin fact-check settings/policies UI now
|
|
// actually governs application answers instead of being inert configuration.
|
|
factCheckEngine := factcheck.NewEngine(db, workbench.NewFactCheckRetriever(workbench.NewPostgreSQLRetriever(workbenchService)), logger)
|
|
workbenchRuntime.SetFactCheckEngine(factCheckEngine)
|
|
portalService := portal.NewService(db, workbenchService, toolService, identityService)
|
|
portalService.SetApplicationRuntime(portal.NewRuntimeCredentials(db, apiKeyRepository, applicationKeyCipher), workbenchRuntime)
|
|
portalService.SetMarketplace(marketplaceService)
|
|
portalHandler := portal.NewHTTPHandler(portalService, identityService)
|
|
portalAdminHandler := portal.NewAdminHTTPHandler(portalService, identityService)
|
|
startedAt := time.Now()
|
|
operationsHandler := operations.NewAdminHTTPHandler(db, identityService, version, startedAt, func(reloadCtx context.Context) error {
|
|
return errors.Join(providerResolver.Reload(reloadCtx), contentPolicyEngine.Reload(reloadCtx), pricingService.Reload(reloadCtx))
|
|
})
|
|
controlMux := http.NewServeMux()
|
|
controlMux.Handle("/api/v1/admin/providers", providerHandler)
|
|
controlMux.Handle("/api/v1/admin/providers/", providerHandler)
|
|
controlMux.Handle("/api/v1/admin/model-routes", providerHandler)
|
|
controlMux.Handle("/api/v1/admin/model-routes/", providerHandler)
|
|
controlMux.Handle("/api/v1/admin/api-keys", apiKeyHandler)
|
|
controlMux.Handle("/api/v1/admin/api-keys/", apiKeyHandler)
|
|
controlMux.Handle("/api/v1/admin/audit-events", auditHandler)
|
|
controlMux.Handle("/api/v1/admin/usage/", auditHandler)
|
|
controlMux.Handle("/api/v1/admin/outbox-events", outboxHandler)
|
|
controlMux.Handle("/api/v1/admin/outbox-events/", outboxHandler)
|
|
controlMux.Handle("/api/v1/admin/content-policies", contentPolicyHandler)
|
|
controlMux.Handle("/api/v1/admin/content-policies/", contentPolicyHandler)
|
|
controlMux.Handle("/api/v1/admin/model-prices", pricingHandler)
|
|
controlMux.Handle("/api/v1/admin/model-prices/", pricingHandler)
|
|
controlMux.Handle("/api/v1/admin/fact-check/", factCheckHandler)
|
|
controlMux.Handle("/api/v1/admin/prompt-categories", workbenchHandler)
|
|
controlMux.Handle("/api/v1/admin/prompt-categories/", workbenchHandler)
|
|
controlMux.Handle("/api/v1/admin/prompts", workbenchHandler)
|
|
controlMux.Handle("/api/v1/admin/prompts/", workbenchHandler)
|
|
controlMux.Handle("/api/v1/admin/knowledge-bases", workbenchHandler)
|
|
controlMux.Handle("/api/v1/admin/knowledge-bases/", workbenchHandler)
|
|
controlMux.Handle("/api/v1/admin/tools", workbenchHandler)
|
|
controlMux.Handle("/api/v1/admin/tools/", workbenchHandler)
|
|
controlMux.Handle("/api/v1/admin/applications", workbenchHandler)
|
|
controlMux.Handle("/api/v1/admin/applications/", workbenchHandler)
|
|
controlMux.Handle("/api/v1/admin/marketplace-categories", marketplaceHandler)
|
|
controlMux.Handle("/api/v1/admin/marketplace-categories/", marketplaceHandler)
|
|
controlMux.Handle("/api/v1/admin/mcp-servers", marketplaceHandler)
|
|
controlMux.Handle("/api/v1/admin/mcp-servers/", marketplaceHandler)
|
|
controlMux.Handle("/api/v1/admin/skills", marketplaceHandler)
|
|
controlMux.Handle("/api/v1/admin/skills/", marketplaceHandler)
|
|
controlMux.Handle("/api/v1/admin/digital-employees", marketplaceHandler)
|
|
controlMux.Handle("/api/v1/admin/digital-employees/", marketplaceHandler)
|
|
controlMux.Handle("/api/v1/admin/marketplace/", marketplaceHandler)
|
|
controlMux.Handle("/api/v1/portal/marketplace", portalHandler)
|
|
controlMux.Handle("/api/v1/portal/marketplace/", portalHandler)
|
|
controlMux.Handle("/api/v1/admin/notification-channels", workbenchHandler)
|
|
controlMux.Handle("/api/v1/admin/notification-channels/", workbenchHandler)
|
|
controlMux.Handle("/api/v1/admin/notification-deliveries", workbenchHandler)
|
|
controlMux.Handle("/api/v1/admin/notification-deliveries/", workbenchHandler)
|
|
controlMux.Handle("/api/v1/admin/models", portalAdminHandler)
|
|
controlMux.Handle("/api/v1/admin/model-requests", portalAdminHandler)
|
|
controlMux.Handle("/api/v1/admin/model-requests/", portalAdminHandler)
|
|
controlMux.Handle("/api/v1/admin/system-info", operationsHandler)
|
|
controlMux.Handle("/api/v1/admin/monitoring/overview", operationsHandler)
|
|
controlMux.Handle("/api/v1/admin/reload", operationsHandler)
|
|
controlMux.Handle("/api/v1/admin/identities/", identityManagementHandler)
|
|
controlMux.Handle("/api/v1/admin/departments", identityManagementHandler)
|
|
controlMux.Handle("/api/v1/admin/departments/", identityManagementHandler)
|
|
controlMux.Handle("/api/v1/admin/identity-providers", identityManagementHandler)
|
|
controlMux.Handle("/api/v1/admin/identity-providers/", identityManagementHandler)
|
|
controlMux.Handle("/api/v1/admin/saml-providers", identityManagementHandler)
|
|
controlMux.Handle("/api/v1/admin/saml-providers/", identityManagementHandler)
|
|
controlMux.Handle("/api/v1/portal/applications", portalHandler)
|
|
controlMux.Handle("/api/v1/portal/apps/", portalHandler)
|
|
controlMux.Handle("/api/v1/portal/catalog", portalHandler)
|
|
controlMux.Handle("/api/v1/portal/cost", portalHandler)
|
|
controlMux.Handle("/api/v1/portal/docs-info", portalHandler)
|
|
controlMux.Handle("/api/v1/portal/knowledge", portalHandler)
|
|
controlMux.Handle("/api/v1/portal/logs", portalHandler)
|
|
controlMux.Handle("/api/v1/portal/logs/", portalHandler)
|
|
controlMux.Handle("/api/v1/portal/model-requests", portalHandler)
|
|
controlMux.Handle("/api/v1/portal/model-requests/", portalHandler)
|
|
controlMux.Handle("/api/v1/portal/password", portalHandler)
|
|
controlMux.Handle("/api/v1/portal/prompts", portalHandler)
|
|
controlMux.Handle("/api/v1/portal/prompts/", portalHandler)
|
|
controlMux.Handle("/api/v1/portal/stats", portalHandler)
|
|
controlMux.Handle("/api/v1/portal/tools", portalHandler)
|
|
controlMux.Handle("/api/v1/", identityHandler)
|
|
publicMux := http.NewServeMux()
|
|
publicMux.Handle("/v1/prompts", workbenchRuntime)
|
|
publicMux.Handle("/v1/prompts/", workbenchRuntime)
|
|
publicMux.Handle("/v1/knowledge/", workbenchRuntime)
|
|
publicMux.Handle("/v1/tools", workbenchRuntime)
|
|
publicMux.Handle("/v1/tools/", workbenchRuntime)
|
|
publicMux.Handle("/v1/applications/", workbenchRuntime)
|
|
publicMux.Handle("/v1/skills/", workbenchRuntime)
|
|
publicMux.Handle("/v1/mcp-servers", workbenchRuntime)
|
|
publicMux.Handle("/v1/mcp-servers/", workbenchRuntime)
|
|
publicMux.Handle("/v1/digital-employees/", workbenchRuntime)
|
|
publicMux.Handle("/v1/", governedGateway)
|
|
server := httpserver.New(httpserver.Dependencies{
|
|
Config: cfg, Logger: logger, Checker: checker, Gateway: publicMux, Control: controlMux,
|
|
Version: version, StartedAt: startedAt,
|
|
BootstrapUses: apiKeyAuthenticator.BootstrapUses,
|
|
ExtraMetrics: func() string { return auditRecorder.Prometheus() + shadowMiddleware.Prometheus() },
|
|
})
|
|
|
|
serverErrors := make(chan error, 1)
|
|
go func() {
|
|
logger.Info("gateway API started", "address", server.Addr, "version", version)
|
|
serverErrors <- server.ListenAndServe()
|
|
}()
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
logger.Info("shutdown requested")
|
|
case err := <-serverErrors:
|
|
if !errors.Is(err, http.ErrServerClosed) {
|
|
logger.Error("gateway API stopped unexpectedly", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), cfg.Server.ShutdownTimeout)
|
|
defer cancel()
|
|
if err := server.Shutdown(shutdownCtx); err != nil {
|
|
logger.Error("graceful shutdown failed", "error", err)
|
|
_ = server.Close()
|
|
os.Exit(1)
|
|
}
|
|
stopAudit()
|
|
select {
|
|
case <-auditStopped:
|
|
case <-shutdownCtx.Done():
|
|
logger.Warn("audit recorder drain timed out")
|
|
}
|
|
logger.Info("gateway API stopped")
|
|
}
|