Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
123 lines
5.9 KiB
Go
123 lines
5.9 KiB
Go
package router
|
|
|
|
import (
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/Tencent/WeKnora/internal/handler"
|
|
"github.com/Tencent/WeKnora/internal/handler/session"
|
|
)
|
|
|
|
// RegisterMessageRoutes 注册消息相关的路由。
|
|
//
|
|
// Per-session ownership is already enforced inside each handler (the
|
|
// user must own the session). We add Viewer+ here so non-members
|
|
// (e.g. revoked accounts retained in the tenant for audit) cannot
|
|
// reach the endpoints at all once RBAC is on.
|
|
func RegisterMessageRoutes(r *gin.RouterGroup, handler *handler.MessageHandler, g *rbacGuards) {
|
|
// Message history is tenant-wide and not attributable to a KB, so it is
|
|
// a full-access surface for API keys by default. The narrow
|
|
// exceptions are explicit capabilities:
|
|
// - chat: load/delete messages inside the caller's own session, where
|
|
// ownership is enforced by the message service.
|
|
// - message_history: search/read tenant chat-history metadata without
|
|
// granting every other full-access API.
|
|
messages := g.apiKeyGroup(r.Group("/messages"), apiKeyFullAccess())
|
|
chatMessages := messages.With(apiKeyChat(apiKeyFullAccess()))
|
|
historyMessages := messages.With(apiKeyMessageHistory(apiKeyFullAccess()))
|
|
{
|
|
historyMessages.POST("/search", g.Viewer(), handler.SearchMessages)
|
|
historyMessages.GET("/chat-history-stats", g.Viewer(), handler.GetChatHistoryKBStats)
|
|
chatMessages.GET("/:session_id/load", g.Viewer(), handler.LoadMessages)
|
|
chatMessages.DELETE("/:session_id/:id", g.Viewer(), handler.DeleteMessage)
|
|
}
|
|
}
|
|
|
|
// RegisterSessionRoutes 注册路由。
|
|
//
|
|
// Sessions are per-user resources; the handler enforces user ownership.
|
|
// We gate at Viewer+ to keep non-members out once RBAC is on, matching
|
|
// the message routes above. A future refactor can introduce
|
|
// per-session ownership in the middleware layer the same way KB/agent
|
|
// routes do today.
|
|
func RegisterSessionRoutes(
|
|
r *gin.RouterGroup,
|
|
handler *session.Handler,
|
|
suggestionHandler *handler.MessageSuggestionHandler,
|
|
g *rbacGuards,
|
|
) {
|
|
// Sessions are per-user chat state, not knowledge-base content. The
|
|
// chat capability lets a scoped key run the full conversation flow
|
|
// (create/manage its own sessions) without full tenant access.
|
|
sessions := g.apiKeyGroup(r.Group("/sessions", g.Viewer()), apiKeyChat(apiKeyFullAccess()))
|
|
{
|
|
sessions.POST("", handler.CreateSession)
|
|
sessions.DELETE("/batch", handler.BatchDeleteSessions)
|
|
sessions.GET("/:id", handler.GetSession)
|
|
sessions.GET("", handler.GetSessionsByTenant)
|
|
sessions.PUT("/:id", handler.UpdateSession)
|
|
sessions.DELETE("/:id", handler.DeleteSession)
|
|
sessions.DELETE("/:id/messages", handler.ClearSessionMessages)
|
|
sessions.POST("/:session_id/generate_title", handler.GenerateTitle)
|
|
sessions.POST("/:session_id/attachments", handler.UploadTemporaryDocument)
|
|
sessions.GET("/:id/attachments", handler.ListTemporaryDocuments)
|
|
sessions.GET("/:id/attachments/:attachment_id", handler.GetTemporaryDocument)
|
|
sessions.GET("/:id/attachments/:attachment_id/preview", handler.PreviewTemporaryDocument)
|
|
sessions.DELETE("/:id/attachments/:attachment_id", handler.DeleteTemporaryDocument)
|
|
sessions.POST("/:session_id/stop", handler.StopSession)
|
|
// POST and DELETE share this path but gin maintains a separate radix tree
|
|
// per HTTP verb, and the existing trees use different wildcard names
|
|
// (POST uses :session_id, DELETE uses :id). Use whatever matches each
|
|
// tree to avoid "wildcard conflicts" panic at route registration.
|
|
sessions.POST("/:session_id/pin", handler.PinSession)
|
|
sessions.DELETE("/:id/pin", handler.UnpinSession)
|
|
// 继续接收活跃流
|
|
sessions.GET("/continue-stream/:session_id", handler.ContinueStream)
|
|
if suggestionHandler != nil {
|
|
// Gin requires wildcard names to be identical within the same HTTP-method
|
|
// radix tree. Existing GET session routes use :id, so keep that name here.
|
|
sessions.GET("/:id/messages/:message_id/suggestions", suggestionHandler.Get)
|
|
sessions.POST("/:session_id/messages/:message_id/suggestions", suggestionHandler.Ensure)
|
|
sessions.POST("/:session_id/suggestion-events", suggestionHandler.RecordEvent)
|
|
}
|
|
|
|
// Skill-generated file artifacts. The list endpoints only expose
|
|
// metadata; the actual bytes are streamed via /artifacts/:index/download
|
|
// so the storage URL never appears on the wire.
|
|
//
|
|
// NOTE: gin builds a separate radix tree per HTTP verb but every
|
|
// path in the same tree must share the same wildcard name. The GET
|
|
// tree already binds :id via /sessions/:id (GetSession); reusing
|
|
// :id here (instead of :session_id) avoids the
|
|
// "wildcard conflicts" panic at route registration. The handlers
|
|
// read the URL param via c.Param("session_id") with a fallback to
|
|
// c.Param("id") for exactly this reason.
|
|
sessions.GET("/:id/artifacts", handler.ListSessionArtifacts)
|
|
sessions.GET("/:id/messages/:message_id/artifacts", handler.ListMessageArtifacts)
|
|
sessions.GET("/:id/messages/:message_id/artifacts/:index/download", handler.DownloadMessageArtifact)
|
|
}
|
|
}
|
|
|
|
// RegisterChatRoutes 注册路由。Chat endpoints are tenant-member usage
|
|
// surfaces; Viewer+ is sufficient because per-session/per-agent
|
|
// authorisation is enforced inside the handlers.
|
|
func RegisterChatRoutes(r *gin.RouterGroup, handler *session.Handler, g *rbacGuards) {
|
|
// These POST routes append messages and run generation, so a scoped key
|
|
// needs the explicit chat capability unless it has full tenant access.
|
|
knowledgeChat := g.apiKeyGroup(r.Group("/knowledge-chat", g.Viewer()), apiKeyChat(apiKeyFullAccess()))
|
|
{
|
|
knowledgeChat.POST("/:session_id", handler.KnowledgeQA)
|
|
}
|
|
|
|
// Agent-based chat
|
|
agentChat := g.apiKeyGroup(r.Group("/agent-chat", g.Viewer()), apiKeyChat(apiKeyFullAccess()))
|
|
{
|
|
agentChat.POST("/:session_id", handler.AgentQA)
|
|
}
|
|
|
|
// 新增知识检索接口,不需要session_id
|
|
knowledgeSearch := g.apiKeyGroup(r.Group("/knowledge-search", g.Viewer()), apiKeyRetrieve(apiKeyFullAccess()))
|
|
{
|
|
knowledgeSearch.POST("", handler.SearchKnowledge)
|
|
}
|
|
}
|