mirror of
https://github.com/Tencent/WeKnora.git
synced 2026-09-19 02:18:25 +08:00
feat: add pinning functionality for knowledge bases
- Introduced a new API endpoint to toggle the pin status of knowledge bases, allowing users to pin important entries to the top of the list. - Updated the frontend to include UI elements for pinning and unpinning knowledge bases, with corresponding success and error messages. - Enhanced localization support by adding translations for pinning actions in English, Korean, Russian, and Chinese. - Modified the database schema to include `is_pinned` and `pinned_at` fields for knowledge bases, enabling persistent pinning status. These changes improve the organization and accessibility of knowledge bases, enhancing user experience by allowing quick access to prioritized entries.
This commit is contained in:
@@ -45,6 +45,10 @@ export function copyKnowledgeBase(data: { source_id: string; target_id?: string
|
||||
return post(`/api/v1/knowledge-bases/copy`, data);
|
||||
}
|
||||
|
||||
export function togglePinKnowledgeBase(id: string) {
|
||||
return put(`/api/v1/knowledge-bases/${id}/pin`);
|
||||
}
|
||||
|
||||
// 知识文件 API(基于具体知识库)
|
||||
// data.tag_id: 可选,指定知识所属的分类ID
|
||||
export function uploadKnowledgeFile(kbId: string, data: { file: File; tag_id?: string; [key: string]: any } = { file: new File([], '') }, onProgress?: (progressEvent: any) => void) {
|
||||
|
||||
@@ -1173,6 +1173,13 @@ export default {
|
||||
menu: {
|
||||
viewDetails: 'View Details',
|
||||
},
|
||||
pin: {
|
||||
pin: 'Pin to Top',
|
||||
unpin: 'Unpin',
|
||||
pinSuccess: 'Pinned',
|
||||
unpinSuccess: 'Unpinned',
|
||||
failed: 'Operation failed',
|
||||
},
|
||||
messages: {
|
||||
deleted: 'Knowledge base deleted',
|
||||
deleteFailed: 'Failed to delete knowledge base'
|
||||
|
||||
@@ -1603,6 +1603,13 @@ export default {
|
||||
menu: {
|
||||
viewDetails: "세부 사항을 확인하세요",
|
||||
},
|
||||
pin: {
|
||||
pin: "상단 고정",
|
||||
unpin: "고정 해제",
|
||||
pinSuccess: "상단에 고정됨",
|
||||
unpinSuccess: "고정 해제됨",
|
||||
failed: "작업 실패",
|
||||
},
|
||||
detail: {
|
||||
title: "공유 지식베이스",
|
||||
overview: "개요",
|
||||
|
||||
@@ -1045,6 +1045,13 @@ export default {
|
||||
confirmMessage: 'Удалить базу знаний «{name}»? Отменить действие будет невозможно.',
|
||||
confirmButton: 'Удалить'
|
||||
},
|
||||
pin: {
|
||||
pin: 'Закрепить',
|
||||
unpin: 'Открепить',
|
||||
pinSuccess: 'Закреплено',
|
||||
unpinSuccess: 'Откреплено',
|
||||
failed: 'Операция не удалась',
|
||||
},
|
||||
messages: {
|
||||
deleted: 'База знаний удалена',
|
||||
deleteFailed: 'Не удалось удалить базу знаний'
|
||||
|
||||
@@ -1581,6 +1581,13 @@ export default {
|
||||
menu: {
|
||||
viewDetails: "查看详情",
|
||||
},
|
||||
pin: {
|
||||
pin: "置顶",
|
||||
unpin: "取消置顶",
|
||||
pinSuccess: "已置顶",
|
||||
unpinSuccess: "已取消置顶",
|
||||
failed: "操作失败",
|
||||
},
|
||||
detail: {
|
||||
title: "共享知识库",
|
||||
overview: "概览",
|
||||
|
||||
@@ -93,6 +93,10 @@
|
||||
:ref="el => { if (highlightedKbId !== null && highlightedKbId === kb.id && el) highlightedCardRef = el as HTMLElement }"
|
||||
@click="handleCardClick(kb)"
|
||||
>
|
||||
<!-- 置顶标识 -->
|
||||
<div v-if="kb.is_pinned" class="pin-indicator">
|
||||
<t-icon name="pin-filled" size="14px" />
|
||||
</div>
|
||||
<!-- 卡片头部 -->
|
||||
<div class="card-header">
|
||||
<span class="card-title" :title="kb.name">{{ kb.name }}</span>
|
||||
@@ -107,6 +111,10 @@
|
||||
</div>
|
||||
<template #content>
|
||||
<div class="popup-menu" @click.stop>
|
||||
<div class="popup-menu-item" @click.stop="handleTogglePinById(kb.id)">
|
||||
<t-icon class="menu-icon" :name="kb.is_pinned ? 'pin-filled' : 'pin'" />
|
||||
<span>{{ kb.is_pinned ? $t('knowledgeList.pin.unpin') : $t('knowledgeList.pin.pin') }}</span>
|
||||
</div>
|
||||
<div class="popup-menu-item" @click.stop="handleSettingsById(kb.id)">
|
||||
<t-icon class="menu-icon" name="setting" />
|
||||
<span>{{ $t('knowledgeBase.settings') }}</span>
|
||||
@@ -251,6 +259,10 @@
|
||||
:ref="el => { if (highlightedKbId !== null && highlightedKbId === kb.id && el) highlightedCardRef = el as HTMLElement }"
|
||||
@click="handleCardClick(kb)"
|
||||
>
|
||||
<!-- 置顶标识 -->
|
||||
<div v-if="kb.is_pinned" class="pin-indicator">
|
||||
<t-icon name="pin-filled" size="14px" />
|
||||
</div>
|
||||
<!-- 卡片头部 -->
|
||||
<div class="card-header">
|
||||
<span class="card-title" :title="kb.name">{{ kb.name }}</span>
|
||||
@@ -272,6 +284,10 @@
|
||||
</div>
|
||||
<template #content>
|
||||
<div class="popup-menu" @click.stop>
|
||||
<div class="popup-menu-item" @click.stop="handleTogglePin(kb)">
|
||||
<t-icon class="menu-icon" :name="kb.is_pinned ? 'pin-filled' : 'pin'" />
|
||||
<span>{{ kb.is_pinned ? $t('knowledgeList.pin.unpin') : $t('knowledgeList.pin.pin') }}</span>
|
||||
</div>
|
||||
<div class="popup-menu-item" @click.stop="handleSettings(kb)">
|
||||
<t-icon class="menu-icon" name="setting" />
|
||||
<span>{{ $t('knowledgeBase.settings') }}</span>
|
||||
@@ -586,7 +602,7 @@
|
||||
import { onMounted, onUnmounted, ref, computed, watch, nextTick } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { MessagePlugin, Icon as TIcon } from 'tdesign-vue-next'
|
||||
import { listKnowledgeBases, deleteKnowledgeBase } from '@/api/knowledge-base'
|
||||
import { listKnowledgeBases, deleteKnowledgeBase, togglePinKnowledgeBase } from '@/api/knowledge-base'
|
||||
import { formatStringDate } from '@/utils/index'
|
||||
import { useUIStore } from '@/stores/ui'
|
||||
import { useOrganizationStore } from '@/stores/organization'
|
||||
@@ -620,9 +636,10 @@ interface KB {
|
||||
question_generation_config?: { enabled?: boolean; question_count?: number };
|
||||
knowledge_count?: number;
|
||||
chunk_count?: number;
|
||||
isProcessing?: boolean; // 是否有正在处理的导入任务
|
||||
processing_count?: number; // 正在处理的文档数量(仅文档类型)
|
||||
share_count?: number; // 共享给组织的数量
|
||||
isProcessing?: boolean;
|
||||
processing_count?: number;
|
||||
share_count?: number;
|
||||
is_pinned?: boolean;
|
||||
}
|
||||
|
||||
const kbs = ref<KB[]>([])
|
||||
@@ -850,6 +867,35 @@ const handleDeleteById = (id: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleTogglePin = async (kb: KB) => {
|
||||
kb.showMore = false
|
||||
try {
|
||||
const res: any = await togglePinKnowledgeBase(kb.id)
|
||||
if (res.success) {
|
||||
MessagePlugin.success(
|
||||
res.data.is_pinned ? t('knowledgeList.pin.pinSuccess') : t('knowledgeList.pin.unpinSuccess')
|
||||
)
|
||||
fetchList()
|
||||
}
|
||||
} catch {
|
||||
MessagePlugin.error(t('knowledgeList.pin.failed'))
|
||||
}
|
||||
}
|
||||
|
||||
const handleTogglePinById = async (id: string) => {
|
||||
try {
|
||||
const res: any = await togglePinKnowledgeBase(id)
|
||||
if (res.success) {
|
||||
MessagePlugin.success(
|
||||
res.data.is_pinned ? t('knowledgeList.pin.pinSuccess') : t('knowledgeList.pin.unpinSuccess')
|
||||
)
|
||||
fetchList()
|
||||
}
|
||||
} catch {
|
||||
MessagePlugin.error(t('knowledgeList.pin.failed'))
|
||||
}
|
||||
}
|
||||
|
||||
const handleShare = (kb: KB) => {
|
||||
// 手动关闭弹窗
|
||||
kb.showMore = false
|
||||
@@ -1616,6 +1662,15 @@ const handleUploadFinishedEvent = (event: Event) => {
|
||||
}
|
||||
}
|
||||
|
||||
.pin-indicator {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
color: var(--td-brand-color);
|
||||
z-index: 2;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
// 确保内容在装饰之上
|
||||
.card-header,
|
||||
.card-content,
|
||||
|
||||
@@ -3,6 +3,7 @@ package repository
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/types"
|
||||
"github.com/Tencent/WeKnora/internal/types/interfaces"
|
||||
@@ -77,12 +78,40 @@ func (r *knowledgeBaseRepository) ListKnowledgeBasesByTenantID(
|
||||
) ([]*types.KnowledgeBase, error) {
|
||||
var kbs []*types.KnowledgeBase
|
||||
if err := r.db.WithContext(ctx).Where("tenant_id = ? AND is_temporary = ?", tenantID, false).
|
||||
Order("created_at DESC").Find(&kbs).Error; err != nil {
|
||||
Order("is_pinned DESC, pinned_at DESC, created_at DESC").Find(&kbs).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return kbs, nil
|
||||
}
|
||||
|
||||
// TogglePinKnowledgeBase toggles the pin status of a knowledge base
|
||||
func (r *knowledgeBaseRepository) TogglePinKnowledgeBase(ctx context.Context, id string, tenantID uint64) (*types.KnowledgeBase, error) {
|
||||
var kb types.KnowledgeBase
|
||||
if err := r.db.WithContext(ctx).Where("id = ? AND tenant_id = ?", id, tenantID).First(&kb).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrKnowledgeBaseNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if kb.IsPinned {
|
||||
kb.IsPinned = false
|
||||
kb.PinnedAt = nil
|
||||
} else {
|
||||
kb.IsPinned = true
|
||||
now := time.Now()
|
||||
kb.PinnedAt = &now
|
||||
}
|
||||
|
||||
if err := r.db.WithContext(ctx).Model(&kb).Updates(map[string]interface{}{
|
||||
"is_pinned": kb.IsPinned,
|
||||
"pinned_at": kb.PinnedAt,
|
||||
}).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &kb, nil
|
||||
}
|
||||
|
||||
// UpdateKnowledgeBase updates a knowledge base
|
||||
func (r *knowledgeBaseRepository) UpdateKnowledgeBase(ctx context.Context, kb *types.KnowledgeBase) error {
|
||||
return r.db.WithContext(ctx).Save(kb).Error
|
||||
|
||||
@@ -312,6 +312,23 @@ func (s *knowledgeBaseService) UpdateKnowledgeBase(ctx context.Context,
|
||||
return kb, nil
|
||||
}
|
||||
|
||||
// TogglePinKnowledgeBase toggles the pin status of a knowledge base
|
||||
func (s *knowledgeBaseService) TogglePinKnowledgeBase(ctx context.Context, id string) (*types.KnowledgeBase, error) {
|
||||
if id == "" {
|
||||
return nil, errors.New("knowledge base ID cannot be empty")
|
||||
}
|
||||
tenantID := types.MustTenantIDFromContext(ctx)
|
||||
kb, err := s.repo.TogglePinKnowledgeBase(ctx, id, tenantID)
|
||||
if err != nil {
|
||||
logger.ErrorWithFields(ctx, err, map[string]interface{}{
|
||||
"knowledge_base_id": id,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
logger.Infof(ctx, "Knowledge base pin toggled, ID: %s, is_pinned: %v", id, kb.IsPinned)
|
||||
return kb, nil
|
||||
}
|
||||
|
||||
// DeleteKnowledgeBase deletes a knowledge base by its ID
|
||||
// This method marks the knowledge base as deleted and enqueues an async task
|
||||
// to handle the heavy cleanup operations (embeddings, chunks, files, graph data)
|
||||
|
||||
@@ -386,6 +386,43 @@ func (h *KnowledgeBaseHandler) ListKnowledgeBases(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// TogglePinKnowledgeBase godoc
|
||||
// @Summary 置顶/取消置顶知识库
|
||||
// @Description 切换知识库的置顶状态
|
||||
// @Tags 知识库
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "知识库ID"
|
||||
// @Success 200 {object} map[string]interface{} "更新后的知识库"
|
||||
// @Failure 404 {object} errors.AppError "知识库不存在"
|
||||
// @Security Bearer
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /knowledge-bases/{id}/pin [put]
|
||||
func (h *KnowledgeBaseHandler) TogglePinKnowledgeBase(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
c.Error(apperrors.NewBadRequestError("knowledge base ID is required"))
|
||||
return
|
||||
}
|
||||
|
||||
kb, err := h.service.TogglePinKnowledgeBase(ctx, id)
|
||||
if err != nil {
|
||||
if stderrors.Is(err, repository.ErrKnowledgeBaseNotFound) {
|
||||
c.Error(apperrors.NewNotFoundError("knowledge base not found"))
|
||||
return
|
||||
}
|
||||
logger.ErrorWithFields(ctx, err, nil)
|
||||
c.Error(apperrors.NewInternalServerError(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": kb,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateKnowledgeBaseRequest defines the request body structure for updating a knowledge base
|
||||
type UpdateKnowledgeBaseRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
|
||||
@@ -246,6 +246,8 @@ func RegisterKnowledgeBaseRoutes(r *gin.RouterGroup, handler *handler.KnowledgeB
|
||||
kb.PUT("/:id", handler.UpdateKnowledgeBase)
|
||||
// 删除知识库
|
||||
kb.DELETE("/:id", handler.DeleteKnowledgeBase)
|
||||
// 置顶/取消置顶知识库
|
||||
kb.PUT("/:id/pin", handler.TogglePinKnowledgeBase)
|
||||
// 混合搜索
|
||||
kb.GET("/:id/hybrid-search", handler.HybridSearch)
|
||||
// 拷贝知识库
|
||||
|
||||
@@ -80,6 +80,9 @@ type KnowledgeBaseService interface {
|
||||
// - Possible errors such as not existing, insufficient permissions, etc.
|
||||
DeleteKnowledgeBase(ctx context.Context, id string) error
|
||||
|
||||
// TogglePinKnowledgeBase toggles the pin status of a knowledge base
|
||||
TogglePinKnowledgeBase(ctx context.Context, id string) (*types.KnowledgeBase, error)
|
||||
|
||||
// HybridSearch performs hybrid search (vector + keywords) in the knowledge base
|
||||
// Parameters:
|
||||
// - ctx: Context information
|
||||
@@ -190,4 +193,7 @@ type KnowledgeBaseRepository interface {
|
||||
// Returns:
|
||||
// - Possible errors such as record not existing, database errors, etc.
|
||||
DeleteKnowledgeBase(ctx context.Context, id string) error
|
||||
|
||||
// TogglePinKnowledgeBase toggles the pin status of a knowledge base
|
||||
TogglePinKnowledgeBase(ctx context.Context, id string, tenantID uint64) (*types.KnowledgeBase, error)
|
||||
}
|
||||
|
||||
@@ -70,6 +70,10 @@ type KnowledgeBase struct {
|
||||
FAQConfig *FAQConfig `yaml:"faq_config" json:"faq_config" gorm:"column:faq_config;type:json"`
|
||||
// QuestionGenerationConfig stores question generation configuration for document knowledge bases
|
||||
QuestionGenerationConfig *QuestionGenerationConfig `yaml:"question_generation_config" json:"question_generation_config" gorm:"column:question_generation_config;type:json"`
|
||||
// Whether this knowledge base is pinned to the top of the list
|
||||
IsPinned bool `yaml:"is_pinned" json:"is_pinned" gorm:"default:false"`
|
||||
// Time when the knowledge base was pinned (nil if not pinned)
|
||||
PinnedAt *time.Time `yaml:"pinned_at" json:"pinned_at"`
|
||||
// Creation time of the knowledge base
|
||||
CreatedAt time.Time `yaml:"created_at" json:"created_at"`
|
||||
// Last updated time of the knowledge base
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
-- Remove pin (置顶) support from knowledge bases
|
||||
ALTER TABLE knowledge_bases DROP COLUMN IF EXISTS pinned_at;
|
||||
ALTER TABLE knowledge_bases DROP COLUMN IF EXISTS is_pinned;
|
||||
@@ -0,0 +1,3 @@
|
||||
-- Add pin (置顶) support for knowledge bases
|
||||
ALTER TABLE knowledge_bases ADD COLUMN IF NOT EXISTS is_pinned BOOLEAN NOT NULL DEFAULT false;
|
||||
ALTER TABLE knowledge_bases ADD COLUMN IF NOT EXISTS pinned_at TIMESTAMP WITH TIME ZONE NULL;
|
||||
Reference in New Issue
Block a user