diff --git a/internal/handler/knowledge.go b/internal/handler/knowledge.go index dc8b2d9f..fa6c009b 100644 --- a/internal/handler/knowledge.go +++ b/internal/handler/knowledge.go @@ -50,18 +50,168 @@ func (h *KnowledgeHandler) GetCategories(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"categories": categories}) } -// GetItems 获取知识项列表 +// GetItems 获取知识项列表(支持按分类分页和关键字搜索,默认不返回完整内容) func (h *KnowledgeHandler) GetItems(c *gin.Context) { category := c.Query("category") + searchKeyword := c.Query("search") // 搜索关键字 + + // 如果提供了搜索关键字,执行关键字搜索(在所有数据中搜索) + if searchKeyword != "" { + items, err := h.manager.SearchItemsByKeyword(searchKeyword, category) + if err != nil { + h.logger.Error("搜索知识项失败", zap.Error(err)) + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } - items, err := h.manager.GetItems(category) - if err != nil { - h.logger.Error("获取知识项失败", zap.Error(err)) - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + // 按分类分组结果 + groupedByCategory := make(map[string][]*knowledge.KnowledgeItemSummary) + for _, item := range items { + cat := item.Category + if cat == "" { + cat = "未分类" + } + groupedByCategory[cat] = append(groupedByCategory[cat], item) + } + + // 转换为CategoryWithItems格式 + categoriesWithItems := make([]*knowledge.CategoryWithItems, 0, len(groupedByCategory)) + for cat, catItems := range groupedByCategory { + categoriesWithItems = append(categoriesWithItems, &knowledge.CategoryWithItems{ + Category: cat, + ItemCount: len(catItems), + Items: catItems, + }) + } + + // 按分类名称排序 + for i := 0; i < len(categoriesWithItems)-1; i++ { + for j := i + 1; j < len(categoriesWithItems); j++ { + if categoriesWithItems[i].Category > categoriesWithItems[j].Category { + categoriesWithItems[i], categoriesWithItems[j] = categoriesWithItems[j], categoriesWithItems[i] + } + } + } + + c.JSON(http.StatusOK, gin.H{ + "categories": categoriesWithItems, + "total": len(categoriesWithItems), + "search": searchKeyword, + "is_search": true, + }) + return + } + + // 分页模式:categoryPage=true 表示按分类分页,否则按项分页(向后兼容) + categoryPageMode := c.Query("categoryPage") != "false" // 默认使用分类分页 + + // 分页参数 + limit := 50 // 默认每页50条(分类分页时为分类数,项分页时为项数) + offset := 0 + if limitStr := c.Query("limit"); limitStr != "" { + if parsed, err := parseInt(limitStr); err == nil && parsed > 0 && parsed <= 500 { + limit = parsed + } + } + if offsetStr := c.Query("offset"); offsetStr != "" { + if parsed, err := parseInt(offsetStr); err == nil && parsed >= 0 { + offset = parsed + } + } + + // 如果指定了category参数,且使用分类分页模式,则只返回该分类 + if category != "" && categoryPageMode { + // 单分类模式:返回该分类的所有知识项(不分页) + items, total, err := h.manager.GetItemsSummary(category, 0, 0) + if err != nil { + h.logger.Error("获取知识项失败", zap.Error(err)) + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // 包装成分类结构 + categoriesWithItems := []*knowledge.CategoryWithItems{ + { + Category: category, + ItemCount: total, + Items: items, + }, + } + + c.JSON(http.StatusOK, gin.H{ + "categories": categoriesWithItems, + "total": 1, // 只有一个分类 + "limit": limit, + "offset": offset, + }) return } - c.JSON(http.StatusOK, gin.H{"items": items}) + if categoryPageMode { + // 按分类分页模式(默认) + // limit表示每页分类数,推荐5-10个分类 + if limit <= 0 || limit > 100 { + limit = 10 // 默认每页10个分类 + } + + categoriesWithItems, totalCategories, err := h.manager.GetCategoriesWithItems(limit, offset) + if err != nil { + h.logger.Error("获取分类知识项失败", zap.Error(err)) + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "categories": categoriesWithItems, + "total": totalCategories, + "limit": limit, + "offset": offset, + }) + return + } + + // 按项分页模式(向后兼容) + // 是否包含完整内容(默认false,只返回摘要) + includeContent := c.Query("includeContent") == "true" + + if includeContent { + // 返回完整内容(向后兼容) + items, err := h.manager.GetItemsWithOptions(category, limit, offset, true) + if err != nil { + h.logger.Error("获取知识项失败", zap.Error(err)) + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // 获取总数 + total, err := h.manager.GetItemsCount(category) + if err != nil { + h.logger.Warn("获取知识项总数失败", zap.Error(err)) + total = len(items) + } + + c.JSON(http.StatusOK, gin.H{ + "items": items, + "total": total, + "limit": limit, + "offset": offset, + }) + } else { + // 返回摘要(不包含完整内容,推荐方式) + items, total, err := h.manager.GetItemsSummary(category, limit, offset) + if err != nil { + h.logger.Error("获取知识项失败", zap.Error(err)) + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "items": items, + "total": total, + "limit": limit, + "offset": offset, + }) + } } // GetItem 获取单个知识项 diff --git a/internal/knowledge/manager.go b/internal/knowledge/manager.go index 4f9dc95a..8ddf6409 100644 --- a/internal/knowledge/manager.go +++ b/internal/knowledge/manager.go @@ -153,21 +153,115 @@ func (m *Manager) GetCategories() ([]string, error) { return categories, nil } -// GetItems 获取知识项列表 +// GetCategoriesWithItems 按分类分页获取知识项(每个分类包含其下的所有知识项) +// limit: 每页分类数量(0表示不限制) +// offset: 偏移量(按分类偏移) +func (m *Manager) GetCategoriesWithItems(limit, offset int) ([]*CategoryWithItems, int, error) { + // 首先获取所有分类(带数量统计) + rows, err := m.db.Query(` + SELECT category, COUNT(*) as item_count + FROM knowledge_base_items + GROUP BY category + ORDER BY category + `) + if err != nil { + return nil, 0, fmt.Errorf("查询分类失败: %w", err) + } + defer rows.Close() + + // 收集所有分类信息 + type categoryInfo struct { + name string + itemCount int + } + var allCategories []categoryInfo + for rows.Next() { + var info categoryInfo + if err := rows.Scan(&info.name, &info.itemCount); err != nil { + return nil, 0, fmt.Errorf("扫描分类失败: %w", err) + } + allCategories = append(allCategories, info) + } + + totalCategories := len(allCategories) + + // 应用分页(按分类分页) + var paginatedCategories []categoryInfo + if limit > 0 { + start := offset + end := offset + limit + if start >= totalCategories { + paginatedCategories = []categoryInfo{} + } else { + if end > totalCategories { + end = totalCategories + } + paginatedCategories = allCategories[start:end] + } + } else { + paginatedCategories = allCategories + } + + // 为每个分类获取其下的知识项(只返回摘要,不包含完整内容) + result := make([]*CategoryWithItems, 0, len(paginatedCategories)) + for _, catInfo := range paginatedCategories { + // 获取该分类下的所有知识项 + items, _, err := m.GetItemsSummary(catInfo.name, 0, 0) + if err != nil { + return nil, 0, fmt.Errorf("获取分类 %s 的知识项失败: %w", catInfo.name, err) + } + + result = append(result, &CategoryWithItems{ + Category: catInfo.name, + ItemCount: catInfo.itemCount, + Items: items, + }) + } + + return result, totalCategories, nil +} + +// GetItems 获取知识项列表(完整内容,用于向后兼容) func (m *Manager) GetItems(category string) ([]*KnowledgeItem, error) { + return m.GetItemsWithOptions(category, 0, 0, true) +} + +// GetItemsWithOptions 获取知识项列表(支持分页和可选内容) +// category: 分类筛选(空字符串表示所有分类) +// limit: 每页数量(0表示不限制) +// offset: 偏移量 +// includeContent: 是否包含完整内容(false时只返回摘要) +func (m *Manager) GetItemsWithOptions(category string, limit, offset int, includeContent bool) ([]*KnowledgeItem, error) { var rows *sql.Rows var err error - if category != "" { - rows, err = m.db.Query( - "SELECT id, category, title, file_path, content, created_at, updated_at FROM knowledge_base_items WHERE category = ? ORDER BY title", - category, - ) + // 构建SQL查询 + var query string + var args []interface{} + + if includeContent { + query = "SELECT id, category, title, file_path, content, created_at, updated_at FROM knowledge_base_items" } else { - rows, err = m.db.Query( - "SELECT id, category, title, file_path, content, created_at, updated_at FROM knowledge_base_items ORDER BY category, title", - ) + query = "SELECT id, category, title, file_path, created_at, updated_at FROM knowledge_base_items" } + + if category != "" { + query += " WHERE category = ?" + args = append(args, category) + } + + query += " ORDER BY category, title" + + if limit > 0 { + query += " LIMIT ?" + args = append(args, limit) + if offset > 0 { + query += " OFFSET ?" + args = append(args, offset) + } + } + + rows, err = m.db.Query(query, args...) if err != nil { return nil, fmt.Errorf("查询知识项失败: %w", err) } @@ -177,8 +271,17 @@ func (m *Manager) GetItems(category string) ([]*KnowledgeItem, error) { for rows.Next() { item := &KnowledgeItem{} var createdAt, updatedAt string - if err := rows.Scan(&item.ID, &item.Category, &item.Title, &item.FilePath, &item.Content, &createdAt, &updatedAt); err != nil { - return nil, fmt.Errorf("扫描知识项失败: %w", err) + + if includeContent { + if err := rows.Scan(&item.ID, &item.Category, &item.Title, &item.FilePath, &item.Content, &createdAt, &updatedAt); err != nil { + return nil, fmt.Errorf("扫描知识项失败: %w", err) + } + } else { + if err := rows.Scan(&item.ID, &item.Category, &item.Title, &item.FilePath, &createdAt, &updatedAt); err != nil { + return nil, fmt.Errorf("扫描知识项失败: %w", err) + } + // 不包含内容时,Content为空字符串 + item.Content = "" } // 解析时间 - 支持多种格式 @@ -225,6 +328,196 @@ func (m *Manager) GetItems(category string) ([]*KnowledgeItem, error) { return items, nil } +// GetItemsCount 获取知识项总数 +func (m *Manager) GetItemsCount(category string) (int, error) { + var count int + var err error + + if category != "" { + err = m.db.QueryRow("SELECT COUNT(*) FROM knowledge_base_items WHERE category = ?", category).Scan(&count) + } else { + err = m.db.QueryRow("SELECT COUNT(*) FROM knowledge_base_items").Scan(&count) + } + + if err != nil { + return 0, fmt.Errorf("查询知识项总数失败: %w", err) + } + + return count, nil +} + +// SearchItemsByKeyword 按关键字搜索知识项(在所有数据中搜索,支持标题、分类、路径匹配) +func (m *Manager) SearchItemsByKeyword(keyword string, category string) ([]*KnowledgeItemSummary, error) { + if keyword == "" { + return nil, fmt.Errorf("搜索关键字不能为空") + } + + // 构建SQL查询,使用LIKE进行关键字匹配(不区分大小写) + var query string + var args []interface{} + + // SQLite的LIKE不区分大小写,使用COLLATE NOCASE或LOWER()函数 + // 使用%keyword%进行模糊匹配 + searchPattern := "%" + keyword + "%" + + query = ` + SELECT id, category, title, file_path, created_at, updated_at + FROM knowledge_base_items + WHERE (LOWER(title) LIKE LOWER(?) OR LOWER(category) LIKE LOWER(?) OR LOWER(file_path) LIKE LOWER(?)) + ` + args = append(args, searchPattern, searchPattern, searchPattern) + + // 如果指定了分类,添加分类过滤 + if category != "" { + query += " AND category = ?" + args = append(args, category) + } + + query += " ORDER BY category, title" + + rows, err := m.db.Query(query, args...) + if err != nil { + return nil, fmt.Errorf("搜索知识项失败: %w", err) + } + defer rows.Close() + + var items []*KnowledgeItemSummary + for rows.Next() { + item := &KnowledgeItemSummary{} + var createdAt, updatedAt string + + if err := rows.Scan(&item.ID, &item.Category, &item.Title, &item.FilePath, &createdAt, &updatedAt); err != nil { + return nil, fmt.Errorf("扫描知识项失败: %w", err) + } + + // 解析时间 + timeFormats := []string{ + "2006-01-02 15:04:05.999999999-07:00", + "2006-01-02 15:04:05.999999999", + "2006-01-02T15:04:05.999999999Z07:00", + "2006-01-02T15:04:05Z", + "2006-01-02 15:04:05", + time.RFC3339, + time.RFC3339Nano, + } + + if createdAt != "" { + for _, format := range timeFormats { + parsed, err := time.Parse(format, createdAt) + if err == nil && !parsed.IsZero() { + item.CreatedAt = parsed + break + } + } + } + + if updatedAt != "" { + for _, format := range timeFormats { + parsed, err := time.Parse(format, updatedAt) + if err == nil && !parsed.IsZero() { + item.UpdatedAt = parsed + break + } + } + } + + if item.UpdatedAt.IsZero() && !item.CreatedAt.IsZero() { + item.UpdatedAt = item.CreatedAt + } + + items = append(items, item) + } + + return items, nil +} + +// GetItemsSummary 获取知识项摘要列表(不包含完整内容,支持分页) +func (m *Manager) GetItemsSummary(category string, limit, offset int) ([]*KnowledgeItemSummary, int, error) { + // 获取总数 + total, err := m.GetItemsCount(category) + if err != nil { + return nil, 0, err + } + + // 获取列表数据(不包含内容) + var rows *sql.Rows + var query string + var args []interface{} + + query = "SELECT id, category, title, file_path, created_at, updated_at FROM knowledge_base_items" + + if category != "" { + query += " WHERE category = ?" + args = append(args, category) + } + + query += " ORDER BY category, title" + + if limit > 0 { + query += " LIMIT ?" + args = append(args, limit) + if offset > 0 { + query += " OFFSET ?" + args = append(args, offset) + } + } + + rows, err = m.db.Query(query, args...) + if err != nil { + return nil, 0, fmt.Errorf("查询知识项失败: %w", err) + } + defer rows.Close() + + var items []*KnowledgeItemSummary + for rows.Next() { + item := &KnowledgeItemSummary{} + var createdAt, updatedAt string + + if err := rows.Scan(&item.ID, &item.Category, &item.Title, &item.FilePath, &createdAt, &updatedAt); err != nil { + return nil, 0, fmt.Errorf("扫描知识项失败: %w", err) + } + + // 解析时间 + timeFormats := []string{ + "2006-01-02 15:04:05.999999999-07:00", + "2006-01-02 15:04:05.999999999", + "2006-01-02T15:04:05.999999999Z07:00", + "2006-01-02T15:04:05Z", + "2006-01-02 15:04:05", + time.RFC3339, + time.RFC3339Nano, + } + + if createdAt != "" { + for _, format := range timeFormats { + parsed, err := time.Parse(format, createdAt) + if err == nil && !parsed.IsZero() { + item.CreatedAt = parsed + break + } + } + } + + if updatedAt != "" { + for _, format := range timeFormats { + parsed, err := time.Parse(format, updatedAt) + if err == nil && !parsed.IsZero() { + item.UpdatedAt = parsed + break + } + } + } + + if item.UpdatedAt.IsZero() && !item.CreatedAt.IsZero() { + item.UpdatedAt = item.CreatedAt + } + + items = append(items, item) + } + + return items, total, nil +} + // GetItem 获取单个知识项 func (m *Manager) GetItem(id string) (*KnowledgeItem, error) { item := &KnowledgeItem{} diff --git a/internal/knowledge/types.go b/internal/knowledge/types.go index 53b54444..656b03a7 100644 --- a/internal/knowledge/types.go +++ b/internal/knowledge/types.go @@ -16,6 +16,45 @@ type KnowledgeItem struct { UpdatedAt time.Time `json:"updatedAt"` } +// KnowledgeItemSummary 知识库项摘要(用于列表,不包含完整内容) +type KnowledgeItemSummary struct { + ID string `json:"id"` + Category string `json:"category"` + Title string `json:"title"` + FilePath string `json:"filePath"` + Content string `json:"content,omitempty"` // 可选:内容预览(如果提供,通常只包含前150字符) + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +// MarshalJSON 自定义JSON序列化,确保时间格式正确 +func (k *KnowledgeItemSummary) MarshalJSON() ([]byte, error) { + type Alias KnowledgeItemSummary + aux := &struct { + *Alias + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + }{ + Alias: (*Alias)(k), + } + + // 格式化创建时间 + if k.CreatedAt.IsZero() { + aux.CreatedAt = "" + } else { + aux.CreatedAt = k.CreatedAt.Format(time.RFC3339) + } + + // 格式化更新时间 + if k.UpdatedAt.IsZero() { + aux.UpdatedAt = "" + } else { + aux.UpdatedAt = k.UpdatedAt.Format(time.RFC3339) + } + + return json.Marshal(aux) +} + // MarshalJSON 自定义JSON序列化,确保时间格式正确 func (k *KnowledgeItem) MarshalJSON() ([]byte, error) { type Alias KnowledgeItem @@ -85,6 +124,13 @@ func (r *RetrievalLog) MarshalJSON() ([]byte, error) { }) } +// CategoryWithItems 分类及其下的知识项(用于按分类分页) +type CategoryWithItems struct { + Category string `json:"category"` // 分类名称 + ItemCount int `json:"itemCount"` // 该分类下的知识项总数 + Items []*KnowledgeItemSummary `json:"items"` // 该分类下的知识项列表 +} + // SearchRequest 搜索请求 type SearchRequest struct { Query string `json:"query"` diff --git a/web/static/js/knowledge.js b/web/static/js/knowledge.js index dc0848fe..c4c46134 100644 --- a/web/static/js/knowledge.js +++ b/web/static/js/knowledge.js @@ -4,6 +4,13 @@ let knowledgeItems = []; let currentEditingItemId = null; let isSavingKnowledgeItem = false; // 防止重复提交 let retrievalLogsData = []; // 存储检索日志数据,用于详情查看 +let knowledgePagination = { + currentPage: 1, + pageSize: 10, // 每页分类数(改为按分类分页) + total: 0, + currentCategory: '' +}; +let searchTimeout = null; // 搜索防抖定时器 // 加载知识分类 async function loadKnowledgeCategories() { @@ -77,14 +84,21 @@ async function loadKnowledgeCategories() { } } -// 加载知识项列表 -async function loadKnowledgeItems(category = '') { +// 加载知识项列表(支持按分类分页,默认不加载完整内容) +async function loadKnowledgeItems(category = '', page = 1, pageSize = 10) { try { - // 添加时间戳参数避免缓存 + // 更新分页状态 + knowledgePagination.currentCategory = category; + knowledgePagination.currentPage = page; + knowledgePagination.pageSize = pageSize; + + // 构建URL(按分类分页模式,不包含完整内容) const timestamp = Date.now(); - const url = category - ? `/api/knowledge/items?category=${encodeURIComponent(category)}&_t=${timestamp}` - : `/api/knowledge/items?_t=${timestamp}`; + const offset = (page - 1) * pageSize; + let url = `/api/knowledge/items?categoryPage=true&limit=${pageSize}&offset=${offset}&_t=${timestamp}`; + if (category) { + url += `&category=${encodeURIComponent(category)}`; + } const response = await apiFetch(url, { method: 'GET', @@ -123,12 +137,27 @@ async function loadKnowledgeItems(category = '') { `; } knowledgeItems = []; + knowledgePagination.total = 0; + renderKnowledgePagination(); return []; } - knowledgeItems = data.items || []; - renderKnowledgeItems(knowledgeItems); - return knowledgeItems; + // 处理按分类分页的响应数据 + const categoriesWithItems = data.categories || []; + knowledgePagination.total = data.total || 0; // 总分类数 + + renderKnowledgeItemsByCategories(categoriesWithItems); + + // 如果选择了单个分类,不显示分页(因为只显示一个分类) + if (category) { + const paginationContainer = document.getElementById('knowledge-pagination'); + if (paginationContainer) { + paginationContainer.innerHTML = ''; + } + } else { + renderKnowledgePagination(); + } + return categoriesWithItems; } catch (error) { console.error('加载知识项失败:', error); // 只在非功能未启用的情况下显示错误 @@ -139,7 +168,51 @@ async function loadKnowledgeItems(category = '') { } } -// 渲染知识项列表 +// 渲染知识项列表(按分类分页的数据结构) +function renderKnowledgeItemsByCategories(categoriesWithItems) { + const container = document.getElementById('knowledge-items-list'); + if (!container) return; + + if (categoriesWithItems.length === 0) { + container.innerHTML = '
暂无知识项
'; + return; + } + + // 计算总项数和分类数 + const totalItems = categoriesWithItems.reduce((sum, cat) => sum + (cat.items?.length || 0), 0); + const categoryCount = categoriesWithItems.length; + + // 更新统计信息 + updateKnowledgeStats(categoriesWithItems, categoryCount); + + // 渲染分类及知识项 + let html = '
'; + + categoriesWithItems.forEach(categoryData => { + const category = categoryData.category || '未分类'; + const categoryItems = categoryData.items || []; + const categoryCount = categoryData.itemCount || categoryItems.length; + + html += ` +
+
+
+

${escapeHtml(category)}

+ ${categoryCount} 项 +
+
+
+ ${categoryItems.map(item => renderKnowledgeItemCard(item)).join('')} +
+
+ `; + }); + + html += '
'; + container.innerHTML = html; +} + +// 渲染知识项列表(向后兼容,用于按项分页的旧代码) function renderKnowledgeItems(items) { const container = document.getElementById('knowledge-items-list'); if (!container) return; @@ -189,22 +262,66 @@ function renderKnowledgeItems(items) { container.innerHTML = html; } +// 渲染分页控件(按分类分页) +function renderKnowledgePagination() { + const container = document.getElementById('knowledge-pagination'); + if (!container) return; + + const { currentPage, pageSize, total } = knowledgePagination; + const totalPages = Math.ceil(total / pageSize); // total是总分类数 + + if (totalPages <= 1) { + container.innerHTML = ''; + return; + } + + let html = '
'; + + // 上一页按钮 + html += ``; + + // 页码显示(显示分类数) + html += `第 ${currentPage} 页,共 ${totalPages} 页(共 ${total} 个分类)`; + + // 下一页按钮 + html += ``; + + html += '
'; + container.innerHTML = html; +} + +// 加载指定页码的知识项 +function loadKnowledgePage(page) { + const { currentCategory, pageSize, total } = knowledgePagination; + const totalPages = Math.ceil(total / pageSize); + + if (page < 1 || page > totalPages) { + return; + } + + loadKnowledgeItems(currentCategory, page, pageSize); +} + // 渲染单个知识项卡片 function renderKnowledgeItemCard(item) { - // 提取内容预览(去除markdown格式,取前150字符) - let preview = item.content || ''; - // 移除markdown标题标记 - preview = preview.replace(/^#+\s+/gm, ''); - // 移除代码块 - preview = preview.replace(/```[\s\S]*?```/g, ''); - // 移除行内代码 - preview = preview.replace(/`[^`]+`/g, ''); - // 移除链接 - preview = preview.replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1'); - // 清理多余空白 - preview = preview.replace(/\n+/g, ' ').replace(/\s+/g, ' ').trim(); - - const previewText = preview.length > 150 ? preview.substring(0, 150) + '...' : preview; + // 提取内容预览(如果item没有content字段,说明是摘要,不显示预览) + let previewText = ''; + if (item.content) { + // 去除markdown格式,取前150字符 + let preview = item.content; + // 移除markdown标题标记 + preview = preview.replace(/^#+\s+/gm, ''); + // 移除代码块 + preview = preview.replace(/```[\s\S]*?```/g, ''); + // 移除行内代码 + preview = preview.replace(/`[^`]+`/g, ''); + // 移除链接 + preview = preview.replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1'); + // 清理多余空白 + preview = preview.replace(/\n+/g, ' ').replace(/\s+/g, ' ').trim(); + + previewText = preview.length > 150 ? preview.substring(0, 150) + '...' : preview; + } // 提取文件路径显示 const filePath = item.filePath || ''; @@ -248,9 +365,11 @@ function renderKnowledgeItemCard(item) { ${relativePath ? `
📁 ${escapeHtml(relativePath)}
` : ''} + ${previewText ? `
-

${escapeHtml(previewText || '无内容预览')}

+

${escapeHtml(previewText)}

+ ` : ''} @@ -392,6 +392,7 @@
加载中...
+