feat: Obsidian 知识库批量导入 + 语义搜索体验升级
- 新增 import_obsidian_kb.py 批量导入脚本(164篇入库,知识库186条) - parse_md_file 补强:来源fallback、相关装备/应用领域实体提取、 Obsidian双链去括号、原始类别/源文件路径存metadata、TYPE_MAP扩充 - search_similar 改进:智能摘要(中文2-gram拆词+加权段落匹配)、 min_similarity=0.3过滤、top_k 5→10、返回完整content_md - 前端搜索卡片升级:展开全文、关键词加粗渲染、相关度分档样式 - CLAUDE.md 状态更新 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,19 @@ import useAuthStore from '../../stores/authStore'
|
||||
import knowledgeService from '../../services/knowledgeService'
|
||||
import KnowledgeTree from '../../components/KnowledgeTree/KnowledgeTree'
|
||||
|
||||
// 渲染 snippet 中的 **keyword** 加粗标记(不使用 dangerouslySetInnerHTML)
|
||||
function renderSnippet(text) {
|
||||
if (!text) return null
|
||||
const parts = text.split(/\*\*(.+?)\*\*/g)
|
||||
return parts.map((part, i) => {
|
||||
// 奇数索引是被 ** 包裹的关键词
|
||||
if (i % 2 === 1) {
|
||||
return <strong key={i} style={{ color: '#1677ff' }}>{part}</strong>
|
||||
}
|
||||
return <span key={i}>{part}</span>
|
||||
})
|
||||
}
|
||||
|
||||
const { Dragger } = Upload
|
||||
|
||||
// source_type 枚举值(固定五类,不写死但供 Select 用)
|
||||
@@ -38,6 +51,20 @@ export default function KnowledgeBase() {
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [searchResults, setSearchResults] = useState([])
|
||||
const [searchLoading, setSearchLoading] = useState(false)
|
||||
const [expandedIds, setExpandedIds] = useState(new Set()) // 展开全文的卡片 id 集合
|
||||
|
||||
// 切换卡片展开/收起
|
||||
const toggleExpand = (id) => {
|
||||
setExpandedIds(prev => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(id)) {
|
||||
next.delete(id)
|
||||
} else {
|
||||
next.add(id)
|
||||
}
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
const fetchItems = async () => {
|
||||
setLoading(true)
|
||||
@@ -287,8 +314,8 @@ export default function KnowledgeBase() {
|
||||
{searchQuery && (
|
||||
<span style={{ fontSize: 12, color: '#888' }}>
|
||||
{searchResults.length > 0
|
||||
? `找到 ${searchResults.length} 条相关结果`
|
||||
: '无相关结果'}
|
||||
? `找到 ${searchResults.length} 条相关结果(相关度 > 30%)`
|
||||
: '未找到相关内容,试试换个说法?'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -299,63 +326,110 @@ export default function KnowledgeBase() {
|
||||
<div style={{ fontSize: 13, color: '#666', marginBottom: 8, fontWeight: 500 }}>
|
||||
搜索结果
|
||||
</div>
|
||||
{searchResults.map(item => (
|
||||
<Card
|
||||
key={item.id}
|
||||
size="small"
|
||||
style={{ borderRadius: 10, marginBottom: 8 }}
|
||||
bodyStyle={{ padding: '12px 16px' }}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontWeight: 600, fontSize: 14, color: '#3b4a3b', marginBottom: 4 }}>
|
||||
{item.title}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center', marginBottom: 6 }}>
|
||||
<Tag color="default" style={{ borderRadius: 6, fontSize: 12 }}>
|
||||
{SOURCE_TYPE_LABEL[item.source_type] || item.source_type}
|
||||
</Tag>
|
||||
{item.author && (
|
||||
<span style={{ fontSize: 12, color: '#888' }}>作者:{item.author}</span>
|
||||
{searchResults.map(item => {
|
||||
const isHigh = item.similarity >= 0.7
|
||||
const isLow = item.similarity < 0.5
|
||||
const isExpanded = expandedIds.has(item.id)
|
||||
return (
|
||||
<Card
|
||||
key={item.id}
|
||||
size="small"
|
||||
style={{
|
||||
borderRadius: 10,
|
||||
marginBottom: 8,
|
||||
opacity: isLow ? 0.65 : 1,
|
||||
background: isLow ? '#fafafa' : undefined,
|
||||
borderLeft: isHigh ? '3px solid #1677ff' : undefined,
|
||||
}}
|
||||
bodyStyle={{ padding: '12px 16px' }}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontWeight: 600, fontSize: 14, color: '#3b4a3b', marginBottom: 4 }}>
|
||||
{item.title}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center', marginBottom: 6 }}>
|
||||
<Tag color="default" style={{ borderRadius: 6, fontSize: 12 }}>
|
||||
{SOURCE_TYPE_LABEL[item.source_type] || item.source_type}
|
||||
</Tag>
|
||||
{item.author && (
|
||||
<span style={{ fontSize: 12, color: '#888' }}>作者:{item.author}</span>
|
||||
)}
|
||||
{item.source_detail && (
|
||||
<span style={{ fontSize: 12, color: '#888' }}>出处:{item.source_detail}</span>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: '#555',
|
||||
background: '#f5f5f5',
|
||||
borderRadius: 6,
|
||||
padding: '6px 10px',
|
||||
lineHeight: 1.6,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 3,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
}}
|
||||
>
|
||||
{renderSnippet(item.snippet)}
|
||||
</div>
|
||||
{/* 展开全文区域 */}
|
||||
{isExpanded && item.content_md && (
|
||||
<div
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: '#444',
|
||||
background: '#fafafa',
|
||||
borderRadius: 6,
|
||||
padding: '10px 12px',
|
||||
marginTop: 8,
|
||||
lineHeight: 1.7,
|
||||
whiteSpace: 'pre-wrap',
|
||||
maxHeight: 400,
|
||||
overflowY: 'auto',
|
||||
border: '1px solid #e8e8e8',
|
||||
}}
|
||||
>
|
||||
{item.content_md}
|
||||
</div>
|
||||
)}
|
||||
{item.source_detail && (
|
||||
<span style={{ fontSize: 12, color: '#888' }}>出处:{item.source_detail}</span>
|
||||
{/* 展开/收起按钮 */}
|
||||
{item.content_md && (
|
||||
<div style={{ marginTop: 6 }}>
|
||||
<span
|
||||
onClick={() => toggleExpand(item.id)}
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: '#1677ff',
|
||||
cursor: 'pointer',
|
||||
userSelect: 'none',
|
||||
}}
|
||||
>
|
||||
{isExpanded ? '收起全文 ▲' : '展开全文 ▼'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: '#555',
|
||||
background: '#f5f5f5',
|
||||
borderRadius: 6,
|
||||
padding: '6px 10px',
|
||||
lineHeight: 1.6,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 3,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
}}
|
||||
>
|
||||
{item.snippet}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{
|
||||
marginLeft: 16,
|
||||
minWidth: 56,
|
||||
textAlign: 'center',
|
||||
background: item.similarity >= 0.7 ? '#e6f4ff' : '#f5f5f5',
|
||||
background: isHigh ? '#e6f4ff' : '#f5f5f5',
|
||||
borderRadius: 8,
|
||||
padding: '4px 8px',
|
||||
}}>
|
||||
<div style={{ fontSize: 18, fontWeight: 700, color: item.similarity >= 0.7 ? '#1677ff' : '#666' }}>
|
||||
<div style={{ fontSize: 18, fontWeight: 700, color: isHigh ? '#1677ff' : '#666' }}>
|
||||
{Math.max(0, Math.round(item.similarity * 100))}%
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: '#888' }}>相关度</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -56,9 +56,9 @@ const knowledgeService = {
|
||||
/**
|
||||
* 语义检索:输入一段文字,返回最相关的知识库条目
|
||||
* @param {string} queryText - 查询文字
|
||||
* @param {number} topK - 返回条数,默认 5
|
||||
* @param {number} topK - 返回条数,默认 10
|
||||
*/
|
||||
async searchItems(queryText, topK = 5) {
|
||||
async searchItems(queryText, topK = 10) {
|
||||
const resp = await http.post('/knowledge/search', {
|
||||
query: queryText,
|
||||
top_k: topK,
|
||||
|
||||
Reference in New Issue
Block a user