Files

356 lines
16 KiB
Go
Raw Permalink Normal View History

package router
import (
"context"
"os"
"strings"
"time"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"github.com/redis/go-redis/v9"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
"go.uber.org/dig"
"github.com/Tencent/WeKnora/internal/accesscontrol"
"github.com/Tencent/WeKnora/internal/config"
"github.com/Tencent/WeKnora/internal/handler"
"github.com/Tencent/WeKnora/internal/handler/session"
workbenchHandler "github.com/Tencent/WeKnora/internal/handler/workbench"
"github.com/Tencent/WeKnora/internal/logger"
"github.com/Tencent/WeKnora/internal/middleware"
"github.com/Tencent/WeKnora/internal/resumable"
"github.com/Tencent/WeKnora/internal/tracing/langfuse"
"github.com/Tencent/WeKnora/internal/types/interfaces"
_ "github.com/Tencent/WeKnora/docs" // swagger docs
)
// RouterParams 路由参数
type RouterParams struct {
dig.In
Config *config.Config
FileService interfaces.FileService
UserService interfaces.UserService
KBService interfaces.KnowledgeBaseService
KnowledgeService interfaces.KnowledgeService
ChunkService interfaces.ChunkService
SessionService interfaces.SessionService
MessageService interfaces.MessageService
ModelService interfaces.ModelService
EvaluationService interfaces.EvaluationService
KBShareService interfaces.KBShareService
AgentShareService interfaces.AgentShareService
KBHandler *handler.KnowledgeBaseHandler
KnowledgeHandler *handler.KnowledgeHandler
TenantHandler *handler.TenantHandler
TenantService interfaces.TenantService
TenantAPIKeyService interfaces.TenantAPIKeyService
TenantMemberService interfaces.TenantMemberService
TenantMemberHandler *handler.TenantMemberHandler
TenantInvitationHandler *handler.TenantInvitationHandler
AuditLogHandler *handler.AuditLogHandler
AuditLogService interfaces.AuditLogService
ChunkHandler *handler.ChunkHandler
SessionHandler *session.Handler
MessageHandler *handler.MessageHandler
MessageSuggestionHandler *handler.MessageSuggestionHandler
ModelHandler *handler.ModelHandler
ModelCredentialsHandler *handler.ModelCredentialsHandler
SandboxConfigHandler *handler.SandboxConfigHandler
SandboxSkillHandler *handler.SandboxSkillHandler
MeEnvVarHandler *handler.MeEnvVarHandler
EvaluationHandler *handler.EvaluationHandler
AuthHandler *handler.AuthHandler
InitializationHandler *handler.InitializationHandler
SystemHandler *handler.SystemHandler
MCPServiceHandler *handler.MCPServiceHandler
MCPCredentialsHandler *handler.MCPCredentialsHandler
MCPOAuthHandler *handler.MCPOAuthHandler
WebSearchHandler *handler.WebSearchHandler
WebSearchProviderHandler *handler.WebSearchProviderHandler
WebSearchCredentialsHandler *handler.WebSearchProviderCredentialsHandler
VectorStoreHandler *handler.VectorStoreHandler
StorageBackendHandler *handler.StorageBackendHandler
StorageBackendResolver interfaces.StorageBackendResolver
ResourceCatalog interfaces.ResourceCatalog
FAQHandler *handler.FAQHandler
TagHandler *handler.TagHandler
CustomAgentHandler *handler.CustomAgentHandler
UserFavoriteHandler *handler.UserResourceFavoriteHandler
SkillHandler *handler.SkillHandler
OrganizationHandler *handler.OrganizationHandler
IMHandler *handler.IMHandler
EmbedChannelHandler *handler.EmbedChannelHandler
EmbedChannelService interfaces.EmbedChannelService
RedisClient *redis.Client
DataSourceHandler *handler.DataSourceHandler
DataSourceCredentialsHandler *handler.DataSourceCredentialsHandler
WeKnoraCloudHandler *handler.WeKnoraCloudHandler
WikiPageHandler *handler.WikiPageHandler
MemoryHandler *handler.MemoryHandler
DigitalHumanHandler *handler.DigitalHumanHandler
WorkbenchHandler *workbenchHandler.Handler
NativeGraphHandler *handler.NativeGraphHandler
KnowledgeClassificationHandler *handler.KnowledgeClassificationHandler
AccessControl *accesscontrol.Service
Uploads *resumable.Service
}
// NewRouter 创建新的路由
func NewRouter(params RouterParams) *gin.Engine {
r := gin.New()
r.ContextWithFallback = true
// Trusted proxies: gin defaults to trusting ALL proxies, which makes
// c.ClientIP() honor a client-supplied X-Forwarded-For. Public, unauthed
// embed endpoints rate-limit per (channel, ClientIP), so a spoofed XFF would
// trivially bypass the limiter. Restrict to the fronting proxy network so
// only the real client IP (appended by nginx) is returned. Configurable via
// WEKNORA_TRUSTED_PROXIES (comma-separated CIDRs/IPs).
if err := r.SetTrustedProxies(trustedProxies()); err != nil {
logger.Errorf(context.Background(), "[Router] failed to set trusted proxies: %v", err)
}
// CORS 中间件应放在最前面。
// 注意:通配符 AllowOrigins 下浏览器会拒绝一切带凭据(cookie)的跨域
// 请求(CORS 规范禁止 "*" 与 credentials 组合),因此 AllowCredentials
// 实际只对未来改为回显具体 Origin 时才生效;当前认证全部走显式的
// Authorization / X-API-Key 头,不依赖 ambient 凭据。若引入 cookie
// 认证,必须先把 AllowOrigins 换成受控清单。
r.Use(cors.New(cors.Config{
AllowOrigins: []string{"*"},
AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization", "X-API-Key", "X-Request-ID", "X-Tenant-ID", "X-Embed-Session", "X-External-User-ID", "X-External-User-Token", "Upload-Offset", "Upload-Checksum"},
ExposeHeaders: []string{"Content-Length", "Access-Control-Allow-Origin"},
AllowCredentials: true,
MaxAge: 12 * time.Hour,
}))
// 基础中间件(不需要认证)
r.Use(middleware.RequestID())
r.Use(middleware.Language())
r.Use(middleware.Logger())
r.Use(middleware.Recovery())
r.Use(middleware.ErrorHandler())
// 健康检查(不需要认证)
r.GET("/health", func(c *gin.Context) {
c.JSON(200, gin.H{"status": "ok"})
})
// Swagger API 文档(仅在非生产环境下启用)
// 通过 GIN_MODE 环境变量判断:release 模式下禁用 Swagger
if gin.Mode() != gin.ReleaseMode {
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler,
ginSwagger.DefaultModelsExpandDepth(-1), // 默认折叠 Models
ginSwagger.DocExpansion("list"), // 展开模式: "list"(展开标签), "full"(全部展开), "none"(全部折叠)
ginSwagger.DeepLinking(true), // 启用深度链接
ginSwagger.PersistAuthorization(true), // 持久化认证信息
))
}
// Embed page framing policy: emit a per-channel `frame-ancestors` CSP so the
// embed SPA page (/embed/:channelId) can only be iframed by the channel's
// allowed origins. This is the page-level counterpart to the API Origin
// allowlist enforced in EmbedAuth. Registered before the static handler so
// it runs for the embed HTML response.
if params.EmbedChannelService != nil {
r.Use(embedFrameAncestorsMiddleware(params.EmbedChannelService))
}
// 前端静态文件(仅 Lite 版本内嵌前端)
if handler.Edition == "lite" {
serveFrontendStatic(r)
}
// IM 回调路由(在认证中间件之前注册,使用各平台自身的签名验证)
RegisterIMRoutes(r, params.IMHandler)
// Web embed 公开路由(使用 publish token 鉴权,不走全局 Auth)
RegisterEmbedPublicRoutes(
r,
params.EmbedChannelHandler,
params.EmbedChannelService,
params.TenantService,
params.RedisClient,
params.FileService,
params.StorageBackendResolver,
params.ResourceCatalog,
)
// Short-lived capability URLs for IM and other clients that cannot attach
// WeKnora authentication headers.
serveResourceGrants(r, params.ResourceCatalog, params.TenantService, params.FileService, params.StorageBackendResolver)
// 认证中间件
r.Use(middleware.Auth(params.TenantService, params.UserService, params.TenantMemberService, params.TenantAPIKeyService, params.Config))
// 文件服务:统一代理本地/MinIO/COS/TOS存储后端(需要认证)
if params.AccessControl != nil {
serveFilesWithResources(r.Group("", params.AccessControl.Middleware()), params.FileService, params.StorageBackendResolver, params.ResourceCatalog)
} else {
serveFilesWithResources(r, params.FileService, params.StorageBackendResolver, params.ResourceCatalog)
}
// Presigned file access: no auth required, signature-verified.
servePresignedFiles(r, params.TenantService, params.StorageBackendResolver)
// Diagnostic preview of presigned URLs (Admin only, behind auth middleware).
servePresignedPreview(r, params.Config, params.StorageBackendResolver)
// Langfuse observability — only active when LANGFUSE_* env vars are set.
// The middleware is registered unconditionally; when disabled it's a no-op.
r.Use(langfuse.GinMiddleware())
// Audit log injection — middleware/rbac.go's reject paths and the
// admin-only /tenants/:id/audit-log endpoint pull the service out
// of the gin context. Provider is a no-op when AuditLogService is
// nil (e.g. lite mode without DB), so the rbac path degrades to
// "log to stderr only" instead of crashing.
r.Use(middleware.AuditServiceProvider(params.AuditLogService))
// 需要认证的API路由
v1 := r.Group("/api/v1")
{
// rbacGuards bundles the role-gating middleware factories so each
// Register* function below can attach the right guard without
// taking a *config.Config dependency directly. The guards honour
// cfg.Tenant.EnableRBAC: when false, they log but pass through,
// preserving today's behaviour during the rollout window.
rbacGuards := newRBACGuards(
params.Config,
params.KBHandler,
params.CustomAgentHandler,
params.KnowledgeHandler,
params.ChunkHandler,
params.WikiPageHandler,
params.KBService,
params.KnowledgeService,
params.ChunkService,
params.KBShareService,
params.AgentShareService,
)
// API-key gate: single authority for X-API-Key principals. Runs
// first on every /api/v1 route (JWT sessions pass straight
// through) and denies any route not explicitly declared via the
// apiKeyGroup helpers. Must be attached BEFORE the Register* calls
// so that sub-groups inherit it.
v1.Use(rbacGuards.apiKeyAuthorizer.Middleware())
if params.WorkbenchHandler != nil {
v1.Use(params.WorkbenchHandler.OperationAudit())
}
if params.AccessControl != nil {
v1.Use(params.AccessControl.Middleware())
params.AccessControl.Register(v1)
params.AccessControl.RegisterResourceVersions(v1)
}
RegisterAuthRoutes(v1, params.AuthHandler, rbacGuards)
RegisterTenantRoutes(v1, params.TenantHandler, params.TenantMemberHandler, params.TenantInvitationHandler, params.AuditLogHandler, rbacGuards)
RegisterMyInvitationRoutes(v1, params.TenantInvitationHandler)
RegisterKnowledgeBaseRoutes(v1, params.KBHandler, rbacGuards)
RegisterKnowledgeBaseActivityRoutes(v1, params.AuditLogHandler, rbacGuards)
// KB-scoped image proxy: lets tenants render images embedded in
// org-shared / agent-visible KB content, which the tenant-scoped
// /files route cannot serve because it enforces same-tenant paths.
serveKBScopedFiles(
v1,
rbacGuards,
params.TenantService,
params.FileService,
params.StorageBackendResolver,
params.ResourceCatalog,
)
// Message-scoped image proxy: shared-agent replies belong to the
// caller's session but may reference resources stored in the agent's
// source workspace. Authorization is derived from the persisted message,
// never from a client-provided workspace ID.
serveMessageScopedFiles(
v1,
rbacGuards,
params.MessageService,
params.AgentShareService,
params.TenantService,
params.FileService,
params.StorageBackendResolver,
params.ResourceCatalog,
)
RegisterKnowledgeTagRoutes(v1, params.TagHandler, rbacGuards)
RegisterKnowledgeRoutes(v1, params.KnowledgeHandler, rbacGuards)
RegisterFAQRoutes(v1, params.FAQHandler, rbacGuards)
RegisterChunkRoutes(v1, params.ChunkHandler, rbacGuards)
RegisterSessionRoutes(v1, params.SessionHandler, params.MessageSuggestionHandler, rbacGuards)
RegisterChatRoutes(v1, params.SessionHandler, rbacGuards)
RegisterMessageRoutes(v1, params.MessageHandler, rbacGuards)
RegisterModelRoutes(v1, params.ModelHandler, params.ModelCredentialsHandler, rbacGuards)
RegisterSandboxConfigRoutes(v1, params.SandboxConfigHandler, params.SandboxSkillHandler, rbacGuards)
RegisterMyEnvVarRoutes(v1, params.MeEnvVarHandler)
RegisterEvaluationRoutes(v1, params.EvaluationHandler, rbacGuards)
RegisterInitializationRoutes(v1, params.InitializationHandler, rbacGuards)
params.SystemHandler.BindDeploymentCapabilities(deploymentCapabilitiesFromRouter(params))
RegisterSystemRoutes(v1, params.SystemHandler, rbacGuards)
RegisterSystemAdminRoutes(v1, params.SystemHandler, params.AuditLogHandler, rbacGuards)
RegisterMCPServiceRoutes(v1, params.MCPServiceHandler, params.MCPCredentialsHandler, params.MCPOAuthHandler, rbacGuards)
RegisterWebSearchRoutes(v1, params.WebSearchHandler, rbacGuards)
RegisterWebSearchProviderRoutes(v1, params.WebSearchProviderHandler, params.WebSearchCredentialsHandler, rbacGuards)
RegisterVectorStoreRoutes(v1, params.VectorStoreHandler, rbacGuards)
RegisterStorageBackendRoutes(v1, params.StorageBackendHandler, rbacGuards)
RegisterCustomAgentRoutes(v1, params.CustomAgentHandler, rbacGuards)
RegisterUserFavoriteRoutes(v1, params.UserFavoriteHandler, rbacGuards)
RegisterSkillRoutes(v1, params.SkillHandler, rbacGuards)
RegisterOrganizationRoutes(v1, params.OrganizationHandler, rbacGuards)
RegisterIMChannelRoutes(v1, params.IMHandler, rbacGuards)
RegisterEmbedChannelRoutes(v1, params.EmbedChannelHandler, rbacGuards)
RegisterDataSourceRoutes(v1, params.DataSourceHandler, params.DataSourceCredentialsHandler, rbacGuards)
RegisterWeKnoraCloudRoutes(v1, params.WeKnoraCloudHandler, rbacGuards)
RegisterWikiPageRoutes(v1, params.WikiPageHandler, rbacGuards)
RegisterMemoryRoutes(v1, params.MemoryHandler, rbacGuards)
RegisterWorkbenchRoutes(v1, params.WorkbenchHandler, rbacGuards)
if params.Uploads != nil {
params.Uploads.Register(v1)
}
RegisterNativeGraphRoutes(v1, params.NativeGraphHandler, rbacGuards)
RegisterKnowledgeClassificationRoutes(v1, params.KnowledgeClassificationHandler, rbacGuards)
RegisterDigitalHumanRoutes(v1, params.DigitalHumanHandler, rbacGuards)
RegisterChunkerDebugRoutes(v1, rbacGuards)
// Fail fast if any declared API-key policy points at a route
// template that does not actually exist (typo / path drift). A
// stale template would silently 403 every API key on that route,
// so we panic at startup instead of shipping a dead policy.
rbacGuards.assertAPIKeyPoliciesMatchRoutes(r)
}
return r
}
// trustedProxies returns the proxy CIDRs/IPs whose X-Forwarded-For headers
// gin should trust when resolving the client IP. Defaults to loopback and
// private ranges (covers the bundled nginx in a container network); override
// with WEKNORA_TRUSTED_PROXIES (comma-separated). An explicit empty value
// disables proxy trust entirely so ClientIP() returns the direct peer.
func trustedProxies() []string {
raw, ok := os.LookupEnv("WEKNORA_TRUSTED_PROXIES")
if !ok {
return []string{
"127.0.0.0/8",
"::1/128",
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
"fc00::/7",
}
}
proxies := make([]string, 0)
for _, p := range strings.Split(raw, ",") {
if p = strings.TrimSpace(p); p != "" {
proxies = append(proxies, p)
}
}
return proxies
}