& { 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,
}