diff --git a/src/components/chat-view/InsightView.tsx b/src/components/chat-view/InsightView.tsx index 5c92026..54e0816 100644 --- a/src/components/chat-view/InsightView.tsx +++ b/src/components/chat-view/InsightView.tsx @@ -723,7 +723,7 @@ const InsightView = () => {
{t('insights.initConfirm.modelLabel')} - {settings.chatModelProvider} / {settings.chatModelId || t('insights.initConfirm.defaultModel')} + {settings.chatModelProvider}/{settings.chatModelId || t('insights.initConfirm.defaultModel')}
diff --git a/src/components/chat-view/SearchView.tsx b/src/components/chat-view/SearchView.tsx index 4a5e442..7c1b3b7 100644 --- a/src/components/chat-view/SearchView.tsx +++ b/src/components/chat-view/SearchView.tsx @@ -69,6 +69,35 @@ const SearchView = () => { // 当前搜索范围信息 const [currentSearchScope, setCurrentSearchScope] = useState('') + // 统计信息状态 + 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 & { similarity: number }) => { // 如果用户正在选择文本,不触发点击事件 const selection = window.getSelection() @@ -355,36 +526,80 @@ const SearchView = () => { return (
- {/* 搜索输入框 */} -
- - - {/* 搜索模式切换 */} -
- - + {/* 头部信息 */} +
+
+

语义索引

+
+ + +
+
+ + {/* 统计信息 */} + {!isLoadingStats && statisticsInfo && ( +
+
+
+ {statisticsInfo.totalChunks} + 个向量块 +
+
+
+ 📄 + {statisticsInfo.totalFiles} + 文件 +
+
+
+
+ )} + + {/* 搜索输入框 */} +
+ + + {/* 搜索模式切换 */} +
+ + +
@@ -398,11 +613,6 @@ const SearchView = () => { `${insightGroupedResults.length} 个文件,${insightResults.length} 个洞察` )}
- {currentSearchScope && ( -
- 搜索范围: {currentSearchScope} -
- )}
)} @@ -413,6 +623,151 @@ const SearchView = () => {
)} + {/* RAG 初始化进度 */} + {isInitializingRAG && ( +
+
+

正在初始化工作区 RAG 向量索引

+

为当前工作区的文件建立向量索引,提高搜索精度

+
+ {ragInitProgress && ragInitProgress.type === 'indexing' && ragInitProgress.indexProgress && ( +
+
+ 建立向量索引 + + {ragInitProgress.indexProgress.completedChunks} / {ragInitProgress.indexProgress.totalChunks} 块 + +
+
+
+
+
+
+ 共 {ragInitProgress.indexProgress.totalFiles} 个文件 +
+
+ {Math.round((ragInitProgress.indexProgress.completedChunks / Math.max(ragInitProgress.indexProgress.totalChunks, 1)) * 100)}% +
+
+
+ )} +
+ )} + + {/* RAG 初始化成功消息 */} + {ragInitSuccess.show && ( +
+
+ +
+ + 工作区 RAG 向量索引初始化完成: {ragInitSuccess.workspaceName} + + + 处理了 {ragInitSuccess.totalFiles} 个文件,生成 {ragInitSuccess.totalChunks} 个向量块 + +
+ +
+
+ )} + + {/* 确认删除对话框 */} + {showDeleteConfirm && ( +
+
+
+

清除工作区索引

+
+
+

+ 将清除当前工作区的所有向量索引数据。 +

+

+ 此操作无法撤销,清除后需要重新初始化索引才能进行语义搜索。 +

+
+ 工作区: {settings.workspace === 'vault' ? '整个 Vault' : settings.workspace} +
+
+
+ + +
+
+
+ )} + + {/* 确认初始化对话框 */} + {showRAGInitConfirm && ( +
+
+
+

{statisticsInfo && (statisticsInfo.totalFiles > 0 || statisticsInfo.totalChunks > 0) ? '更新工作区索引' : '初始化工作区索引'}

+
+
+

+ {statisticsInfo && (statisticsInfo.totalFiles > 0 || statisticsInfo.totalChunks > 0) + ? '将更新当前工作区的向量索引,重新处理所有文件以确保索引最新。' + : '将为当前工作区的所有文件建立向量索引,这将提高语义搜索的准确性。' + } +

+
+
+ 嵌入模型: + + {settings.embeddingModelProvider} / {settings.embeddingModelId || '默认模型'} + +
+
+ 工作区: + + {settings.workspace === 'vault' ? '整个 Vault' : settings.workspace} + +
+
+

+ 此操作可能需要几分钟时间,具体取决于文件数量和大小。 +

+
+
+ + +
+
+
+ )} + {/* 搜索结果 */}
{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; + } `}
diff --git a/src/core/rag/rag-engine.ts b/src/core/rag/rag-engine.ts index e03c42e..d25b2ae 100644 --- a/src/core/rag/rag-engine.ts +++ b/src/core/rag/rag-engine.ts @@ -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 { + 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 { + 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) + } + } } diff --git a/src/database/modules/vector/vector-manager.ts b/src/database/modules/vector/vector-manager.ts index 5ccb058..6f72a1c 100644 --- a/src/database/modules/vector/vector-manager.ts +++ b/src/database/modules/vector/vector-manager.ts @@ -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 { + 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 { + // 获取工作区中的所有文件 + const workspaceFiles = new Set() + + 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 [] + } + } } diff --git a/src/database/modules/vector/vector-repository.ts b/src/database/modules/vector/vector-repository.ts index e7d07d8..c989ed3 100644 --- a/src/database/modules/vector/vector-repository.ts +++ b/src/database/modules/vector/vector-repository.ts @@ -188,4 +188,94 @@ export class VectorRepository { const result = await this.db.query(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) + } + } } diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index 41f8304..dc0494f 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -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', diff --git a/src/lang/locale/zh-cn.ts b/src/lang/locale/zh-cn.ts index 83f2a3d..a89a92a 100644 --- a/src/lang/locale/zh-cn.ts +++ b/src/lang/locale/zh-cn.ts @@ -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: '提供商', diff --git a/src/settings/components/ModelProviderSettings.tsx b/src/settings/components/ModelProviderSettings.tsx index e1999ff..413f9ab 100644 --- a/src/settings/components/ModelProviderSettings.tsx +++ b/src/settings/components/ModelProviderSettings.tsx @@ -105,13 +105,19 @@ const CustomProviderSettings: React.FC = ({ 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 = ({ 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 = ({ plugin, updateModel={updateChatModelId} /> + + > { // 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, }