681 lines
22 KiB
Go
681 lines
22 KiB
Go
package handler
|
|
|
|
import (
|
|
stderrors "errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/Tencent/WeKnora/internal/application/repository"
|
|
"github.com/Tencent/WeKnora/internal/application/service"
|
|
"github.com/Tencent/WeKnora/internal/errors"
|
|
"github.com/Tencent/WeKnora/internal/logger"
|
|
"github.com/Tencent/WeKnora/internal/types"
|
|
"github.com/Tencent/WeKnora/internal/types/interfaces"
|
|
secutils "github.com/Tencent/WeKnora/internal/utils"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// WikiPageHandler handles HTTP requests for wiki page operations
|
|
type WikiPageHandler struct {
|
|
wikiService interfaces.WikiPageService
|
|
kbService interfaces.KnowledgeBaseService
|
|
lintService *service.WikiLintService
|
|
logEntryService interfaces.WikiLogEntryService
|
|
}
|
|
|
|
// NewWikiPageHandler creates a new wiki page handler
|
|
func NewWikiPageHandler(
|
|
wikiService interfaces.WikiPageService,
|
|
kbService interfaces.KnowledgeBaseService,
|
|
lintService *service.WikiLintService,
|
|
logEntryService interfaces.WikiLogEntryService,
|
|
) *WikiPageHandler {
|
|
return &WikiPageHandler{
|
|
wikiService: wikiService,
|
|
kbService: kbService,
|
|
lintService: lintService,
|
|
logEntryService: logEntryService,
|
|
}
|
|
}
|
|
|
|
// validateWikiKB validates that the KB exists and is a wiki type
|
|
func (h *WikiPageHandler) validateWikiKB(c *gin.Context) (string, uint64, error) {
|
|
ctx := c.Request.Context()
|
|
kbID := secutils.SanitizeForLog(c.Param("kb_id"))
|
|
tenantID := c.GetUint64(types.TenantIDContextKey.String())
|
|
|
|
if kbID == "" {
|
|
return "", 0, errors.NewBadRequestError("Knowledge base ID is required")
|
|
}
|
|
|
|
kb, err := h.kbService.GetKnowledgeBaseByID(ctx, kbID)
|
|
if err != nil {
|
|
logger.ErrorWithFields(ctx, err, nil)
|
|
return "", 0, errors.NewNotFoundError("Knowledge base not found")
|
|
}
|
|
|
|
if !kb.IsWikiEnabled() {
|
|
return "", 0, errors.NewBadRequestError("Wiki feature is not enabled for this knowledge base")
|
|
}
|
|
|
|
return kbID, tenantID, nil
|
|
}
|
|
|
|
// getSlugParam extracts and cleans the slug from gin's wildcard path param
|
|
func getSlugParam(c *gin.Context) string {
|
|
slug := c.Param("slug")
|
|
// gin wildcard params include a leading "/"
|
|
slug = strings.TrimPrefix(slug, "/")
|
|
return strings.TrimSpace(slug)
|
|
}
|
|
|
|
// ListPages godoc
|
|
// @Summary List wiki pages
|
|
// @Description List wiki pages with optional filtering and pagination
|
|
// @Tags Wiki
|
|
// @Produce json
|
|
// @Param kb_id path string true "Knowledge base ID"
|
|
// @Param page_type query string false "Filter by page type"
|
|
// @Param status query string false "Filter by status"
|
|
// @Param query query string false "Full-text search"
|
|
// @Param page query int false "Page number"
|
|
// @Param page_size query int false "Page size"
|
|
// @Param sort_by query string false "Sort field"
|
|
// @Param sort_order query string false "Sort order (asc/desc)"
|
|
// @Success 200 {object} types.WikiPageListResponse
|
|
// @Failure 400 {object} errors.AppError
|
|
// @Security Bearer
|
|
// @Router /knowledgebase/{kb_id}/wiki/pages [get]
|
|
func (h *WikiPageHandler) ListPages(c *gin.Context) {
|
|
kbID, _, err := h.validateWikiKB(c)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
|
|
|
req := &types.WikiPageListRequest{
|
|
KnowledgeBaseID: kbID,
|
|
PageType: c.Query("page_type"),
|
|
Status: c.Query("status"),
|
|
Query: c.Query("query"),
|
|
Page: page,
|
|
PageSize: pageSize,
|
|
SortBy: c.DefaultQuery("sort_by", "updated_at"),
|
|
SortOrder: c.DefaultQuery("sort_order", "desc"),
|
|
}
|
|
|
|
resp, err := h.wikiService.ListPages(c.Request.Context(), req)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, resp)
|
|
}
|
|
|
|
// CreatePage godoc
|
|
// @Summary Create a wiki page
|
|
// @Description Create a new wiki page in the knowledge base
|
|
// @Tags Wiki
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param kb_id path string true "Knowledge base ID"
|
|
// @Param page body types.WikiPage true "Wiki page data"
|
|
// @Success 201 {object} types.WikiPage
|
|
// @Failure 400 {object} errors.AppError
|
|
// @Security Bearer
|
|
// @Router /knowledgebase/{kb_id}/wiki/pages [post]
|
|
func (h *WikiPageHandler) CreatePage(c *gin.Context) {
|
|
kbID, tenantID, err := h.validateWikiKB(c)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
var page types.WikiPage
|
|
if err := c.ShouldBindJSON(&page); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
page.KnowledgeBaseID = kbID
|
|
page.TenantID = tenantID
|
|
|
|
created, err := h.wikiService.CreatePage(c.Request.Context(), &page)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, created)
|
|
}
|
|
|
|
// GetPage godoc
|
|
// @Summary Get a wiki page by slug
|
|
// @Description Retrieve a wiki page by its slug
|
|
// @Tags Wiki
|
|
// @Produce json
|
|
// @Param kb_id path string true "Knowledge base ID"
|
|
// @Param slug path string true "Page slug"
|
|
// @Success 200 {object} types.WikiPage
|
|
// @Failure 404 {object} errors.AppError
|
|
// @Security Bearer
|
|
// @Router /knowledgebase/{kb_id}/wiki/pages/{slug} [get]
|
|
func (h *WikiPageHandler) GetPage(c *gin.Context) {
|
|
kbID, _, err := h.validateWikiKB(c)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
slug := getSlugParam(c)
|
|
if slug == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Page slug is required"})
|
|
return
|
|
}
|
|
|
|
page, err := h.wikiService.GetPageBySlug(c.Request.Context(), kbID, slug)
|
|
if err != nil {
|
|
if stderrors.Is(err, repository.ErrWikiPageNotFound) {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Wiki page not found"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, page)
|
|
}
|
|
|
|
// UpdatePage godoc
|
|
// @Summary Update a wiki page
|
|
// @Description Update an existing wiki page by slug
|
|
// @Tags Wiki
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param kb_id path string true "Knowledge base ID"
|
|
// @Param slug path string true "Page slug"
|
|
// @Param page body types.WikiPage true "Updated wiki page data"
|
|
// @Success 200 {object} types.WikiPage
|
|
// @Failure 404 {object} errors.AppError
|
|
// @Security Bearer
|
|
// @Router /knowledgebase/{kb_id}/wiki/pages/{slug} [put]
|
|
func (h *WikiPageHandler) UpdatePage(c *gin.Context) {
|
|
kbID, tenantID, err := h.validateWikiKB(c)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
slug := getSlugParam(c)
|
|
if slug == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Page slug is required"})
|
|
return
|
|
}
|
|
|
|
var page types.WikiPage
|
|
if err := c.ShouldBindJSON(&page); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
page.KnowledgeBaseID = kbID
|
|
page.TenantID = tenantID
|
|
page.Slug = slug
|
|
|
|
updated, err := h.wikiService.UpdatePage(c.Request.Context(), &page)
|
|
if err != nil {
|
|
if stderrors.Is(err, repository.ErrWikiPageNotFound) {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Wiki page not found"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, updated)
|
|
}
|
|
|
|
// DeletePage godoc
|
|
// @Summary Delete a wiki page
|
|
// @Description Soft-delete a wiki page by slug
|
|
// @Tags Wiki
|
|
// @Param kb_id path string true "Knowledge base ID"
|
|
// @Param slug path string true "Page slug"
|
|
// @Success 204
|
|
// @Failure 404 {object} errors.AppError
|
|
// @Security Bearer
|
|
// @Router /knowledgebase/{kb_id}/wiki/pages/{slug} [delete]
|
|
func (h *WikiPageHandler) DeletePage(c *gin.Context) {
|
|
kbID, _, err := h.validateWikiKB(c)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
slug := getSlugParam(c)
|
|
if slug == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Page slug is required"})
|
|
return
|
|
}
|
|
|
|
if err := h.wikiService.DeletePage(c.Request.Context(), kbID, slug); err != nil {
|
|
if stderrors.Is(err, repository.ErrWikiPageNotFound) {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Wiki page not found"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.Status(http.StatusNoContent)
|
|
}
|
|
|
|
// GetIndex godoc
|
|
// @Summary Get wiki index view
|
|
// @Description Returns the wiki index as intro text plus per-type paginated
|
|
// @Description directory groups. The heavy directory markdown that used to
|
|
// @Description live in wiki_pages.content was replaced with this structured
|
|
// @Description response so a KB with tens of thousands of pages no longer
|
|
// @Description materializes megabytes of TEXT on every index open.
|
|
// @Tags Wiki
|
|
// @Produce json
|
|
// @Param kb_id path string true "Knowledge base ID"
|
|
// @Param types query string false "Comma-separated page types (default: all content types)"
|
|
// @Param limit query int false "Per-group window size, 1-200 (default 50)"
|
|
// @Param cursor query string false "Opaque offset cursor from previous response"
|
|
// @Success 200 {object} types.WikiIndexResponse
|
|
// @Security Bearer
|
|
// @Router /knowledgebase/{kb_id}/wiki/index [get]
|
|
func (h *WikiPageHandler) GetIndex(c *gin.Context) {
|
|
kbID, _, err := h.validateWikiKB(c)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
var pageTypes []string
|
|
if raw := c.Query("types"); raw != "" {
|
|
for _, t := range strings.Split(raw, ",") {
|
|
t = strings.TrimSpace(t)
|
|
if t != "" {
|
|
pageTypes = append(pageTypes, t)
|
|
}
|
|
}
|
|
}
|
|
|
|
limit := 50
|
|
if raw := c.Query("limit"); raw != "" {
|
|
if v, convErr := strconv.Atoi(raw); convErr == nil && v > 0 {
|
|
limit = v
|
|
}
|
|
}
|
|
|
|
resp, err := h.wikiService.GetIndexView(c.Request.Context(), kbID, pageTypes, limit, c.Query("cursor"))
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, resp)
|
|
}
|
|
|
|
// GetLog godoc
|
|
// @Summary Get wiki operation log
|
|
// @Description Returns a paginated feed of wiki operation events (ingest, retract, ...)
|
|
// @Description newest-first. Pagination is cursor-based: pass `next_cursor` from the
|
|
// @Description previous response back as `cursor` to fetch the next page.
|
|
// @Tags Wiki
|
|
// @Produce json
|
|
// @Param kb_id path string true "Knowledge base ID"
|
|
// @Param cursor query string false "Opaque cursor from the previous page (empty = newest)"
|
|
// @Param limit query int false "Page size, 1-200 (default 50)"
|
|
// @Success 200 {object} types.WikiLogEntryListResponse
|
|
// @Security Bearer
|
|
// @Router /knowledgebase/{kb_id}/wiki/log [get]
|
|
func (h *WikiPageHandler) GetLog(c *gin.Context) {
|
|
kbID, _, err := h.validateWikiKB(c)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
cursor := c.Query("cursor")
|
|
limit := 50
|
|
if raw := c.Query("limit"); raw != "" {
|
|
if v, convErr := strconv.Atoi(raw); convErr == nil && v > 0 {
|
|
limit = v
|
|
}
|
|
}
|
|
|
|
resp, err := h.logEntryService.List(c.Request.Context(), kbID, cursor, limit)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, resp)
|
|
}
|
|
|
|
// Graph query parameter bounds. The defaults cap an `overview` request at
|
|
// 500 nodes — comfortably renderable in the frontend's hand-rolled SVG
|
|
// force simulation — while the hard max of 2000 is the upper bound a
|
|
// power user can opt into before rendering gets choppy. Ego depth is
|
|
// capped at 3 hops because the node population grows super-linearly with
|
|
// depth and wider searches are better served by repeated ego jumps.
|
|
const (
|
|
wikiGraphDefaultLimit = 500
|
|
wikiGraphMaxLimit = 2000
|
|
wikiGraphMaxDepth = 3
|
|
wikiGraphDefaultDepth = 1
|
|
)
|
|
|
|
// GetGraph godoc
|
|
// @Summary Get wiki link graph
|
|
// @Description Returns a slice of the wiki link graph for visualization. Supports
|
|
// @Description `mode=overview` (top-N most-connected pages, default) and
|
|
// @Description `mode=ego` (BFS neighborhood of a center slug) to keep response
|
|
// @Description size tractable for knowledge bases with tens of thousands of pages.
|
|
// @Tags Wiki
|
|
// @Produce json
|
|
// @Param kb_id path string true "Knowledge base ID"
|
|
// @Param mode query string false "overview (default) | ego"
|
|
// @Param center query string false "Center slug for ego mode"
|
|
// @Param depth query int false "Ego BFS depth (1-3, default 1)"
|
|
// @Param types query string false "Comma-separated page_type allow-list"
|
|
// @Param limit query int false "Max nodes to return (default 500, max 2000)"
|
|
// @Success 200 {object} types.WikiGraphData
|
|
// @Security Bearer
|
|
// @Router /knowledgebase/{kb_id}/wiki/graph [get]
|
|
func (h *WikiPageHandler) GetGraph(c *gin.Context) {
|
|
kbID, _, err := h.validateWikiKB(c)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
mode := strings.TrimSpace(c.Query("mode"))
|
|
if mode == "" {
|
|
mode = types.WikiGraphModeOverview
|
|
}
|
|
if mode != types.WikiGraphModeOverview && mode != types.WikiGraphModeEgo {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "mode must be 'overview' or 'ego'"})
|
|
return
|
|
}
|
|
|
|
center := strings.TrimSpace(c.Query("center"))
|
|
if mode == types.WikiGraphModeEgo && center == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "center is required when mode=ego"})
|
|
return
|
|
}
|
|
|
|
depth := wikiGraphDefaultDepth
|
|
if v := c.Query("depth"); v != "" {
|
|
parsed, parseErr := strconv.Atoi(v)
|
|
if parseErr != nil || parsed < 1 {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "depth must be a positive integer"})
|
|
return
|
|
}
|
|
if parsed > wikiGraphMaxDepth {
|
|
parsed = wikiGraphMaxDepth
|
|
}
|
|
depth = parsed
|
|
}
|
|
|
|
limit := wikiGraphDefaultLimit
|
|
if v := c.Query("limit"); v != "" {
|
|
parsed, parseErr := strconv.Atoi(v)
|
|
if parseErr != nil || parsed < 1 {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "limit must be a positive integer"})
|
|
return
|
|
}
|
|
if parsed > wikiGraphMaxLimit {
|
|
parsed = wikiGraphMaxLimit
|
|
}
|
|
limit = parsed
|
|
}
|
|
|
|
var typesFilter []string
|
|
if v := strings.TrimSpace(c.Query("types")); v != "" {
|
|
for _, t := range strings.Split(v, ",") {
|
|
t = strings.TrimSpace(t)
|
|
if t != "" {
|
|
typesFilter = append(typesFilter, t)
|
|
}
|
|
}
|
|
}
|
|
|
|
req := &types.WikiGraphRequest{
|
|
KnowledgeBaseID: kbID,
|
|
Mode: mode,
|
|
Center: center,
|
|
Depth: depth,
|
|
Types: typesFilter,
|
|
Limit: limit,
|
|
}
|
|
|
|
graph, err := h.wikiService.GetGraph(c.Request.Context(), req)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, graph)
|
|
}
|
|
|
|
// GetStats godoc
|
|
// @Summary Get wiki statistics
|
|
// @Description Returns aggregate statistics about the wiki
|
|
// @Tags Wiki
|
|
// @Produce json
|
|
// @Param kb_id path string true "Knowledge base ID"
|
|
// @Success 200 {object} types.WikiStats
|
|
// @Security Bearer
|
|
// @Router /knowledgebase/{kb_id}/wiki/stats [get]
|
|
func (h *WikiPageHandler) GetStats(c *gin.Context) {
|
|
kbID, _, err := h.validateWikiKB(c)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
stats, err := h.wikiService.GetStats(c.Request.Context(), kbID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, stats)
|
|
}
|
|
|
|
// ListIssues godoc
|
|
// @Summary List wiki page issues
|
|
// @Description List issues flagged on wiki pages with optional filtering
|
|
// @Tags Wiki
|
|
// @Produce json
|
|
// @Param kb_id path string true "Knowledge base ID"
|
|
// @Param slug query string false "Filter by page slug"
|
|
// @Param status query string false "Filter by status (pending, ignored, resolved)"
|
|
// @Success 200 {array} types.WikiPageIssue
|
|
// @Security Bearer
|
|
// @Router /knowledgebase/{kb_id}/wiki/issues [get]
|
|
func (h *WikiPageHandler) ListIssues(c *gin.Context) {
|
|
kbID, _, err := h.validateWikiKB(c)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
slug := c.Query("slug")
|
|
status := c.Query("status")
|
|
|
|
issues, err := h.wikiService.ListIssues(c.Request.Context(), kbID, slug, status)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, issues)
|
|
}
|
|
|
|
// UpdateIssueStatus godoc
|
|
// @Summary Update wiki page issue status
|
|
// @Description Update the status of a flagged wiki page issue
|
|
// @Tags Wiki
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param kb_id path string true "Knowledge base ID"
|
|
// @Param issue_id path string true "Issue ID"
|
|
// @Param status body object true "New status {'status': 'ignored'}"
|
|
// @Success 200 {object} map[string]string
|
|
// @Failure 400 {object} errors.AppError
|
|
// @Security Bearer
|
|
// @Router /knowledgebase/{kb_id}/wiki/issues/{issue_id}/status [put]
|
|
func (h *WikiPageHandler) UpdateIssueStatus(c *gin.Context) {
|
|
_, _, err := h.validateWikiKB(c)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
issueID := secutils.SanitizeForLog(c.Param("issue_id"))
|
|
if issueID == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Issue ID is required"})
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
Status string `json:"status" binding:"required"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
validStatuses := map[string]bool{"pending": true, "ignored": true, "resolved": true}
|
|
if !validStatuses[req.Status] {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid status. Must be pending, ignored, or resolved"})
|
|
return
|
|
}
|
|
|
|
if err := h.wikiService.UpdateIssueStatus(c.Request.Context(), issueID, req.Status); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "Issue status updated successfully"})
|
|
}
|
|
|
|
// SearchPages godoc
|
|
// @Summary Search wiki pages
|
|
// @Description Full-text search over wiki pages
|
|
// @Tags Wiki
|
|
// @Produce json
|
|
// @Param kb_id path string true "Knowledge base ID"
|
|
// @Param q query string true "Search query"
|
|
// @Param limit query int false "Max results (default 10)"
|
|
// @Success 200 {array} types.WikiPage
|
|
// @Security Bearer
|
|
// @Router /knowledgebase/{kb_id}/wiki/search [get]
|
|
func (h *WikiPageHandler) SearchPages(c *gin.Context) {
|
|
kbID, _, err := h.validateWikiKB(c)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
query := c.Query("q")
|
|
if query == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Search query 'q' is required"})
|
|
return
|
|
}
|
|
|
|
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
|
|
|
pages, err := h.wikiService.SearchPages(c.Request.Context(), kbID, query, limit)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"pages": pages})
|
|
}
|
|
|
|
// RebuildLinks godoc
|
|
// @Summary Rebuild wiki links
|
|
// @Description Re-parse all pages and rebuild bidirectional link references
|
|
// @Tags Wiki
|
|
// @Param kb_id path string true "Knowledge base ID"
|
|
// @Success 200 {object} map[string]string
|
|
// @Security Bearer
|
|
// @Router /knowledgebase/{kb_id}/wiki/rebuild-links [post]
|
|
func (h *WikiPageHandler) RebuildLinks(c *gin.Context) {
|
|
kbID, _, err := h.validateWikiKB(c)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
if err := h.wikiService.RebuildLinks(c.Request.Context(), kbID); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "Links rebuilt successfully"})
|
|
}
|
|
|
|
// Lint godoc
|
|
// @Summary Run wiki lint
|
|
// @Description Perform a comprehensive health check on the wiki
|
|
// @Tags Wiki
|
|
// @Produce json
|
|
// @Param kb_id path string true "Knowledge base ID"
|
|
// @Success 200 {object} service.WikiLintReport
|
|
// @Security Bearer
|
|
// @Router /knowledgebase/{kb_id}/wiki/lint [get]
|
|
func (h *WikiPageHandler) Lint(c *gin.Context) {
|
|
kbID, _, err := h.validateWikiKB(c)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
report, err := h.lintService.RunLint(c.Request.Context(), kbID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, report)
|
|
}
|
|
|
|
// AutoFix godoc
|
|
// @Summary Auto-fix wiki issues
|
|
// @Description Automatically fix fixable wiki issues (broken links, etc.)
|
|
// @Tags Wiki
|
|
// @Produce json
|
|
// @Param kb_id path string true "Knowledge base ID"
|
|
// @Success 200 {object} map[string]interface{}
|
|
// @Security Bearer
|
|
// @Router /knowledgebase/{kb_id}/wiki/auto-fix [post]
|
|
func (h *WikiPageHandler) AutoFix(c *gin.Context) {
|
|
kbID, _, err := h.validateWikiKB(c)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
fixed, err := h.lintService.AutoFix(c.Request.Context(), kbID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"fixed": fixed, "message": fmt.Sprintf("Auto-fixed %d issues", fixed)})
|
|
}
|