Add files via upload

This commit is contained in:
公明
2025-12-29 00:01:25 +08:00
committed by GitHub
parent 31b2aae568
commit c00d504572
5 changed files with 816 additions and 77 deletions
+156 -6
View File
@@ -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 获取单个知识项
+304 -11
View File
@@ -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{}
+46
View File
@@ -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"`
+308 -59
View File
@@ -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 = '<div class="empty-state">暂无知识项</div>';
return;
}
// 计算总项数和分类数
const totalItems = categoriesWithItems.reduce((sum, cat) => sum + (cat.items?.length || 0), 0);
const categoryCount = categoriesWithItems.length;
// 更新统计信息
updateKnowledgeStats(categoriesWithItems, categoryCount);
// 渲染分类及知识项
let html = '<div class="knowledge-categories-container">';
categoriesWithItems.forEach(categoryData => {
const category = categoryData.category || '未分类';
const categoryItems = categoryData.items || [];
const categoryCount = categoryData.itemCount || categoryItems.length;
html += `
<div class="knowledge-category-section" data-category="${escapeHtml(category)}">
<div class="knowledge-category-header">
<div class="knowledge-category-info">
<h3 class="knowledge-category-title">${escapeHtml(category)}</h3>
<span class="knowledge-category-count">${categoryCount} 项</span>
</div>
</div>
<div class="knowledge-items-grid">
${categoryItems.map(item => renderKnowledgeItemCard(item)).join('')}
</div>
</div>
`;
});
html += '</div>';
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 = '<div class="knowledge-pagination" style="display: flex; justify-content: center; align-items: center; gap: 8px; padding: 20px; flex-wrap: wrap;">';
// 上一页按钮
html += `<button class="pagination-btn" onclick="loadKnowledgePage(${currentPage - 1})" ${currentPage <= 1 ? 'disabled style="opacity: 0.5; cursor: not-allowed;"' : ''}>上一页</button>`;
// 页码显示(显示分类数)
html += `<span style="padding: 0 12px;">第 ${currentPage} 页,共 ${totalPages} 页(共 ${total} 个分类)</span>`;
// 下一页按钮
html += `<button class="pagination-btn" onclick="loadKnowledgePage(${currentPage + 1})" ${currentPage >= totalPages ? 'disabled style="opacity: 0.5; cursor: not-allowed;"' : ''}>下一页</button>`;
html += '</div>';
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) {
</div>
${relativePath ? `<div class="knowledge-item-path">📁 ${escapeHtml(relativePath)}</div>` : ''}
</div>
${previewText ? `
<div class="knowledge-item-card-content">
<p class="knowledge-item-preview">${escapeHtml(previewText || '无内容预览')}</p>
<p class="knowledge-item-preview">${escapeHtml(previewText)}</p>
</div>
` : ''}
<div class="knowledge-item-card-footer">
<div class="knowledge-item-meta">
${displayTime ? `<span class="knowledge-item-time" title="${timeLabel}">🕒 ${displayTime}</span>` : ''}
@@ -261,27 +380,39 @@ function renderKnowledgeItemCard(item) {
`;
}
// 更新统计信息
function updateKnowledgeStats(items, categoryCount) {
// 更新统计信息(支持按分类分页的数据结构)
function updateKnowledgeStats(data, categoryCount) {
const statsContainer = document.getElementById('knowledge-stats');
if (!statsContainer) return;
const totalItems = items.length;
const totalSize = items.reduce((sum, item) => sum + (item.content?.length || 0), 0);
const sizeKB = (totalSize / 1024).toFixed(1);
// 计算当前页的知识项数
let currentPageItemCount = 0;
if (Array.isArray(data) && data.length > 0) {
// 判断是categoriesWithItems还是items数组
if (data[0].category !== undefined && data[0].items !== undefined) {
// 是按分类分页的数据结构
currentPageItemCount = data.reduce((sum, cat) => sum + (cat.items?.length || 0), 0);
} else {
// 是按项分页的数据结构(向后兼容)
currentPageItemCount = data.length;
}
}
// 总分类数(来自分页信息)
const totalCategories = knowledgePagination.total || categoryCount;
statsContainer.innerHTML = `
<div class="knowledge-stat-item">
<span class="knowledge-stat-label">总知识项</span>
<span class="knowledge-stat-value">${totalItems}</span>
<span class="knowledge-stat-label">总分类数</span>
<span class="knowledge-stat-value">${totalCategories}</span>
</div>
<div class="knowledge-stat-item">
<span class="knowledge-stat-label">分类</span>
<span class="knowledge-stat-value">${categoryCount}</span>
<span class="knowledge-stat-label">当前页分类</span>
<span class="knowledge-stat-value">${categoryCount}</span>
</div>
<div class="knowledge-stat-item">
<span class="knowledge-stat-label">总内容</span>
<span class="knowledge-stat-value">${sizeKB} KB</span>
<span class="knowledge-stat-label">当前页知识项</span>
<span class="knowledge-stat-value">${currentPageItemCount} </span>
</div>
`;
@@ -396,7 +527,8 @@ function selectKnowledgeCategory(category) {
}
});
}
loadKnowledgeItems(category);
// 切换分类时重置到第一页(如果选择了分类,API会返回该分类的所有项)
loadKnowledgeItems(category, 1, knowledgePagination.pageSize);
}
// 筛选知识项
@@ -405,32 +537,149 @@ function filterKnowledgeItems() {
if (wrapper) {
const selectedOption = wrapper.querySelector('.custom-select-option.selected');
const category = selectedOption ? selectedOption.getAttribute('data-value') : '';
loadKnowledgeItems(category);
// 重置到第一页
loadKnowledgeItems(category, 1, knowledgePagination.pageSize);
}
}
// 搜索知识项
function searchKnowledgeItems() {
const searchTerm = document.getElementById('knowledge-search').value.toLowerCase().trim();
// 处理搜索输入(带防抖)
function handleKnowledgeSearchInput() {
const searchInput = document.getElementById('knowledge-search');
const searchTerm = searchInput?.value.trim() || '';
// 清除之前的定时器
if (searchTimeout) {
clearTimeout(searchTimeout);
}
// 如果搜索框为空,立即恢复列表
if (!searchTerm) {
// 恢复原始列表
const wrapper = document.getElementById('knowledge-category-filter-wrapper');
let category = '';
if (wrapper) {
const selectedOption = wrapper.querySelector('.custom-select-option.selected');
category = selectedOption ? selectedOption.getAttribute('data-value') : '';
}
loadKnowledgeItems(category);
loadKnowledgeItems(category, 1, knowledgePagination.pageSize);
return;
}
const filtered = knowledgeItems.filter(item =>
item.title.toLowerCase().includes(searchTerm) ||
item.content.toLowerCase().includes(searchTerm) ||
item.category.toLowerCase().includes(searchTerm) ||
(item.filePath && item.filePath.toLowerCase().includes(searchTerm))
);
renderKnowledgeItems(filtered);
// 有搜索词时,延迟500ms后执行搜索(防抖)
searchTimeout = setTimeout(() => {
searchKnowledgeItems();
}, 500);
}
// 搜索知识项(后端关键字匹配,在所有数据中搜索)
async function searchKnowledgeItems() {
const searchInput = document.getElementById('knowledge-search');
const searchTerm = searchInput?.value.trim() || '';
if (!searchTerm) {
// 恢复原始列表(重置到第一页)
const wrapper = document.getElementById('knowledge-category-filter-wrapper');
let category = '';
if (wrapper) {
const selectedOption = wrapper.querySelector('.custom-select-option.selected');
category = selectedOption ? selectedOption.getAttribute('data-value') : '';
}
await loadKnowledgeItems(category, 1, knowledgePagination.pageSize);
return;
}
try {
// 获取当前选择的分类
const wrapper = document.getElementById('knowledge-category-filter-wrapper');
let category = '';
if (wrapper) {
const selectedOption = wrapper.querySelector('.custom-select-option.selected');
category = selectedOption ? selectedOption.getAttribute('data-value') : '';
}
// 调用后端API进行全量搜索
const timestamp = Date.now();
let url = `/api/knowledge/items?search=${encodeURIComponent(searchTerm)}&_t=${timestamp}`;
if (category) {
url += `&category=${encodeURIComponent(category)}`;
}
const response = await apiFetch(url, {
method: 'GET',
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0'
}
});
if (!response.ok) {
throw new Error('搜索失败');
}
const data = await response.json();
// 检查知识库功能是否启用
if (data.enabled === false) {
const container = document.getElementById('knowledge-items-list');
if (container) {
container.innerHTML = `
<div class="empty-state" style="text-align: center; padding: 40px 20px;">
<div style="font-size: 48px; margin-bottom: 20px;">📚</div>
<h3 style="margin-bottom: 10px; color: #666;">知识库功能未启用</h3>
<p style="color: #999; margin-bottom: 20px;">${data.message || '请前往系统设置启用知识检索功能'}</p>
<button onclick="switchToSettings()" style="
background: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
">前往设置</button>
</div>
`;
}
return;
}
// 处理搜索结果
const categoriesWithItems = data.categories || [];
// 渲染搜索结果
const container = document.getElementById('knowledge-items-list');
if (!container) return;
if (categoriesWithItems.length === 0) {
container.innerHTML = `
<div class="empty-state" style="text-align: center; padding: 40px 20px;">
<div style="font-size: 48px; margin-bottom: 20px;">🔍</div>
<h3 style="margin-bottom: 10px;">未找到匹配的知识项</h3>
<p style="color: #999;">关键词 "<strong>${escapeHtml(searchTerm)}</strong>" 在所有数据中没有匹配结果</p>
<p style="color: #999; margin-top: 10px; font-size: 0.9em;">请尝试其他关键词,或使用分类筛选功能</p>
</div>
`;
} else {
// 计算总项数和分类数
const totalItems = categoriesWithItems.reduce((sum, cat) => sum + (cat.items?.length || 0), 0);
const categoryCount = categoriesWithItems.length;
// 更新统计信息
updateKnowledgeStats(categoriesWithItems, categoryCount);
// 渲染搜索结果
renderKnowledgeItemsByCategories(categoriesWithItems);
}
// 搜索时隐藏分页(因为搜索结果显示所有匹配结果)
const paginationContainer = document.getElementById('knowledge-pagination');
if (paginationContainer) {
paginationContainer.innerHTML = '';
}
} catch (error) {
console.error('搜索知识项失败:', error);
showNotification('搜索失败: ' + error.message, 'error');
}
}
// 刷新知识库
@@ -450,9 +699,9 @@ async function refreshKnowledgeBase() {
} else {
showNotification(data.message || '扫描完成,没有需要索引的新项或更新项', 'success');
}
// 重新加载知识项
// 重新加载知识项(重置到第一页)
await loadKnowledgeCategories();
await loadKnowledgeItems();
await loadKnowledgeItems(knowledgePagination.currentCategory, 1, knowledgePagination.pageSize);
// 停止现有的轮询
if (indexProgressInterval) {
@@ -686,8 +935,8 @@ async function saveKnowledgeItem() {
showNotification(`${action}成功!已切换到分类"${newItemCategory}"查看新添加的知识项。`, 'success');
}
// 刷新知识项列表
await loadKnowledgeItems(categoryToShow);
// 刷新知识项列表(重置到第一页)
await loadKnowledgeItems(categoryToShow, 1, knowledgePagination.pageSize);
console.log('知识项刷新完成');
} catch (err) {
console.error('刷新数据失败:', err);
@@ -805,9 +1054,9 @@ async function deleteKnowledgeItem(id) {
// 显示成功通知
showNotification('✅ 删除成功!知识项已从系统中移除。', 'success');
// 重新加载数据以确保数据同步
// 重新加载数据以确保数据同步(保持当前页码)
await loadKnowledgeCategories();
await loadKnowledgeItems();
await loadKnowledgeItems(knowledgePagination.currentCategory, knowledgePagination.currentPage, knowledgePagination.pageSize);
} catch (error) {
console.error('删除知识项失败:', error);
@@ -821,8 +1070,8 @@ async function deleteKnowledgeItem(id) {
// 如果分类被移除了,需要恢复
if (categorySection && !categorySection.parentElement) {
// 需要重新加载来恢复
await loadKnowledgeItems();
// 需要重新加载来恢复(保持当前分页状态)
await loadKnowledgeItems(knowledgePagination.currentCategory, knowledgePagination.currentPage, knowledgePagination.pageSize);
}
}
@@ -1537,7 +1786,7 @@ if (typeof switchPage === 'function') {
if (page === 'knowledge-management') {
loadKnowledgeCategories();
loadKnowledgeItems();
loadKnowledgeItems(knowledgePagination.currentCategory, 1, knowledgePagination.pageSize);
updateIndexProgress(); // 更新索引进度
} else if (page === 'knowledge-retrieval-logs') {
loadRetrievalLogs();
+2 -1
View File
@@ -384,7 +384,7 @@
</div>
</label>
<div class="search-box">
<input type="text" id="knowledge-search" placeholder="搜索知识..." oninput="searchKnowledgeItems()" />
<input type="text" id="knowledge-search" placeholder="搜索知识..." oninput="handleKnowledgeSearchInput()" onkeydown="if(event.key==='Enter') searchKnowledgeItems()" />
<button class="btn-search" onclick="searchKnowledgeItems()" title="搜索">🔍</button>
</div>
</div>
@@ -392,6 +392,7 @@
<div id="knowledge-items-list" class="knowledge-items-list">
<div class="loading-spinner">加载中...</div>
</div>
<div id="knowledge-pagination"></div>
</div>
</div>