mirror of
https://github.com/EthanMarti/infio-copilot.git
synced 2026-05-06 06:56:29 +00:00
Optimize the search view component, add workspace statistics and RAG vector initialization features, update internationalization support, improve user interaction prompts, enhance log output, and ensure better user experience and code readability.
This commit is contained in:
@@ -723,7 +723,7 @@ const InsightView = () => {
|
||||
<div className="obsidian-confirm-dialog-info-item">
|
||||
<strong>{t('insights.initConfirm.modelLabel')}</strong>
|
||||
<span className="obsidian-confirm-dialog-model">
|
||||
{settings.chatModelProvider} / {settings.chatModelId || t('insights.initConfirm.defaultModel')}
|
||||
{settings.chatModelProvider}/{settings.chatModelId || t('insights.initConfirm.defaultModel')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="obsidian-confirm-dialog-info-item">
|
||||
|
||||
@@ -69,6 +69,35 @@ const SearchView = () => {
|
||||
// 当前搜索范围信息
|
||||
const [currentSearchScope, setCurrentSearchScope] = useState<string>('')
|
||||
|
||||
// 统计信息状态
|
||||
const [statisticsInfo, setStatisticsInfo] = useState<{
|
||||
totalFiles: number
|
||||
totalChunks: number
|
||||
} | null>(null)
|
||||
const [isLoadingStats, setIsLoadingStats] = useState(false)
|
||||
|
||||
// 工作区 RAG 向量初始化状态
|
||||
const [isInitializingRAG, setIsInitializingRAG] = useState(false)
|
||||
const [ragInitProgress, setRAGInitProgress] = useState<{
|
||||
type: 'indexing' | 'querying' | 'querying-done'
|
||||
indexProgress?: {
|
||||
completedChunks: number
|
||||
totalChunks: number
|
||||
totalFiles: number
|
||||
}
|
||||
} | null>(null)
|
||||
const [ragInitSuccess, setRAGInitSuccess] = useState<{
|
||||
show: boolean
|
||||
totalFiles?: number
|
||||
totalChunks?: number
|
||||
workspaceName?: string
|
||||
}>({ show: false })
|
||||
|
||||
// 删除和确认对话框状态
|
||||
const [isDeleting, setIsDeleting] = useState(false)
|
||||
const [showRAGInitConfirm, setShowRAGInitConfirm] = useState(false)
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
||||
|
||||
const handleSearch = useCallback(async (editorState?: SerializedEditorState) => {
|
||||
let searchTerm = ''
|
||||
|
||||
@@ -171,6 +200,148 @@ const SearchView = () => {
|
||||
}
|
||||
}, [searchMode, handleSearch]) // 监听搜索模式变化
|
||||
|
||||
// 加载统计信息
|
||||
const loadStatistics = useCallback(async () => {
|
||||
setIsLoadingStats(true)
|
||||
|
||||
try {
|
||||
// 获取当前工作区
|
||||
let currentWorkspace: Workspace | null = null
|
||||
if (settings.workspace && settings.workspace !== 'vault') {
|
||||
currentWorkspace = await workspaceManager.findByName(String(settings.workspace))
|
||||
}
|
||||
|
||||
const ragEngine = await getRAGEngine()
|
||||
const stats = await ragEngine.getWorkspaceStatistics(currentWorkspace)
|
||||
setStatisticsInfo(stats)
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载统计信息失败:', error)
|
||||
setStatisticsInfo({ totalFiles: 0, totalChunks: 0 })
|
||||
} finally {
|
||||
setIsLoadingStats(false)
|
||||
}
|
||||
}, [getRAGEngine, settings, workspaceManager])
|
||||
|
||||
// 初始化工作区 RAG 向量
|
||||
const initializeWorkspaceRAG = useCallback(async () => {
|
||||
setIsInitializingRAG(true)
|
||||
setRAGInitProgress(null)
|
||||
|
||||
try {
|
||||
// 获取当前工作区
|
||||
let currentWorkspace: Workspace | null = null
|
||||
if (settings.workspace && settings.workspace !== 'vault') {
|
||||
currentWorkspace = await workspaceManager.findByName(String(settings.workspace))
|
||||
}
|
||||
|
||||
if (!currentWorkspace) {
|
||||
// 如果没有当前工作区,使用默认的 vault 工作区
|
||||
currentWorkspace = await workspaceManager.ensureDefaultVaultWorkspace()
|
||||
}
|
||||
|
||||
const ragEngine = await getRAGEngine()
|
||||
|
||||
// 使用新的 updateWorkspaceIndex 方法
|
||||
await ragEngine.updateWorkspaceIndex(
|
||||
currentWorkspace,
|
||||
{ reindexAll: true },
|
||||
(progress) => {
|
||||
setRAGInitProgress(progress)
|
||||
}
|
||||
)
|
||||
|
||||
// 刷新统计信息
|
||||
await loadStatistics()
|
||||
|
||||
// 显示成功消息
|
||||
console.log(`✅ 工作区 RAG 向量初始化完成: ${currentWorkspace.name}`)
|
||||
|
||||
// 显示成功状态
|
||||
setRAGInitSuccess({
|
||||
show: true,
|
||||
totalFiles: ragInitProgress?.indexProgress?.totalFiles || 0,
|
||||
totalChunks: ragInitProgress?.indexProgress?.totalChunks || 0,
|
||||
workspaceName: currentWorkspace.name
|
||||
})
|
||||
|
||||
// 3秒后自动隐藏成功消息
|
||||
setTimeout(() => {
|
||||
setRAGInitSuccess({ show: false })
|
||||
}, 5000)
|
||||
|
||||
} catch (error) {
|
||||
console.error('工作区 RAG 向量初始化失败:', error)
|
||||
setRAGInitSuccess({ show: false })
|
||||
} finally {
|
||||
setIsInitializingRAG(false)
|
||||
setRAGInitProgress(null)
|
||||
}
|
||||
}, [getRAGEngine, settings, workspaceManager, loadStatistics])
|
||||
|
||||
// 清除工作区索引
|
||||
const clearWorkspaceIndex = useCallback(async () => {
|
||||
setIsDeleting(true)
|
||||
|
||||
try {
|
||||
// 获取当前工作区
|
||||
let currentWorkspace: Workspace | null = null
|
||||
if (settings.workspace && settings.workspace !== 'vault') {
|
||||
currentWorkspace = await workspaceManager.findByName(String(settings.workspace))
|
||||
}
|
||||
|
||||
const ragEngine = await getRAGEngine()
|
||||
await ragEngine.clearWorkspaceIndex(currentWorkspace)
|
||||
|
||||
// 刷新统计信息
|
||||
await loadStatistics()
|
||||
|
||||
console.log('✅ 工作区索引清除完成')
|
||||
|
||||
} catch (error) {
|
||||
console.error('清除工作区索引失败:', error)
|
||||
} finally {
|
||||
setIsDeleting(false)
|
||||
}
|
||||
}, [getRAGEngine, settings, workspaceManager, loadStatistics])
|
||||
|
||||
// 组件加载时自动获取统计信息
|
||||
useEffect(() => {
|
||||
loadStatistics()
|
||||
}, [loadStatistics])
|
||||
|
||||
// 确认初始化/更新 RAG 向量
|
||||
const handleInitWorkspaceRAG = useCallback(() => {
|
||||
setShowRAGInitConfirm(true)
|
||||
}, [])
|
||||
|
||||
// 确认删除工作区索引
|
||||
const handleDeleteWorkspaceIndex = useCallback(() => {
|
||||
setShowDeleteConfirm(true)
|
||||
}, [])
|
||||
|
||||
// 确认初始化 RAG 向量
|
||||
const confirmInitWorkspaceRAG = useCallback(async () => {
|
||||
setShowRAGInitConfirm(false)
|
||||
await initializeWorkspaceRAG()
|
||||
}, [initializeWorkspaceRAG])
|
||||
|
||||
// 确认删除工作区索引
|
||||
const confirmDeleteWorkspaceIndex = useCallback(async () => {
|
||||
setShowDeleteConfirm(false)
|
||||
await clearWorkspaceIndex()
|
||||
}, [clearWorkspaceIndex])
|
||||
|
||||
// 取消初始化确认
|
||||
const cancelRAGInitConfirm = useCallback(() => {
|
||||
setShowRAGInitConfirm(false)
|
||||
}, [])
|
||||
|
||||
// 取消删除确认
|
||||
const cancelDeleteConfirm = useCallback(() => {
|
||||
setShowDeleteConfirm(false)
|
||||
}, [])
|
||||
|
||||
const handleResultClick = (result: Omit<SelectVector, 'embedding'> & { similarity: number }) => {
|
||||
// 如果用户正在选择文本,不触发点击事件
|
||||
const selection = window.getSelection()
|
||||
@@ -355,36 +526,80 @@ const SearchView = () => {
|
||||
|
||||
return (
|
||||
<div className="obsidian-search-container">
|
||||
{/* 搜索输入框 */}
|
||||
<div className="obsidian-search-header">
|
||||
<SearchInputWithActions
|
||||
ref={searchInputRef}
|
||||
initialSerializedEditorState={searchEditorState}
|
||||
onChange={setSearchEditorState}
|
||||
onSubmit={handleSearch}
|
||||
mentionables={mentionables}
|
||||
setMentionables={setMentionables}
|
||||
placeholder="语义搜索(按回车键搜索)..."
|
||||
autoFocus={true}
|
||||
disabled={isSearching}
|
||||
/>
|
||||
|
||||
{/* 搜索模式切换 */}
|
||||
<div className="obsidian-search-mode-toggle">
|
||||
<button
|
||||
className={`obsidian-search-mode-btn ${searchMode === 'notes' ? 'active' : ''}`}
|
||||
onClick={() => setSearchMode('notes')}
|
||||
title="搜索原始笔记内容"
|
||||
>
|
||||
📝 原始笔记
|
||||
</button>
|
||||
<button
|
||||
className={`obsidian-search-mode-btn ${searchMode === 'insights' ? 'active' : ''}`}
|
||||
onClick={() => setSearchMode('insights')}
|
||||
title="搜索 AI 洞察内容"
|
||||
>
|
||||
🧠 AI 洞察
|
||||
</button>
|
||||
{/* 头部信息 */}
|
||||
<div className="obsidian-search-header-wrapper">
|
||||
<div className="obsidian-search-title">
|
||||
<h3>语义索引</h3>
|
||||
<div className="obsidian-search-actions">
|
||||
<button
|
||||
onClick={handleInitWorkspaceRAG}
|
||||
disabled={isInitializingRAG || isDeleting || isSearching}
|
||||
className="obsidian-search-init-btn"
|
||||
title={statisticsInfo && (statisticsInfo.totalFiles > 0 || statisticsInfo.totalChunks > 0) ? '更新索引' : '初始化索引'}
|
||||
>
|
||||
{isInitializingRAG ? '🔄 正在初始化...' : (statisticsInfo && (statisticsInfo.totalFiles > 0 || statisticsInfo.totalChunks > 0) ? '🔄 更新索引' : '🚀 初始化索引')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDeleteWorkspaceIndex}
|
||||
disabled={isDeleting || isInitializingRAG || isSearching}
|
||||
className="obsidian-search-delete-btn"
|
||||
title="清除索引"
|
||||
>
|
||||
{isDeleting ? '🗑️ 正在清除...' : '🗑️ 清除索引'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 统计信息 */}
|
||||
{!isLoadingStats && statisticsInfo && (
|
||||
<div className="obsidian-search-stats">
|
||||
<div className="obsidian-search-stats-overview">
|
||||
<div className="obsidian-search-stats-main">
|
||||
<span className="obsidian-search-stats-number">{statisticsInfo.totalChunks}</span>
|
||||
<span className="obsidian-search-stats-label">个向量块</span>
|
||||
</div>
|
||||
<div className="obsidian-search-stats-breakdown">
|
||||
<div className="obsidian-search-stats-item">
|
||||
<span className="obsidian-search-stats-item-icon">📄</span>
|
||||
<span className="obsidian-search-stats-item-value">{statisticsInfo.totalFiles}</span>
|
||||
<span className="obsidian-search-stats-item-label">文件</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 搜索输入框 */}
|
||||
<div className="obsidian-search-input-section">
|
||||
<SearchInputWithActions
|
||||
ref={searchInputRef}
|
||||
initialSerializedEditorState={searchEditorState}
|
||||
onChange={setSearchEditorState}
|
||||
onSubmit={handleSearch}
|
||||
mentionables={mentionables}
|
||||
setMentionables={setMentionables}
|
||||
placeholder="语义搜索(按回车键搜索)..."
|
||||
autoFocus={true}
|
||||
disabled={isSearching}
|
||||
/>
|
||||
|
||||
{/* 搜索模式切换 */}
|
||||
<div className="obsidian-search-mode-toggle">
|
||||
<button
|
||||
className={`obsidian-search-mode-btn ${searchMode === 'notes' ? 'active' : ''}`}
|
||||
onClick={() => setSearchMode('notes')}
|
||||
title="搜索原始笔记内容"
|
||||
>
|
||||
📝 原始笔记
|
||||
</button>
|
||||
<button
|
||||
className={`obsidian-search-mode-btn ${searchMode === 'insights' ? 'active' : ''}`}
|
||||
onClick={() => setSearchMode('insights')}
|
||||
title="搜索 AI 洞察内容"
|
||||
>
|
||||
🧠 AI 洞察
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -398,11 +613,6 @@ const SearchView = () => {
|
||||
`${insightGroupedResults.length} 个文件,${insightResults.length} 个洞察`
|
||||
)}
|
||||
</div>
|
||||
{currentSearchScope && (
|
||||
<div className="obsidian-search-scope">
|
||||
搜索范围: {currentSearchScope}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -413,6 +623,151 @@ const SearchView = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* RAG 初始化进度 */}
|
||||
{isInitializingRAG && (
|
||||
<div className="obsidian-rag-initializing">
|
||||
<div className="obsidian-rag-init-header">
|
||||
<h4>正在初始化工作区 RAG 向量索引</h4>
|
||||
<p>为当前工作区的文件建立向量索引,提高搜索精度</p>
|
||||
</div>
|
||||
{ragInitProgress && ragInitProgress.type === 'indexing' && ragInitProgress.indexProgress && (
|
||||
<div className="obsidian-rag-progress">
|
||||
<div className="obsidian-rag-progress-info">
|
||||
<span className="obsidian-rag-progress-stage">建立向量索引</span>
|
||||
<span className="obsidian-rag-progress-counter">
|
||||
{ragInitProgress.indexProgress.completedChunks} / {ragInitProgress.indexProgress.totalChunks} 块
|
||||
</span>
|
||||
</div>
|
||||
<div className="obsidian-rag-progress-bar">
|
||||
<div
|
||||
className="obsidian-rag-progress-fill"
|
||||
style={{
|
||||
width: `${(ragInitProgress.indexProgress.completedChunks / Math.max(ragInitProgress.indexProgress.totalChunks, 1)) * 100}%`
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
<div className="obsidian-rag-progress-details">
|
||||
<div className="obsidian-rag-progress-files">
|
||||
共 {ragInitProgress.indexProgress.totalFiles} 个文件
|
||||
</div>
|
||||
<div className="obsidian-rag-progress-percentage">
|
||||
{Math.round((ragInitProgress.indexProgress.completedChunks / Math.max(ragInitProgress.indexProgress.totalChunks, 1)) * 100)}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* RAG 初始化成功消息 */}
|
||||
{ragInitSuccess.show && (
|
||||
<div className="obsidian-rag-success">
|
||||
<div className="obsidian-rag-success-content">
|
||||
<span className="obsidian-rag-success-icon">✅</span>
|
||||
<div className="obsidian-rag-success-text">
|
||||
<span className="obsidian-rag-success-title">
|
||||
工作区 RAG 向量索引初始化完成: {ragInitSuccess.workspaceName}
|
||||
</span>
|
||||
<span className="obsidian-rag-success-summary">
|
||||
处理了 {ragInitSuccess.totalFiles} 个文件,生成 {ragInitSuccess.totalChunks} 个向量块
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
className="obsidian-rag-success-close"
|
||||
onClick={() => setRAGInitSuccess({ show: false })}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 确认删除对话框 */}
|
||||
{showDeleteConfirm && (
|
||||
<div className="obsidian-confirm-dialog-overlay">
|
||||
<div className="obsidian-confirm-dialog">
|
||||
<div className="obsidian-confirm-dialog-header">
|
||||
<h3>清除工作区索引</h3>
|
||||
</div>
|
||||
<div className="obsidian-confirm-dialog-body">
|
||||
<p>
|
||||
将清除当前工作区的所有向量索引数据。
|
||||
</p>
|
||||
<p className="obsidian-confirm-dialog-warning">
|
||||
此操作无法撤销,清除后需要重新初始化索引才能进行语义搜索。
|
||||
</p>
|
||||
<div className="obsidian-confirm-dialog-scope">
|
||||
<strong>工作区:</strong> {settings.workspace === 'vault' ? '整个 Vault' : settings.workspace}
|
||||
</div>
|
||||
</div>
|
||||
<div className="obsidian-confirm-dialog-footer">
|
||||
<button
|
||||
onClick={cancelDeleteConfirm}
|
||||
className="obsidian-confirm-dialog-cancel-btn"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
onClick={confirmDeleteWorkspaceIndex}
|
||||
className="obsidian-confirm-dialog-confirm-btn"
|
||||
>
|
||||
确认清除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 确认初始化对话框 */}
|
||||
{showRAGInitConfirm && (
|
||||
<div className="obsidian-confirm-dialog-overlay">
|
||||
<div className="obsidian-confirm-dialog">
|
||||
<div className="obsidian-confirm-dialog-header">
|
||||
<h3>{statisticsInfo && (statisticsInfo.totalFiles > 0 || statisticsInfo.totalChunks > 0) ? '更新工作区索引' : '初始化工作区索引'}</h3>
|
||||
</div>
|
||||
<div className="obsidian-confirm-dialog-body">
|
||||
<p>
|
||||
{statisticsInfo && (statisticsInfo.totalFiles > 0 || statisticsInfo.totalChunks > 0)
|
||||
? '将更新当前工作区的向量索引,重新处理所有文件以确保索引最新。'
|
||||
: '将为当前工作区的所有文件建立向量索引,这将提高语义搜索的准确性。'
|
||||
}
|
||||
</p>
|
||||
<div className="obsidian-confirm-dialog-info">
|
||||
<div className="obsidian-confirm-dialog-info-item">
|
||||
<strong>嵌入模型:</strong>
|
||||
<span className="obsidian-confirm-dialog-model">
|
||||
{settings.embeddingModelProvider} / {settings.embeddingModelId || '默认模型'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="obsidian-confirm-dialog-info-item">
|
||||
<strong>工作区:</strong>
|
||||
<span className="obsidian-confirm-dialog-workspace">
|
||||
{settings.workspace === 'vault' ? '整个 Vault' : settings.workspace}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="obsidian-confirm-dialog-warning">
|
||||
此操作可能需要几分钟时间,具体取决于文件数量和大小。
|
||||
</p>
|
||||
</div>
|
||||
<div className="obsidian-confirm-dialog-footer">
|
||||
<button
|
||||
onClick={cancelRAGInitConfirm}
|
||||
className="obsidian-confirm-dialog-cancel-btn"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
onClick={confirmInitWorkspaceRAG}
|
||||
className="obsidian-confirm-dialog-confirm-btn"
|
||||
>
|
||||
{statisticsInfo && (statisticsInfo.totalFiles > 0 || statisticsInfo.totalChunks > 0) ? '开始更新' : '开始初始化'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 搜索结果 */}
|
||||
<div className="obsidian-search-results">
|
||||
{searchMode === 'notes' ? (
|
||||
@@ -552,8 +907,161 @@ const SearchView = () => {
|
||||
font-family: var(--font-interface);
|
||||
}
|
||||
|
||||
.obsidian-search-header {
|
||||
.obsidian-search-header-wrapper {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.obsidian-search-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.obsidian-search-title h3 {
|
||||
margin: 0;
|
||||
color: var(--text-normal);
|
||||
font-size: var(--font-ui-large);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.obsidian-search-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.obsidian-search-init-btn {
|
||||
padding: 6px 12px;
|
||||
background-color: var(--interactive-accent);
|
||||
border: none;
|
||||
border-radius: var(--radius-s);
|
||||
color: var(--text-on-accent);
|
||||
font-size: var(--font-ui-small);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.obsidian-search-init-btn:hover:not(:disabled) {
|
||||
background-color: var(--interactive-accent-hover);
|
||||
}
|
||||
|
||||
.obsidian-search-init-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.obsidian-search-delete-btn {
|
||||
padding: 6px 12px;
|
||||
background-color: #dc3545;
|
||||
border: none;
|
||||
border-radius: var(--radius-s);
|
||||
color: white;
|
||||
font-size: var(--font-ui-small);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.obsidian-search-delete-btn:hover:not(:disabled) {
|
||||
background-color: #c82333;
|
||||
}
|
||||
|
||||
.obsidian-search-delete-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.obsidian-search-stats {
|
||||
background-color: var(--background-secondary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-m);
|
||||
padding: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.obsidian-search-stats-overview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.obsidian-search-stats-main {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.obsidian-search-stats-number {
|
||||
font-size: var(--font-ui-large);
|
||||
font-weight: 700;
|
||||
color: var(--text-accent);
|
||||
font-family: var(--font-monospace);
|
||||
}
|
||||
|
||||
.obsidian-search-stats-label {
|
||||
font-size: var(--font-ui-medium);
|
||||
color: var(--text-normal);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.obsidian-search-stats-breakdown {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.obsidian-search-stats-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 8px;
|
||||
background-color: var(--background-modifier-border);
|
||||
border-radius: var(--radius-s);
|
||||
}
|
||||
|
||||
.obsidian-search-stats-item-icon {
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.obsidian-search-stats-item-value {
|
||||
font-size: var(--font-ui-small);
|
||||
font-weight: 600;
|
||||
color: var(--text-normal);
|
||||
font-family: var(--font-monospace);
|
||||
}
|
||||
|
||||
.obsidian-search-stats-item-label {
|
||||
font-size: var(--font-ui-smaller);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.obsidian-search-scope {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 8px;
|
||||
background-color: var(--background-modifier-border-hover);
|
||||
border-radius: var(--radius-s);
|
||||
}
|
||||
|
||||
.obsidian-search-scope-label {
|
||||
font-size: var(--font-ui-smaller);
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.obsidian-search-scope-value {
|
||||
font-size: var(--font-ui-smaller);
|
||||
color: var(--text-accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.obsidian-search-input-section {
|
||||
/* padding 由父元素控制 */
|
||||
}
|
||||
|
||||
.obsidian-search-mode-toggle {
|
||||
@@ -588,6 +1096,8 @@ const SearchView = () => {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.obsidian-search-stats {
|
||||
padding: 8px 12px;
|
||||
font-size: var(--font-ui-small);
|
||||
@@ -884,6 +1394,308 @@ const SearchView = () => {
|
||||
user-select: text;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
/* RAG 初始化进度样式 */
|
||||
.obsidian-rag-initializing {
|
||||
padding: 20px;
|
||||
background-color: var(--background-secondary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-m);
|
||||
margin: 12px;
|
||||
}
|
||||
|
||||
.obsidian-rag-init-header {
|
||||
text-align: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.obsidian-rag-init-header h4 {
|
||||
margin: 0 0 8px 0;
|
||||
color: var(--text-normal);
|
||||
font-size: var(--font-ui-medium);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.obsidian-rag-init-header p {
|
||||
margin: 0;
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-ui-small);
|
||||
}
|
||||
|
||||
.obsidian-rag-progress {
|
||||
background-color: var(--background-primary);
|
||||
padding: 12px;
|
||||
border-radius: var(--radius-s);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.obsidian-rag-progress-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.obsidian-rag-progress-stage {
|
||||
color: var(--text-normal);
|
||||
font-size: var(--font-ui-small);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.obsidian-rag-progress-counter {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-ui-small);
|
||||
font-family: var(--font-monospace);
|
||||
}
|
||||
|
||||
.obsidian-rag-progress-bar {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
background-color: var(--background-modifier-border);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.obsidian-rag-progress-fill {
|
||||
height: 100%;
|
||||
background-color: var(--interactive-accent);
|
||||
border-radius: 3px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.obsidian-rag-progress-details {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.obsidian-rag-progress-files {
|
||||
color: var(--text-normal);
|
||||
font-size: var(--font-ui-small);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.obsidian-rag-progress-percentage {
|
||||
color: var(--text-accent);
|
||||
font-size: var(--font-ui-small);
|
||||
font-weight: 600;
|
||||
font-family: var(--font-monospace);
|
||||
}
|
||||
|
||||
/* RAG 初始化成功样式 */
|
||||
.obsidian-rag-success {
|
||||
background-color: var(--background-secondary);
|
||||
border: 1px solid var(--color-green, #28a745);
|
||||
border-radius: var(--radius-m);
|
||||
margin: 12px;
|
||||
animation: slideInFromTop 0.3s ease-out;
|
||||
}
|
||||
|
||||
.obsidian-rag-success-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.obsidian-rag-success-icon {
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
color: var(--color-green, #28a745);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.obsidian-rag-success-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.obsidian-rag-success-title {
|
||||
font-size: var(--font-ui-medium);
|
||||
font-weight: 600;
|
||||
color: var(--text-normal);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.obsidian-rag-success-summary {
|
||||
font-size: var(--font-ui-small);
|
||||
color: var(--text-muted);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.obsidian-rag-success-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
border-radius: var(--radius-s);
|
||||
transition: all 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.obsidian-rag-success-close:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
/* 确认对话框样式 */
|
||||
.obsidian-confirm-dialog-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.obsidian-confirm-dialog {
|
||||
background-color: var(--background-primary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-l);
|
||||
box-shadow: var(--shadow-l);
|
||||
max-width: 400px;
|
||||
width: 90%;
|
||||
max-height: 80vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.obsidian-confirm-dialog-header {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
background-color: var(--background-secondary);
|
||||
}
|
||||
|
||||
.obsidian-confirm-dialog-header h3 {
|
||||
margin: 0;
|
||||
color: var(--text-normal);
|
||||
font-size: var(--font-ui-large);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.obsidian-confirm-dialog-body {
|
||||
padding: 20px;
|
||||
color: var(--text-normal);
|
||||
font-size: var(--font-ui-medium);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.obsidian-confirm-dialog-body p {
|
||||
margin: 0 0 12px 0;
|
||||
}
|
||||
|
||||
.obsidian-confirm-dialog-warning {
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-s);
|
||||
padding: 12px;
|
||||
margin: 12px 0;
|
||||
color: var(--text-error);
|
||||
font-size: var(--font-ui-small);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.obsidian-confirm-dialog-scope {
|
||||
background-color: var(--background-secondary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-s);
|
||||
padding: 8px 12px;
|
||||
margin: 12px 0 0 0;
|
||||
font-size: var(--font-ui-small);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.obsidian-confirm-dialog-info {
|
||||
background-color: var(--background-secondary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-s);
|
||||
padding: 12px;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.obsidian-confirm-dialog-info-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
font-size: var(--font-ui-small);
|
||||
}
|
||||
|
||||
.obsidian-confirm-dialog-info-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.obsidian-confirm-dialog-info-item strong {
|
||||
color: var(--text-normal);
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.obsidian-confirm-dialog-model,
|
||||
.obsidian-confirm-dialog-workspace {
|
||||
color: var(--text-accent);
|
||||
font-weight: 600;
|
||||
font-family: var(--font-monospace);
|
||||
text-align: right;
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.obsidian-confirm-dialog-footer {
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid var(--background-modifier-border);
|
||||
background-color: var(--background-secondary);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.obsidian-confirm-dialog-cancel-btn {
|
||||
padding: 8px 16px;
|
||||
background-color: var(--interactive-normal);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-s);
|
||||
color: var(--text-normal);
|
||||
font-size: var(--font-ui-small);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.obsidian-confirm-dialog-cancel-btn:hover {
|
||||
background-color: var(--interactive-hover);
|
||||
}
|
||||
|
||||
.obsidian-confirm-dialog-confirm-btn {
|
||||
padding: 8px 16px;
|
||||
background-color: #dc3545;
|
||||
border: 1px solid #dc3545;
|
||||
border-radius: var(--radius-s);
|
||||
color: white;
|
||||
font-size: var(--font-ui-small);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.obsidian-confirm-dialog-confirm-btn:hover {
|
||||
background-color: #c82333;
|
||||
border-color: #c82333;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</div>
|
||||
|
||||
@@ -2,11 +2,13 @@ import { App, TFile } from 'obsidian'
|
||||
|
||||
import { QueryProgressState } from '../../components/chat-view/QueryProgress'
|
||||
import { DBManager } from '../../database/database-manager'
|
||||
import { Workspace } from '../../database/json/workspace/types'
|
||||
import { VectorManager } from '../../database/modules/vector/vector-manager'
|
||||
import { SelectVector } from '../../database/schema'
|
||||
import { EmbeddingModel } from '../../types/embedding'
|
||||
import { ApiProvider } from '../../types/llm/model'
|
||||
import { InfioSettings } from '../../types/settings'
|
||||
import { getFilesWithTag } from '../../utils/glob-utils'
|
||||
|
||||
import { getEmbeddingModel } from './embedding'
|
||||
|
||||
@@ -103,6 +105,36 @@ export class RAGEngine {
|
||||
this.initialized = true
|
||||
}
|
||||
|
||||
async updateWorkspaceIndex(
|
||||
workspace: Workspace,
|
||||
options: { reindexAll: boolean },
|
||||
onQueryProgressChange?: (queryProgress: QueryProgressState) => void,
|
||||
): Promise<void> {
|
||||
if (!this.embeddingModel) {
|
||||
throw new Error('Embedding model is not set')
|
||||
}
|
||||
await this.initializeDimension()
|
||||
|
||||
await this.vectorManager.updateWorkspaceIndex(
|
||||
this.embeddingModel,
|
||||
workspace,
|
||||
{
|
||||
chunkSize: this.settings.ragOptions.chunkSize,
|
||||
batchSize: this.settings.ragOptions.batchSize,
|
||||
excludePatterns: this.settings.ragOptions.excludePatterns,
|
||||
includePatterns: this.settings.ragOptions.includePatterns,
|
||||
reindexAll: options.reindexAll,
|
||||
},
|
||||
(indexProgress) => {
|
||||
onQueryProgressChange?.({
|
||||
type: 'indexing',
|
||||
indexProgress,
|
||||
})
|
||||
},
|
||||
)
|
||||
this.initialized = true
|
||||
}
|
||||
|
||||
async updateFileIndex(file: TFile) {
|
||||
if (!this.embeddingModel) {
|
||||
throw new Error('Embedding model is not set')
|
||||
@@ -185,4 +217,65 @@ export class RAGEngine {
|
||||
}
|
||||
return this.embeddingModel.getEmbedding(query)
|
||||
}
|
||||
|
||||
async getWorkspaceStatistics(workspace?: Workspace): Promise<{
|
||||
totalFiles: number
|
||||
totalChunks: number
|
||||
}> {
|
||||
if (!this.embeddingModel) {
|
||||
throw new Error('Embedding model is not set')
|
||||
}
|
||||
await this.initializeDimension()
|
||||
return await this.vectorManager.getWorkspaceStatistics(this.embeddingModel, workspace)
|
||||
}
|
||||
|
||||
async getVaultStatistics(): Promise<{
|
||||
totalFiles: number
|
||||
totalChunks: number
|
||||
}> {
|
||||
if (!this.embeddingModel) {
|
||||
throw new Error('Embedding model is not set')
|
||||
}
|
||||
await this.initializeDimension()
|
||||
return await this.vectorManager.getVaultStatistics(this.embeddingModel)
|
||||
}
|
||||
|
||||
async clearWorkspaceIndex(workspace?: Workspace): Promise<void> {
|
||||
if (!this.embeddingModel) {
|
||||
throw new Error('Embedding model is not set')
|
||||
}
|
||||
await this.initializeDimension()
|
||||
|
||||
if (workspace) {
|
||||
// 获取工作区中的所有文件路径
|
||||
const folders: string[] = []
|
||||
const files: string[] = []
|
||||
|
||||
for (const item of workspace.content) {
|
||||
if (item.type === 'folder') {
|
||||
const folderPath = item.content
|
||||
|
||||
// 获取文件夹下的所有文件
|
||||
const folderFiles = this.app.vault.getMarkdownFiles().filter(file =>
|
||||
file.path.startsWith(folderPath === '/' ? '' : folderPath + '/')
|
||||
)
|
||||
|
||||
files.push(...folderFiles.map(file => file.path))
|
||||
} else if (item.type === 'tag') {
|
||||
// 获取标签对应的所有文件
|
||||
const tagFiles = getFilesWithTag(item.content, this.app)
|
||||
files.push(...tagFiles)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除工作区相关的向量
|
||||
if (files.length > 0) {
|
||||
// 通过 VectorManager 的私有 repository 访问
|
||||
await this.vectorManager['repository'].deleteVectorsForMultipleFiles(files, this.embeddingModel)
|
||||
}
|
||||
} else {
|
||||
// 清除所有向量
|
||||
await this.vectorManager['repository'].clearAllVectors(this.embeddingModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
import { InsertVector, SelectVector } from '../../../database/schema';
|
||||
import { EmbeddingModel } from '../../../types/embedding';
|
||||
import { openSettingsModalWithError } from '../../../utils/open-settings-modal';
|
||||
import { getFilesWithTag } from '../../../utils/glob-utils';
|
||||
import { Workspace } from '../../json/workspace/types';
|
||||
import { DBManager } from '../../database-manager';
|
||||
|
||||
import { VectorRepository } from './vector-repository';
|
||||
@@ -53,6 +55,50 @@ export class VectorManager {
|
||||
)
|
||||
}
|
||||
|
||||
async getWorkspaceStatistics(
|
||||
embeddingModel: EmbeddingModel,
|
||||
workspace?: Workspace
|
||||
): Promise<{
|
||||
totalFiles: number
|
||||
totalChunks: number
|
||||
}> {
|
||||
// 构建工作区范围
|
||||
let scope: { files: string[], folders: string[] } | undefined
|
||||
if (workspace) {
|
||||
const folders: string[] = []
|
||||
const files: string[] = []
|
||||
|
||||
// 处理工作区中的文件夹和标签
|
||||
for (const item of workspace.content) {
|
||||
if (item.type === 'folder') {
|
||||
folders.push(item.content)
|
||||
} else if (item.type === 'tag') {
|
||||
// 获取标签对应的所有文件
|
||||
const tagFiles = getFilesWithTag(item.content, this.app)
|
||||
files.push(...tagFiles)
|
||||
}
|
||||
}
|
||||
|
||||
// 只有当有文件夹或文件时才设置 scope
|
||||
if (folders.length > 0 || files.length > 0) {
|
||||
scope = { files, folders }
|
||||
}
|
||||
}
|
||||
|
||||
if (scope) {
|
||||
return await this.repository.getWorkspaceStatistics(embeddingModel, scope)
|
||||
} else {
|
||||
return await this.repository.getVaultStatistics(embeddingModel)
|
||||
}
|
||||
}
|
||||
|
||||
async getVaultStatistics(embeddingModel: EmbeddingModel): Promise<{
|
||||
totalFiles: number
|
||||
totalChunks: number
|
||||
}> {
|
||||
return await this.repository.getVaultStatistics(embeddingModel)
|
||||
}
|
||||
|
||||
// 强制垃圾回收的辅助方法
|
||||
private forceGarbageCollection() {
|
||||
try {
|
||||
@@ -352,6 +398,289 @@ export class VectorManager {
|
||||
}
|
||||
}
|
||||
|
||||
async updateWorkspaceIndex(
|
||||
embeddingModel: EmbeddingModel,
|
||||
workspace: Workspace,
|
||||
options: {
|
||||
chunkSize: number
|
||||
batchSize: number
|
||||
excludePatterns: string[]
|
||||
includePatterns: string[]
|
||||
reindexAll?: boolean
|
||||
},
|
||||
updateProgress?: (indexProgress: IndexProgress) => void,
|
||||
): Promise<void> {
|
||||
let filesToIndex: TFile[]
|
||||
if (options.reindexAll) {
|
||||
console.log("updateWorkspaceIndex reindexAll")
|
||||
filesToIndex = await this.getFilesToIndexInWorkspace({
|
||||
embeddingModel: embeddingModel,
|
||||
workspace: workspace,
|
||||
excludePatterns: options.excludePatterns,
|
||||
includePatterns: options.includePatterns,
|
||||
reindexAll: true,
|
||||
})
|
||||
// 只清理工作区相关的向量,而不是全部
|
||||
const workspaceFilePaths = filesToIndex.map((file) => file.path)
|
||||
if (workspaceFilePaths.length > 0) {
|
||||
await this.repository.deleteVectorsForMultipleFiles(workspaceFilePaths, embeddingModel)
|
||||
}
|
||||
} else {
|
||||
console.log("updateWorkspaceIndex for update files")
|
||||
await this.cleanVectorsForDeletedFiles(embeddingModel)
|
||||
console.log("updateWorkspaceIndex cleanVectorsForDeletedFiles")
|
||||
filesToIndex = await this.getFilesToIndexInWorkspace({
|
||||
embeddingModel: embeddingModel,
|
||||
workspace: workspace,
|
||||
excludePatterns: options.excludePatterns,
|
||||
includePatterns: options.includePatterns,
|
||||
})
|
||||
console.log("get workspace files to index: ", filesToIndex.length)
|
||||
await this.repository.deleteVectorsForMultipleFiles(
|
||||
filesToIndex.map((file) => file.path),
|
||||
embeddingModel,
|
||||
)
|
||||
console.log("delete vectors for workspace files: ", filesToIndex.length)
|
||||
}
|
||||
console.log("get workspace files to index: ", filesToIndex.length)
|
||||
|
||||
if (filesToIndex.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// Embed the files (使用与 updateVaultIndex 相同的逻辑)
|
||||
const overlap = Math.floor(options.chunkSize * 0.15)
|
||||
const textSplitter = new RecursiveCharacterTextSplitter({
|
||||
chunkSize: options.chunkSize,
|
||||
chunkOverlap: overlap,
|
||||
separators: [
|
||||
"\n\n",
|
||||
"\n",
|
||||
".",
|
||||
",",
|
||||
" ",
|
||||
"\u200b", // Zero-width space
|
||||
"\uff0c", // Fullwidth comma
|
||||
"\u3001", // Ideographic comma
|
||||
"\uff0e", // Fullwidth full stop
|
||||
"\u3002", // Ideographic full stop
|
||||
"",
|
||||
],
|
||||
});
|
||||
console.log("textSplitter chunkSize: ", options.chunkSize, "overlap: ", overlap)
|
||||
|
||||
const skippedFiles: string[] = []
|
||||
const contentChunks: InsertVector[] = (
|
||||
await Promise.all(
|
||||
filesToIndex.map(async (file) => {
|
||||
try {
|
||||
let fileContent = await this.app.vault.cachedRead(file)
|
||||
// 清理null字节,防止PostgreSQL UTF8编码错误
|
||||
fileContent = fileContent.replace(/\0/g, '')
|
||||
const fileDocuments = await textSplitter.createDocuments([
|
||||
fileContent,
|
||||
])
|
||||
return fileDocuments
|
||||
.map((chunk): InsertVector | null => {
|
||||
// 保存原始内容,不在此处调用 removeMarkdown
|
||||
const rawContent = chunk.pageContent.replace(/\0/g, '')
|
||||
if (!rawContent || rawContent.trim().length === 0) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
path: file.path,
|
||||
mtime: file.stat.mtime,
|
||||
content: rawContent, // 保存原始内容
|
||||
embedding: [],
|
||||
metadata: {
|
||||
startLine: Number(chunk.metadata.loc.lines.from),
|
||||
endLine: Number(chunk.metadata.loc.lines.to),
|
||||
},
|
||||
}
|
||||
})
|
||||
.filter((chunk): chunk is InsertVector => chunk !== null)
|
||||
} catch (error) {
|
||||
console.warn(`跳过文件 ${file.path}:`, error.message)
|
||||
skippedFiles.push(file.path)
|
||||
return []
|
||||
}
|
||||
}),
|
||||
)
|
||||
).flat()
|
||||
|
||||
console.log("contentChunks: ", contentChunks.length)
|
||||
|
||||
if (skippedFiles.length > 0) {
|
||||
console.warn(`跳过了 ${skippedFiles.length} 个有问题的文件:`, skippedFiles)
|
||||
new Notice(`跳过了 ${skippedFiles.length} 个有问题的文件`)
|
||||
}
|
||||
|
||||
updateProgress?.({
|
||||
completedChunks: 0,
|
||||
totalChunks: contentChunks.length,
|
||||
totalFiles: filesToIndex.length,
|
||||
})
|
||||
|
||||
const embeddingProgress = { completed: 0 }
|
||||
// 减少批量大小以降低内存压力
|
||||
const batchSize = options.batchSize
|
||||
let batchCount = 0
|
||||
|
||||
try {
|
||||
if (embeddingModel.supportsBatch) {
|
||||
// 支持批量处理的提供商:使用流式处理逻辑
|
||||
for (let i = 0; i < contentChunks.length; i += batchSize) {
|
||||
batchCount++
|
||||
const batchChunks = contentChunks.slice(i, Math.min(i + batchSize, contentChunks.length))
|
||||
|
||||
const embeddedBatch: InsertVector[] = []
|
||||
|
||||
await backOff(
|
||||
async () => {
|
||||
// 在嵌入之前处理 markdown,只处理一次
|
||||
const cleanedBatchData = batchChunks.map(chunk => {
|
||||
const cleanContent = removeMarkdown(chunk.content).replace(/\0/g, '')
|
||||
return { chunk, cleanContent }
|
||||
}).filter(({ cleanContent }) => cleanContent && cleanContent.trim().length > 0)
|
||||
|
||||
if (cleanedBatchData.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const batchTexts = cleanedBatchData.map(({ cleanContent }) => cleanContent)
|
||||
const batchEmbeddings = await embeddingModel.getBatchEmbeddings(batchTexts)
|
||||
|
||||
// 合并embedding结果到chunk数据
|
||||
for (let j = 0; j < cleanedBatchData.length; j++) {
|
||||
const { chunk, cleanContent } = cleanedBatchData[j]
|
||||
const embeddedChunk: InsertVector = {
|
||||
path: chunk.path,
|
||||
mtime: chunk.mtime,
|
||||
content: cleanContent, // 使用已经清理过的内容
|
||||
embedding: batchEmbeddings[j],
|
||||
metadata: chunk.metadata,
|
||||
}
|
||||
embeddedBatch.push(embeddedChunk)
|
||||
}
|
||||
},
|
||||
{
|
||||
numOfAttempts: 3, // 减少重试次数
|
||||
startingDelay: 500, // 减少延迟
|
||||
timeMultiple: 1.5,
|
||||
jitter: 'full',
|
||||
},
|
||||
)
|
||||
|
||||
// 立即插入当前批次,避免内存累积
|
||||
if (embeddedBatch.length > 0) {
|
||||
await this.repository.insertVectors(embeddedBatch, embeddingModel)
|
||||
// 清理批次数据
|
||||
embeddedBatch.length = 0
|
||||
}
|
||||
|
||||
embeddingProgress.completed += batchChunks.length
|
||||
updateProgress?.({
|
||||
completedChunks: embeddingProgress.completed,
|
||||
totalChunks: contentChunks.length,
|
||||
totalFiles: filesToIndex.length,
|
||||
})
|
||||
|
||||
// 定期内存清理
|
||||
await this.memoryCleanup(batchCount)
|
||||
}
|
||||
} else {
|
||||
// 不支持批量处理的提供商:使用流式处理逻辑
|
||||
const limit = pLimit(32) // 从50降低到10,减少并发压力
|
||||
const abortController = new AbortController()
|
||||
|
||||
// 流式处理:分批处理并立即插入
|
||||
for (let i = 0; i < contentChunks.length; i += batchSize) {
|
||||
if (abortController.signal.aborted) {
|
||||
throw new Error('Operation was aborted')
|
||||
}
|
||||
|
||||
batchCount++
|
||||
const batchChunks = contentChunks.slice(i, Math.min(i + batchSize, contentChunks.length))
|
||||
const embeddedBatch: InsertVector[] = []
|
||||
|
||||
const tasks = batchChunks.map((chunk) =>
|
||||
limit(async () => {
|
||||
if (abortController.signal.aborted) {
|
||||
throw new Error('Operation was aborted')
|
||||
}
|
||||
try {
|
||||
await backOff(
|
||||
async () => {
|
||||
// 在嵌入之前处理 markdown
|
||||
const cleanContent = removeMarkdown(chunk.content).replace(/\0/g, '')
|
||||
// 跳过清理后为空的内容
|
||||
if (!cleanContent || cleanContent.trim().length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const embedding = await embeddingModel.getEmbedding(cleanContent)
|
||||
const embeddedChunk = {
|
||||
path: chunk.path,
|
||||
mtime: chunk.mtime,
|
||||
content: cleanContent, // 使用清理后的内容
|
||||
embedding,
|
||||
metadata: chunk.metadata,
|
||||
}
|
||||
embeddedBatch.push(embeddedChunk)
|
||||
},
|
||||
{
|
||||
numOfAttempts: 3, // 减少重试次数
|
||||
startingDelay: 500, // 减少延迟
|
||||
timeMultiple: 1.5,
|
||||
jitter: 'full',
|
||||
},
|
||||
)
|
||||
} catch (error) {
|
||||
abortController.abort()
|
||||
throw error
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
await Promise.all(tasks)
|
||||
|
||||
// 立即插入当前批次
|
||||
if (embeddedBatch.length > 0) {
|
||||
await this.repository.insertVectors(embeddedBatch, embeddingModel)
|
||||
// 清理批次数据
|
||||
embeddedBatch.length = 0
|
||||
}
|
||||
|
||||
embeddingProgress.completed += batchChunks.length
|
||||
updateProgress?.({
|
||||
completedChunks: embeddingProgress.completed,
|
||||
totalChunks: contentChunks.length,
|
||||
totalFiles: filesToIndex.length,
|
||||
})
|
||||
|
||||
// 定期内存清理
|
||||
await this.memoryCleanup(batchCount)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof LLMAPIKeyNotSetException ||
|
||||
error instanceof LLMAPIKeyInvalidException ||
|
||||
error instanceof LLMBaseUrlNotSetException
|
||||
) {
|
||||
openSettingsModalWithError(this.app, error.message)
|
||||
} else if (error instanceof LLMRateLimitExceededException) {
|
||||
new Notice(error.message)
|
||||
} else {
|
||||
console.error('Error embedding chunks:', error)
|
||||
throw error
|
||||
}
|
||||
} finally {
|
||||
// 最终清理
|
||||
this.forceGarbageCollection()
|
||||
}
|
||||
}
|
||||
|
||||
async UpdateFileVectorIndex(
|
||||
embeddingModel: EmbeddingModel,
|
||||
chunkSize: number,
|
||||
@@ -615,4 +944,89 @@ export class VectorManager {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
private async getFilesToIndexInWorkspace({
|
||||
embeddingModel,
|
||||
workspace,
|
||||
excludePatterns,
|
||||
includePatterns,
|
||||
reindexAll,
|
||||
}: {
|
||||
embeddingModel: EmbeddingModel
|
||||
workspace: Workspace
|
||||
excludePatterns: string[]
|
||||
includePatterns: string[]
|
||||
reindexAll?: boolean
|
||||
}): Promise<TFile[]> {
|
||||
// 获取工作区中的所有文件
|
||||
const workspaceFiles = new Set<string>()
|
||||
|
||||
if (workspace) {
|
||||
// 处理工作区中的文件夹和标签
|
||||
for (const item of workspace.content) {
|
||||
if (item.type === 'folder') {
|
||||
const folderPath = item.content
|
||||
|
||||
// 获取文件夹下的所有文件
|
||||
const files = this.app.vault.getMarkdownFiles().filter(file =>
|
||||
file.path.startsWith(folderPath === '/' ? '' : folderPath + '/')
|
||||
)
|
||||
|
||||
// 添加所有文件路径
|
||||
files.forEach(file => {
|
||||
workspaceFiles.add(file.path)
|
||||
})
|
||||
|
||||
} else if (item.type === 'tag') {
|
||||
// 获取标签对应的所有文件
|
||||
const tagFiles = getFilesWithTag(item.content, this.app)
|
||||
|
||||
tagFiles.forEach(filePath => {
|
||||
workspaceFiles.add(filePath)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 将路径转换为 TFile 对象
|
||||
let filesToIndex = Array.from(workspaceFiles)
|
||||
.map(path => this.app.vault.getFileByPath(path))
|
||||
.filter((file): file is TFile => file !== null && file instanceof TFile)
|
||||
|
||||
console.log("get workspace files: ", filesToIndex.length)
|
||||
|
||||
// 应用排除和包含模式
|
||||
filesToIndex = filesToIndex.filter((file) => {
|
||||
return !excludePatterns.some((pattern) => minimatch(file.path, pattern))
|
||||
})
|
||||
|
||||
if (includePatterns.length > 0) {
|
||||
filesToIndex = filesToIndex.filter((file) => {
|
||||
return includePatterns.some((pattern) => minimatch(file.path, pattern))
|
||||
})
|
||||
}
|
||||
|
||||
if (reindexAll) {
|
||||
return filesToIndex
|
||||
}
|
||||
|
||||
// 优化流程:使用数据库最大mtime来过滤需要更新的文件
|
||||
try {
|
||||
const maxMtime = await this.repository.getMaxMtime(embeddingModel)
|
||||
console.log("Database max mtime:", maxMtime)
|
||||
|
||||
if (maxMtime === null) {
|
||||
// 数据库中没有任何向量,需要索引所有文件
|
||||
return filesToIndex
|
||||
}
|
||||
|
||||
// 筛选出在数据库最后更新时间之后修改的文件
|
||||
return filesToIndex.filter((file) => {
|
||||
return file.stat.mtime > maxMtime
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Error getting max mtime from database:", error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,4 +188,94 @@ export class VectorRepository {
|
||||
const result = await this.db.query<SearchResult>(query, params)
|
||||
return result.rows
|
||||
}
|
||||
|
||||
async getWorkspaceStatistics(
|
||||
embeddingModel: EmbeddingModel,
|
||||
scope?: {
|
||||
files: string[]
|
||||
folders: string[]
|
||||
}
|
||||
): Promise<{
|
||||
totalFiles: number
|
||||
totalChunks: number
|
||||
}> {
|
||||
if (!this.db) {
|
||||
throw new DatabaseNotInitializedException()
|
||||
}
|
||||
const tableName = this.getTableName(embeddingModel)
|
||||
|
||||
let scopeCondition = ''
|
||||
const params: unknown[] = []
|
||||
let paramIndex = 1
|
||||
|
||||
if (scope) {
|
||||
const conditions: string[] = []
|
||||
|
||||
if (scope.files.length > 0) {
|
||||
conditions.push(`path = ANY($${paramIndex})`)
|
||||
params.push(scope.files)
|
||||
paramIndex++
|
||||
}
|
||||
|
||||
if (scope.folders.length > 0) {
|
||||
const folderConditions = scope.folders.map((folder, idx) => {
|
||||
params.push(`${folder}/%`)
|
||||
return `path LIKE $${paramIndex + idx}`
|
||||
})
|
||||
conditions.push(`(${folderConditions.join(' OR ')})`)
|
||||
paramIndex += scope.folders.length
|
||||
}
|
||||
|
||||
if (conditions.length > 0) {
|
||||
scopeCondition = `WHERE (${conditions.join(' OR ')})`
|
||||
}
|
||||
}
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
COUNT(DISTINCT path) as total_files,
|
||||
COUNT(*) as total_chunks
|
||||
FROM "${tableName}"
|
||||
${scopeCondition}
|
||||
`
|
||||
|
||||
const result = await this.db.query<{
|
||||
total_files: number
|
||||
total_chunks: number
|
||||
}>(query, params)
|
||||
|
||||
const row = result.rows[0]
|
||||
return {
|
||||
totalFiles: Number(row?.total_files || 0),
|
||||
totalChunks: Number(row?.total_chunks || 0)
|
||||
}
|
||||
}
|
||||
|
||||
async getVaultStatistics(embeddingModel: EmbeddingModel): Promise<{
|
||||
totalFiles: number
|
||||
totalChunks: number
|
||||
}> {
|
||||
if (!this.db) {
|
||||
throw new DatabaseNotInitializedException()
|
||||
}
|
||||
const tableName = this.getTableName(embeddingModel)
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
COUNT(DISTINCT path) as total_files,
|
||||
COUNT(*) as total_chunks
|
||||
FROM "${tableName}"
|
||||
`
|
||||
|
||||
const result = await this.db.query<{
|
||||
total_files: number
|
||||
total_chunks: number
|
||||
}>(query)
|
||||
|
||||
const row = result.rows[0]
|
||||
return {
|
||||
totalFiles: Number(row?.total_files || 0),
|
||||
totalChunks: Number(row?.total_chunks || 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,6 +239,8 @@ export default {
|
||||
autocompleteModelDescription: 'Model used for code and text autocompletion, providing intelligent writing suggestions',
|
||||
embeddingModel: 'Embedding model:',
|
||||
embeddingModelDescription: 'Model used for document vectorization and semantic search, supporting RAG functionality',
|
||||
insightModel: 'Insight model:',
|
||||
insightModelDescription: 'Model used for generating intelligent insights and analysis, providing deep content understanding',
|
||||
},
|
||||
|
||||
// Model Provider Settings
|
||||
@@ -249,6 +251,7 @@ export default {
|
||||
oneClickConfig: 'One-Click Config',
|
||||
oneClickConfigTooltip: 'Automatically configure models to recommended models from providers with API keys set',
|
||||
chatModelConfigured: 'Chat model configured automatically: {provider}/{model}',
|
||||
insightModelConfigured: 'Insight model configured automatically: {provider}/{model}',
|
||||
autocompleteModelConfigured: 'Autocomplete model configured automatically: {provider}/{model}',
|
||||
embeddingModelConfigured: 'Embedding model configured automatically: {provider}/{model}',
|
||||
provider: 'Provider',
|
||||
|
||||
@@ -241,6 +241,8 @@ export default {
|
||||
autocompleteModelDescription: '用于代码和文本自动补全的模型,提供智能写作建议',
|
||||
embeddingModel: '嵌入模型:',
|
||||
embeddingModelDescription: '用于文档向量化和语义搜索的模型,支持 RAG 功能',
|
||||
insightModel: '洞察模型:',
|
||||
insightModelDescription: '用于生成智能洞察和分析的模型,提供深度内容理解',
|
||||
},
|
||||
|
||||
// 模型提供商设置
|
||||
@@ -251,6 +253,7 @@ export default {
|
||||
oneClickConfig: '一键配置',
|
||||
oneClickConfigTooltip: '自动配置模型为已设置 API Key 的提供商的推荐模型',
|
||||
chatModelConfigured: '已自动配置聊天模型:{provider}/{model}',
|
||||
insightModelConfigured: '已自动配置洞察模型:{provider}/{model}',
|
||||
autocompleteModelConfigured: '已自动配置自动补全模型:{provider}/{model}',
|
||||
embeddingModelConfigured: '已自动配置嵌入模型:{provider}/{model}',
|
||||
provider: '提供商',
|
||||
|
||||
@@ -105,13 +105,19 @@ const CustomProviderSettings: React.FC<CustomProviderSettingsProps> = ({ plugin,
|
||||
if (selectedProvider) {
|
||||
const defaultModels = GetDefaultModelId(selectedProvider);
|
||||
|
||||
// 设置chat和autocomplete模型
|
||||
// 设置chat、insight和autocomplete模型
|
||||
if (defaultModels.chat) {
|
||||
newSettings.chatModelProvider = selectedProvider;
|
||||
newSettings.chatModelId = defaultModels.chat;
|
||||
hasUpdates = true;
|
||||
console.debug(t("settings.ModelProvider.chatModelConfigured", { provider: selectedProvider, model: defaultModels.chat }));
|
||||
}
|
||||
if (defaultModels.insight) {
|
||||
newSettings.insightModelProvider = selectedProvider;
|
||||
newSettings.insightModelId = defaultModels.insight;
|
||||
hasUpdates = true;
|
||||
console.debug(t("settings.ModelProvider.insightModelConfigured", { provider: selectedProvider, model: defaultModels.insight }));
|
||||
}
|
||||
if (defaultModels.autoComplete) {
|
||||
newSettings.applyModelProvider = selectedProvider;
|
||||
newSettings.applyModelId = defaultModels.autoComplete;
|
||||
@@ -367,6 +373,28 @@ const CustomProviderSettings: React.FC<CustomProviderSettingsProps> = ({ plugin,
|
||||
});
|
||||
};
|
||||
|
||||
const updateInsightModelId = (provider: ApiProvider, modelId: string, isCustom: boolean = false) => {
|
||||
console.debug(`updateInsightModelId: ${provider} -> ${modelId}, isCustom: ${isCustom}`)
|
||||
const providerSettingKey = getProviderSettingKey(provider);
|
||||
const providerSettings = settings[providerSettingKey] || {};
|
||||
const currentModels = providerSettings.models || [];
|
||||
|
||||
// 如果是自定义模型且不在列表中,则添加
|
||||
const updatedModels = isCustom && !currentModels.includes(modelId)
|
||||
? [...currentModels, modelId]
|
||||
: currentModels;
|
||||
|
||||
handleSettingsUpdate({
|
||||
...settings,
|
||||
insightModelProvider: provider,
|
||||
insightModelId: modelId,
|
||||
[providerSettingKey]: {
|
||||
...providerSettings,
|
||||
models: updatedModels
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 生成包含链接的API Key描述
|
||||
const generateApiKeyDescription = (provider: ApiProvider): React.ReactNode => {
|
||||
const apiUrl = getProviderApiUrl(provider);
|
||||
@@ -493,6 +521,15 @@ const CustomProviderSettings: React.FC<CustomProviderSettingsProps> = ({ plugin,
|
||||
updateModel={updateChatModelId}
|
||||
/>
|
||||
|
||||
<ComboBoxComponent
|
||||
name={t("settings.Models.insightModel")}
|
||||
description={t("settings.Models.insightModelDescription")}
|
||||
settings={settings}
|
||||
provider={settings.insightModelProvider || ApiProvider.Infio}
|
||||
modelId={settings.insightModelId}
|
||||
updateModel={updateInsightModelId}
|
||||
/>
|
||||
|
||||
<ComboBoxComponent
|
||||
name={t("settings.Models.autocompleteModel")}
|
||||
description={t("settings.Models.autocompleteModelDescription")}
|
||||
|
||||
@@ -289,6 +289,10 @@ export const InfioSettingsSchema = z.object({
|
||||
chatModelProvider: z.nativeEnum(ApiProvider).catch(ApiProvider.Infio),
|
||||
chatModelId: z.string().catch(''),
|
||||
|
||||
// Insight Model
|
||||
insightModelProvider: z.nativeEnum(ApiProvider).catch(ApiProvider.Infio),
|
||||
insightModelId: z.string().catch(''),
|
||||
|
||||
// Apply Model
|
||||
applyModelProvider: z.nativeEnum(ApiProvider).catch(ApiProvider.Infio),
|
||||
applyModelId: z.string().catch(''),
|
||||
|
||||
@@ -35,6 +35,7 @@ export interface EmbeddingModelInfo {
|
||||
// https://docs.anthropic.com/en/docs/about-claude/models
|
||||
export type AnthropicModelId = keyof typeof anthropicModels
|
||||
export const anthropicDefaultModelId: AnthropicModelId = "claude-sonnet-4-20250514"
|
||||
export const anthropicDefaultInsightModelId: AnthropicModelId = "claude-sonnet-4-20250514"
|
||||
export const anthropicDefaultAutoCompleteModelId: AnthropicModelId = "claude-3-5-haiku-20241022"
|
||||
export const anthropicDefaultEmbeddingModelId: AnthropicModelId = null // this is not supported embedding model
|
||||
export const anthropicModels = {
|
||||
@@ -131,6 +132,7 @@ export const anthropicModels = {
|
||||
|
||||
// Infio
|
||||
export const infioDefaultModelId = "gemini/gemini-2.5-pro-preview-06-05" // for chat
|
||||
export const infioDefaultInsightModelId = "deepseek/deepseek-v3" // for insight
|
||||
export const infioDefaultAutoCompleteModelId = "groq/llama-3.3-70b-versatile" // for auto complete
|
||||
export const infioDefaultEmbeddingModelId = "openai/text-embedding-3-small" // for embedding
|
||||
export const infioDefaultModelInfo: ModelInfo = {
|
||||
@@ -214,6 +216,7 @@ export const infioEmbeddingModels = {
|
||||
// OpenRouter
|
||||
// https://openrouter.ai/models?order=newest&supported_parameters=tools
|
||||
export const openRouterDefaultModelId = "google/gemini-2.5-pro-preview" // for chat
|
||||
export const openRouterDefaultInsightModelId = "deepseek/deepseek-chat-v3-0324" // for insight
|
||||
export const openRouterDefaultAutoCompleteModelId = "google/gemini-2.5-flash-preview-05-20" // for auto complete
|
||||
export const openRouterDefaultEmbeddingModelId = null // this is not supported embedding model
|
||||
export const openRouterDefaultModelInfo: ModelInfo = {
|
||||
@@ -268,6 +271,7 @@ async function fetchOpenRouterModels(): Promise<Record<string, ModelInfo>> {
|
||||
// https://ai.google.dev/gemini-api/docs/models/gemini
|
||||
export type GeminiModelId = keyof typeof geminiModels
|
||||
export const geminiDefaultModelId: GeminiModelId = "gemini-2.5-pro-preview-05-06"
|
||||
export const geminiDefaultInsightModelId: GeminiModelId = "gemini-2.5-flash-preview-05-20"
|
||||
export const geminiDefaultAutoCompleteModelId: GeminiModelId = "gemini-2.5-flash-preview-05-20"
|
||||
export const geminiDefaultEmbeddingModelId: keyof typeof geminiEmbeddingModels = "text-embedding-004"
|
||||
|
||||
@@ -497,6 +501,7 @@ export const geminiEmbeddingModels = {
|
||||
// https://openai.com/api/pricing/
|
||||
export type OpenAiNativeModelId = keyof typeof openAiNativeModels
|
||||
export const openAiNativeDefaultModelId: OpenAiNativeModelId = "gpt-4o"
|
||||
export const openAiNativeDefaultInsightModelId: OpenAiNativeModelId = "gpt-4o-mini"
|
||||
export const openAiNativeDefaultAutoCompleteModelId: OpenAiNativeModelId = "gpt-4o-mini"
|
||||
export const openAiNativeDefaultEmbeddingModelId: keyof typeof openAINativeEmbeddingModels = "text-embedding-3-small"
|
||||
|
||||
@@ -605,6 +610,7 @@ export const openAINativeEmbeddingModels = {
|
||||
// https://api-docs.deepseek.com/quick_start/pricing
|
||||
export type DeepSeekModelId = keyof typeof deepSeekModels
|
||||
export const deepSeekDefaultModelId: DeepSeekModelId = "deepseek-chat"
|
||||
export const deepSeekDefaultInsightModelId: DeepSeekModelId = "deepseek-chat"
|
||||
export const deepSeekDefaultAutoCompleteModelId: DeepSeekModelId = "deepseek-chat"
|
||||
export const deepSeekDefaultEmbeddingModelId = null // this is not supported embedding model
|
||||
|
||||
@@ -635,6 +641,7 @@ export const deepSeekModels = {
|
||||
// https://help.aliyun.com/zh/model-studio/getting-started/
|
||||
export type QwenModelId = keyof typeof qwenModels
|
||||
export const qwenDefaultModelId: QwenModelId = "qwen3-235b-a22b"
|
||||
export const qwenDefaultInsightModelId: QwenModelId = "qwen3-32b"
|
||||
export const qwenDefaultAutoCompleteModelId: QwenModelId = "qwen3-32b"
|
||||
export const qwenDefaultEmbeddingModelId: keyof typeof qwenEmbeddingModels = "text-embedding-v3"
|
||||
|
||||
@@ -937,6 +944,7 @@ export const qwenEmbeddingModels = {
|
||||
// https://docs.siliconflow.cn/
|
||||
export type SiliconFlowModelId = keyof typeof siliconFlowModels
|
||||
export const siliconFlowDefaultModelId: SiliconFlowModelId = "deepseek-ai/DeepSeek-V3"
|
||||
export const siliconFlowDefaultInsightModelId: SiliconFlowModelId = "deepseek-ai/DeepSeek-V3"
|
||||
export const siliconFlowDefaultAutoCompleteModelId: SiliconFlowModelId = "deepseek-ai/DeepSeek-V3"
|
||||
export const siliconFlowDefaultEmbeddingModelId: keyof typeof siliconFlowEmbeddingModels = "BAAI/bge-m3"
|
||||
|
||||
@@ -1420,6 +1428,7 @@ export const siliconFlowEmbeddingModels = {
|
||||
// https://console.groq.com/docs/overview
|
||||
export type GroqModelId = keyof typeof groqModels
|
||||
export const groqDefaultModelId: GroqModelId = "llama-3.3-70b-versatile"
|
||||
export const groqDefaultInsightModelId: GroqModelId = "llama-3.3-70b-versatile"
|
||||
export const groqDefaultAutoCompleteModelId: GroqModelId = "llama-3.3-70b-versatile"
|
||||
export const groqDefaultEmbeddingModelId = null // this is not supported embedding model
|
||||
|
||||
@@ -1581,6 +1590,7 @@ export const groqModels = {
|
||||
// https://docs.x.ai/docs/models
|
||||
export type GrokModelId = keyof typeof grokModels
|
||||
export const grokDefaultModelId: GrokModelId = "grok-3"
|
||||
export const grokDefaultInsightModelId: GrokModelId = "grok-3-mini"
|
||||
export const grokDefaultAutoCompleteModelId: GrokModelId = "grok-3-mini-fast"
|
||||
export const grokDefaultEmbeddingModelId = null // this is not supported embedding model
|
||||
|
||||
@@ -1637,6 +1647,7 @@ export const grokModels = {
|
||||
|
||||
// LocalProvider (本地嵌入模型)
|
||||
export const localProviderDefaultModelId = null // this is not supported for chat/autocomplete
|
||||
export const localProviderDefaultInsightModelId = null // this is not supported for insight
|
||||
export const localProviderDefaultAutoCompleteModelId = null // this is not supported for chat/autocomplete
|
||||
export const localProviderDefaultEmbeddingModelId: keyof typeof localProviderEmbeddingModels = "TaylorAI/bge-micro-v2"
|
||||
|
||||
@@ -1805,77 +1816,103 @@ export const GetEmbeddingModelInfo = (provider: ApiProvider, modelId: string): E
|
||||
}
|
||||
|
||||
// Get default model id for a provider
|
||||
export const GetDefaultModelId = (provider: ApiProvider): { chat: string, autoComplete: string, embedding: string } => {
|
||||
export const GetDefaultModelId = (provider: ApiProvider): { chat: string, insight: string, autoComplete: string, embedding: string } => {
|
||||
switch (provider) {
|
||||
case ApiProvider.Infio:
|
||||
return {
|
||||
"chat": infioDefaultModelId,
|
||||
"insight": infioDefaultInsightModelId,
|
||||
"autoComplete": infioDefaultAutoCompleteModelId,
|
||||
"embedding": infioDefaultEmbeddingModelId,
|
||||
}
|
||||
case ApiProvider.OpenRouter:
|
||||
return {
|
||||
"chat": openRouterDefaultModelId,
|
||||
"insight": openRouterDefaultInsightModelId,
|
||||
"autoComplete": openRouterDefaultAutoCompleteModelId,
|
||||
"embedding": openRouterDefaultEmbeddingModelId,
|
||||
}
|
||||
case ApiProvider.Anthropic:
|
||||
return {
|
||||
"chat": anthropicDefaultModelId,
|
||||
"insight": anthropicDefaultInsightModelId,
|
||||
"autoComplete": anthropicDefaultAutoCompleteModelId,
|
||||
"embedding": anthropicDefaultEmbeddingModelId,
|
||||
}
|
||||
case ApiProvider.OpenAI:
|
||||
return {
|
||||
"chat": openAiNativeDefaultModelId,
|
||||
"insight": openAiNativeDefaultInsightModelId,
|
||||
"autoComplete": openAiNativeDefaultAutoCompleteModelId,
|
||||
"embedding": openAiNativeDefaultEmbeddingModelId,
|
||||
}
|
||||
case ApiProvider.Deepseek:
|
||||
return {
|
||||
"chat": deepSeekDefaultModelId,
|
||||
"insight": deepSeekDefaultInsightModelId,
|
||||
"autoComplete": deepSeekDefaultAutoCompleteModelId,
|
||||
"embedding": deepSeekDefaultEmbeddingModelId,
|
||||
}
|
||||
case ApiProvider.Google:
|
||||
return {
|
||||
"chat": geminiDefaultModelId,
|
||||
"insight": geminiDefaultInsightModelId,
|
||||
"autoComplete": geminiDefaultAutoCompleteModelId,
|
||||
"embedding": geminiDefaultEmbeddingModelId,
|
||||
}
|
||||
case ApiProvider.AlibabaQwen:
|
||||
return {
|
||||
"chat": qwenDefaultModelId,
|
||||
"insight": qwenDefaultInsightModelId,
|
||||
"autoComplete": qwenDefaultAutoCompleteModelId,
|
||||
"embedding": qwenDefaultEmbeddingModelId,
|
||||
}
|
||||
case ApiProvider.SiliconFlow:
|
||||
return {
|
||||
"chat": siliconFlowDefaultModelId,
|
||||
"insight": siliconFlowDefaultInsightModelId,
|
||||
"autoComplete": siliconFlowDefaultAutoCompleteModelId,
|
||||
"embedding": siliconFlowDefaultEmbeddingModelId,
|
||||
}
|
||||
case ApiProvider.Groq:
|
||||
return {
|
||||
"chat": groqDefaultModelId,
|
||||
"insight": groqDefaultInsightModelId,
|
||||
"autoComplete": groqDefaultAutoCompleteModelId,
|
||||
"embedding": groqDefaultEmbeddingModelId,
|
||||
}
|
||||
case ApiProvider.Grok:
|
||||
return {
|
||||
"chat": grokDefaultModelId,
|
||||
"insight": grokDefaultInsightModelId,
|
||||
"autoComplete": grokDefaultAutoCompleteModelId,
|
||||
"embedding": grokDefaultEmbeddingModelId,
|
||||
}
|
||||
case ApiProvider.Ollama:
|
||||
return {
|
||||
"chat": null, // user-configured
|
||||
"insight": null, // user-configured
|
||||
"autoComplete": null, // user-configured
|
||||
"embedding": null, // not supported
|
||||
}
|
||||
case ApiProvider.OpenAICompatible:
|
||||
return {
|
||||
"chat": null, // user-configured
|
||||
"insight": null, // user-configured
|
||||
"autoComplete": null, // user-configured
|
||||
"embedding": null, // user-configured
|
||||
}
|
||||
case ApiProvider.LocalProvider:
|
||||
return {
|
||||
"chat": localProviderDefaultModelId,
|
||||
"insight": localProviderDefaultInsightModelId,
|
||||
"autoComplete": localProviderDefaultAutoCompleteModelId,
|
||||
"embedding": localProviderDefaultEmbeddingModelId,
|
||||
}
|
||||
default:
|
||||
return {
|
||||
"chat": null,
|
||||
"insight": null,
|
||||
"autoComplete": null,
|
||||
"embedding": null,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user