diff --git a/src/components/chat-view/ChatView.tsx b/src/components/chat-view/ChatView.tsx index c3a2e09..3481fe7 100644 --- a/src/components/chat-view/ChatView.tsx +++ b/src/components/chat-view/ChatView.tsx @@ -2,7 +2,7 @@ import * as path from 'path' import { BaseSerializedNode } from '@lexical/clipboard/clipboard' import { useMutation } from '@tanstack/react-query' -import { Box, CircleStop, History, NotebookPen, Plus, Search, Server, SquareSlash, Undo } from 'lucide-react' +import { Box, Brain, CircleStop, History, NotebookPen, Plus, Search, Server, SquareSlash, Undo } from 'lucide-react' import { App, Notice, TFile, TFolder, WorkspaceLeaf } from 'obsidian' import { forwardRef, @@ -51,7 +51,7 @@ import { MentionableCurrentFile, } from '../../types/mentionable' import { ApplyEditToFile, SearchAndReplace } from '../../utils/apply' -import { listFilesAndFolders } from '../../utils/glob-utils' +import { listFilesAndFolders, semanticSearchFiles } from '../../utils/glob-utils' import { getMentionableKey, serializeMentionable, @@ -70,6 +70,7 @@ import CommandsView from './CommandsView' import CustomModeView from './CustomModeView' import FileReadResults from './FileReadResults' import HelloInfo from './HelloInfo' +import InsightView from './InsightView' import MarkdownReasoningBlock from './Markdown/MarkdownReasoningBlock' import McpHubView from './McpHubView'; // Moved after MarkdownReasoningBlock import QueryProgress, { QueryProgressState } from './QueryProgress' @@ -192,7 +193,7 @@ const Chat = forwardRef((props, ref) => { } } - const [tab, setTab] = useState<'chat' | 'commands' | 'custom-mode' | 'mcp' | 'search' | 'history' | 'workspace'>('chat') + const [tab, setTab] = useState<'chat' | 'commands' | 'custom-mode' | 'mcp' | 'search' | 'history' | 'workspace' | 'insights'>('chat') const [selectedSerializedNodes, setSelectedSerializedNodes] = useState([]) @@ -704,24 +705,25 @@ const Chat = forwardRef((props, ref) => { } } } else if (toolArgs.type === 'semantic_search_files') { - const scope_folders = toolArgs.filepath - && toolArgs.filepath !== '' - && toolArgs.filepath !== '.' - && toolArgs.filepath !== '/' - ? { files: [], folders: [toolArgs.filepath] } - : undefined - const results = await (await getRAGEngine()).processQuery({ - query: toolArgs.query, - scope: scope_folders, - }) - let snippets = results.map(({ path, content, metadata }) => { - const contentWithLineNumbers = addLineNumbers(content, metadata.startLine) - return `\n${contentWithLineNumbers}\n` - }).join('\n\n') - if (snippets.length === 0) { - snippets = `No results found for '${toolArgs.query}'` + // 获取当前工作区 + let currentWorkspace: Workspace | null = null + if (settings.workspace && settings.workspace !== 'vault') { + currentWorkspace = await workspaceManager.findByName(String(settings.workspace)) } - const formattedContent = `[semantic_search_files for '${toolArgs.filepath}'] Result:\n${snippets}\n`; + + const snippets = await semanticSearchFiles( + await getRAGEngine(), + toolArgs.query, + toolArgs.filepath, + currentWorkspace || undefined, + app, + await getTransEngine() + ) + + const contextInfo = currentWorkspace + ? `workspace '${currentWorkspace.name}'` + : toolArgs.filepath || 'vault' + const formattedContent = `[semantic_search_files for '${contextInfo}'] Result:\n${snippets}\n`; return { type: 'semantic_search_files', applyMsgId, @@ -866,11 +868,12 @@ const Chat = forwardRef((props, ref) => { try { console.log("call_transformations", toolArgs) // Validate that the transformation type is a valid enum member - if (!Object.values(TransformationType).includes(toolArgs.transformation as TransformationType)) { + const validTransformationTypes = Object.values(TransformationType) as string[] + if (!validTransformationTypes.includes(toolArgs.transformation)) { throw new Error(`Unsupported transformation type: ${toolArgs.transformation}`); } - const transformationType = toolArgs.transformation as TransformationType; + const transformationType = toolArgs.transformation; const transEngine = await getTransEngine(); // Execute the transformation using the TransEngine @@ -1030,7 +1033,7 @@ const Chat = forwardRef((props, ref) => { break; default: - results.push(`❌ 不支持的操作类型: ${operation.action}`); + results.push(`❌ 不支持的操作类型: ${String(operation.action)}`); } } @@ -1067,7 +1070,7 @@ const Chat = forwardRef((props, ref) => { } } else { // 处理未知的工具类型 - throw new Error(`Unsupported tool type: ${toolArgs.type}`); + throw new Error(`Unsupported tool type: ${(toolArgs as any).type || 'unknown'}`); } } catch (error) { console.error('Failed to apply changes', error) @@ -1369,6 +1372,18 @@ const Chat = forwardRef((props, ref) => { > + {/* main view */} @@ -1569,6 +1584,10 @@ const Chat = forwardRef((props, ref) => {
+ ) : tab === 'insights' ? ( +
+ +
) : (
diff --git a/src/components/chat-view/InsightView.tsx b/src/components/chat-view/InsightView.tsx new file mode 100644 index 0000000..98a847d --- /dev/null +++ b/src/components/chat-view/InsightView.tsx @@ -0,0 +1,1107 @@ +import { ChevronDown, ChevronRight } from 'lucide-react' +import { useCallback, useEffect, useMemo, useState } from 'react' + +import { useApp } from '../../contexts/AppContext' +import { useSettings } from '../../contexts/SettingsContext' +import { useTrans } from '../../contexts/TransContext' +import { TransformationType } from '../../core/transformations/trans-engine' +import { Workspace } from '../../database/json/workspace/types' +import { WorkspaceManager } from '../../database/json/workspace/WorkspaceManager' +import { SelectSourceInsight } from '../../database/schema' +import { t } from '../../lang/helpers' +import { getFilesWithTag } from '../../utils/glob-utils' +import { openMarkdownFile } from '../../utils/obsidian' + +// 洞察源分组结果接口 +interface InsightFileGroup { + path: string + fileName: string + maxCreatedAt: number + insights: (Omit & { displayTime: string })[] + groupType?: 'file' | 'folder' | 'workspace' +} + +const InsightView = () => { + const { getTransEngine } = useTrans() + const app = useApp() + const { settings } = useSettings() + + // 工作区管理器 + const workspaceManager = useMemo(() => { + return new WorkspaceManager(app) + }, [app]) + + const [insightResults, setInsightResults] = useState<(Omit & { displayTime: string })[]>([]) + const [isLoading, setIsLoading] = useState(false) + const [hasLoaded, setHasLoaded] = useState(false) + // 展开状态管理 - 默认全部展开 + const [expandedFiles, setExpandedFiles] = useState>(new Set()) + // 当前搜索范围信息 + const [currentScope, setCurrentScope] = useState('') + // 初始化洞察状态 + const [isInitializing, setIsInitializing] = useState(false) + const [initProgress, setInitProgress] = useState<{ + stage: string + current: number + total: number + currentItem: string + } | null>(null) + + // 删除洞察状态 + const [isDeleting, setIsDeleting] = useState(false) + const [deletingInsightId, setDeletingInsightId] = useState(null) + + const loadInsights = useCallback(async () => { + setIsLoading(true) + setHasLoaded(true) + + try { + // 获取当前工作区 + let currentWorkspace: Workspace | null = null + if (settings.workspace && settings.workspace !== 'vault') { + currentWorkspace = await workspaceManager.findByName(String(settings.workspace)) + } + + // 设置范围信息 + let scopeDescription = '' + if (currentWorkspace) { + scopeDescription = `工作区: ${currentWorkspace.name}` + } else { + scopeDescription = '整个 Vault' + } + setCurrentScope(scopeDescription) + + const transEngine = await getTransEngine() + const allInsights = await transEngine.getAllInsights() + + // 构建工作区范围集合(包含文件、文件夹、工作区路径) + let workspacePaths: Set | null = null + if (currentWorkspace) { + workspacePaths = new Set() + + // 添加工作区路径 + workspacePaths.add(`workspace:${currentWorkspace.name}`) + + // 处理工作区中的文件夹和标签 + for (const item of currentWorkspace.content) { + if (item.type === 'folder') { + const folderPath = item.content + + // 添加文件夹路径本身 + workspacePaths.add(folderPath) + + // 获取文件夹下的所有文件 + const files = app.vault.getMarkdownFiles().filter(file => + file.path.startsWith(folderPath === '/' ? '' : folderPath + '/') + ) + + // 添加所有文件路径 + files.forEach(file => { + workspacePaths.add(file.path) + + // 添加中间文件夹路径 + const dirPath = file.path.substring(0, file.path.lastIndexOf('/')) + if (dirPath && dirPath !== folderPath) { + let currentPath = folderPath === '/' ? '' : folderPath + const pathParts = dirPath.substring(currentPath.length).split('/').filter(Boolean) + + for (let i = 0; i < pathParts.length; i++) { + currentPath += (currentPath ? '/' : '') + pathParts[i] + workspacePaths.add(currentPath) + } + } + }) + + } else if (item.type === 'tag') { + // 获取标签对应的所有文件 + const tagFiles = getFilesWithTag(item.content, app) + + tagFiles.forEach(filePath => { + workspacePaths.add(filePath) + + // 添加文件所在的文件夹路径 + const dirPath = filePath.substring(0, filePath.lastIndexOf('/')) + if (dirPath) { + const pathParts = dirPath.split('/').filter(Boolean) + let currentPath = '' + + for (let i = 0; i < pathParts.length; i++) { + currentPath += (currentPath ? '/' : '') + pathParts[i] + workspacePaths.add(currentPath) + } + } + }) + } + } + } + + // 过滤洞察 + let filteredInsights = allInsights + if (workspacePaths) { + filteredInsights = allInsights.filter(insight => + workspacePaths.has(insight.source_path) + ) + } + + // 按创建时间排序,取最新的50条 + const sortedInsights = filteredInsights + .sort((a, b) => b.created_at.getTime() - a.created_at.getTime()) + .slice(0, 50) + + // 添加显示时间 + const insightsWithDisplayTime = sortedInsights.map(insight => ({ + ...insight, + displayTime: insight.created_at.toLocaleString('zh-CN') + })) + + setInsightResults(insightsWithDisplayTime) + + } catch (error) { + console.error('加载洞察失败:', error) + setInsightResults([]) + } finally { + setIsLoading(false) + } + }, [getTransEngine, settings, workspaceManager, app]) + + // 组件加载时自动获取洞察 + useEffect(() => { + loadInsights() + }, [loadInsights]) + + // 初始化工作区洞察 + const initializeWorkspaceInsights = useCallback(async () => { + setIsInitializing(true) + setInitProgress(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 transEngine = await getTransEngine() + + // 设置初始进度状态 + setInitProgress({ + stage: '准备初始化工作区洞察', + current: 0, + total: 1, + currentItem: currentWorkspace.name + }) + + // 使用 runTransformation 处理工作区 + const result = await transEngine.runTransformation({ + filePath: currentWorkspace.name, // 工作区名称作为标识 + contentType: 'workspace', + transformationType: TransformationType.HIERARCHICAL_SUMMARY, // 使用分层摘要类型 + model: { + provider: settings.applyModelProvider, + modelId: settings.applyModelId, + }, + saveToDatabase: true, + workspaceMetadata: { + name: currentWorkspace.name, + description: currentWorkspace.metadata?.description || '', + workspace: currentWorkspace + } + }) + + // 更新进度为完成状态 + setInitProgress({ + stage: '正在完成初始化', + current: 1, + total: 1, + currentItem: '保存结果' + }) + + if (result.success) { + console.log('工作区洞察初始化完成:', result.result) + + // 刷新洞察列表 + await loadInsights() + + // 显示成功消息 + console.log(`工作区 "${currentWorkspace.name}" 洞察初始化成功`) + } else { + console.error('工作区洞察初始化失败:', result.error) + throw new Error(result.error || '初始化失败') + } + + } catch (error) { + console.error('初始化工作区洞察时出错:', error) + // 可以在这里添加错误提示 + } finally { + setIsInitializing(false) + setInitProgress(null) + } + }, [getTransEngine, settings, workspaceManager, loadInsights]) + + // 删除工作区洞察 + const deleteWorkspaceInsights = useCallback(async () => { + setIsDeleting(true) + + try { + // 获取当前工作区 + let currentWorkspace: Workspace | null = null + if (settings.workspace && settings.workspace !== 'vault') { + currentWorkspace = await workspaceManager.findByName(String(settings.workspace)) + } + + const transEngine = await getTransEngine() + + // 删除工作区的所有转换 + const result = await transEngine.deleteWorkspaceTransformations(currentWorkspace) + + if (result.success) { + const workspaceName = currentWorkspace?.name || 'vault' + console.log(`工作区 "${workspaceName}" 的 ${result.deletedCount} 个转换已成功删除`) + + // 刷新洞察列表 + await loadInsights() + + // 可以在这里添加用户通知,比如显示删除成功的消息 + } else { + console.error('删除工作区洞察失败:', result.error) + // 可以在这里添加错误提示 + } + + } catch (error) { + console.error('删除工作区洞察时出错:', error) + // 可以在这里添加错误提示 + } finally { + setIsDeleting(false) + } + }, [getTransEngine, settings, workspaceManager, loadInsights]) + + // 删除单个洞察 + const deleteSingleInsight = useCallback(async (insightId: number) => { + setDeletingInsightId(insightId) + + try { + const transEngine = await getTransEngine() + + // 删除单个洞察 + const result = await transEngine.deleteSingleInsight(insightId) + + if (result.success) { + console.log(`洞察 ID ${insightId} 已成功删除`) + + // 刷新洞察列表 + await loadInsights() + } else { + console.error('删除洞察失败:', result.error) + // 可以在这里添加错误提示 + } + + } catch (error) { + console.error('删除洞察时出错:', error) + // 可以在这里添加错误提示 + } finally { + setDeletingInsightId(null) + } + }, [getTransEngine, loadInsights]) + + const handleInsightClick = (insight: Omit) => { + // 如果用户正在选择文本,不触发点击事件 + const selection = window.getSelection() + if (selection && selection.toString().length > 0) { + return + } + + console.debug('🔍 [InsightView] 点击洞察结果:', { + id: insight.id, + path: insight.source_path, + type: insight.insight_type, + sourceType: insight.source_type, + content: insight.insight.substring(0, 100) + '...' + }) + + // 检查路径是否存在 + if (!insight.source_path) { + console.error('❌ [InsightView] 文件路径为空') + return + } + + // 根据洞察类型处理不同的点击行为 + if (insight.source_path.startsWith('workspace:')) { + // 工作区洞察 - 显示详细信息或切换工作区 + const workspaceName = insight.source_path.replace('workspace:', '') + console.debug('🌐 [InsightView] 点击工作区洞察:', workspaceName) + // TODO: 可以实现切换到该工作区或显示工作区详情 + return + } else if (insight.source_type === 'folder') { + // 文件夹洞察 - 在文件管理器中显示文件夹 + console.debug('📁 [InsightView] 点击文件夹洞察:', insight.source_path) + + // 尝试在 Obsidian 文件管理器中显示文件夹 + const folder = app.vault.getAbstractFileByPath(insight.source_path) + if (folder) { + // 在文件管理器中显示文件夹 + const fileExplorer = app.workspace.getLeavesOfType('file-explorer')[0] + if (fileExplorer) { + // @ts-expect-error 使用 Obsidian 内部 API + fileExplorer.view.revealInFolder(folder) + } + console.debug('✅ [InsightView] 在文件管理器中显示文件夹') + } else { + console.warn('❌ [InsightView] 文件夹不存在:', insight.source_path) + } + return + } else { + // 文件洞察 - 正常打开文件 + const file = app.vault.getFileByPath(insight.source_path) + if (!file) { + console.error('❌ [InsightView] 在vault中找不到文件:', insight.source_path) + return + } + + console.debug('✅ [InsightView] 文件存在,准备打开:', { + file: file.path + }) + + try { + openMarkdownFile(app, insight.source_path) + console.debug('✅ [InsightView] 成功调用openMarkdownFile') + } catch (error) { + console.error('❌ [InsightView] 调用openMarkdownFile失败:', error) + } + } + } + + const toggleFileExpansion = (filePath: string) => { + // 如果用户正在选择文本,不触发点击事件 + const selection = window.getSelection() + if (selection && selection.toString().length > 0) { + return + } + + const newExpandedFiles = new Set(expandedFiles) + if (newExpandedFiles.has(filePath)) { + newExpandedFiles.delete(filePath) + } else { + newExpandedFiles.add(filePath) + } + setExpandedFiles(newExpandedFiles) + } + + // 按源路径分组并排序 + const insightGroupedResults = useMemo(() => { + if (!insightResults.length) return [] + + // 按源路径分组 + const sourceGroups = new Map() + + insightResults.forEach(result => { + const sourcePath = result.source_path + let displayName = sourcePath + let groupType = 'file' + + // 根据源路径类型确定显示名称和类型 + if (sourcePath.startsWith('workspace:')) { + const workspaceName = sourcePath.replace('workspace:', '') + displayName = `🌐 工作区: ${workspaceName}` + groupType = 'workspace' + } else if (result.source_type === 'folder') { + displayName = `📁 ${sourcePath.split('/').pop() || sourcePath}` + groupType = 'folder' + } else { + displayName = sourcePath.split('/').pop() || sourcePath + groupType = 'file' + } + + if (!sourceGroups.has(sourcePath)) { + sourceGroups.set(sourcePath, { + path: sourcePath, + fileName: displayName, + maxCreatedAt: result.created_at.getTime(), + insights: [], + groupType: groupType === 'workspace' ? 'workspace' : groupType === 'folder' ? 'folder' : 'file' + }) + } + + const group = sourceGroups.get(sourcePath) + if (group) { + group.insights.push(result) + // 更新最新创建时间 + if (result.created_at.getTime() > group.maxCreatedAt) { + group.maxCreatedAt = result.created_at.getTime() + } + } + }) + + // 对每个组内的洞察按创建时间排序 + sourceGroups.forEach(group => { + group.insights.sort((a, b) => b.created_at.getTime() - a.created_at.getTime()) + }) + + // 按类型和时间排序:工作区 > 文件夹 > 文件 + return Array.from(sourceGroups.values()).sort((a, b) => { + // 首先按类型排序 + const typeOrder = { workspace: 0, folder: 1, file: 2 } + const typeComparison = typeOrder[a.groupType || 'file'] - typeOrder[b.groupType || 'file'] + if (typeComparison !== 0) return typeComparison + + // 同类型按时间排序 + return b.maxCreatedAt - a.maxCreatedAt + }) + }, [insightResults]) + + // 获取洞察类型的显示名称 + const getInsightTypeDisplayName = (insightType: string) => { + const typeMapping: Record = { + 'dense_summary': '📋 密集摘要', + 'simple_summary': '📄 简单摘要', + 'key_insights': '💡 关键洞察', + 'analyze_paper': '🔬 论文分析', + 'table_of_contents': '📑 目录大纲', + 'reflections': '🤔 思考反思' + } + return typeMapping[insightType] || insightType.toUpperCase() + } + + return ( +
+ {/* 头部信息 */} +
+
+

{t('insights.title') || 'AI 洞察'}

+
+ + + +
+
+ + {/* 结果统计 */} + {hasLoaded && !isLoading && ( +
+
+ {insightGroupedResults.length} 个项目,{insightResults.length} 个洞察 + {insightGroupedResults.length > 0 && ( + + {' '}( + {insightGroupedResults.filter(g => g.groupType === 'workspace').length > 0 && + `${insightGroupedResults.filter(g => g.groupType === 'workspace').length}工作区 `} + {insightGroupedResults.filter(g => g.groupType === 'folder').length > 0 && + `${insightGroupedResults.filter(g => g.groupType === 'folder').length}文件夹 `} + {insightGroupedResults.filter(g => g.groupType === 'file').length > 0 && + `${insightGroupedResults.filter(g => g.groupType === 'file').length}文件`} + ) + + )} +
+ {currentScope && ( +
+ 范围: {currentScope} +
+ )} +
+ )} +
+ + {/* 加载进度 */} + {isLoading && ( +
+ 正在加载洞察... +
+ )} + + {/* 初始化进度 */} + {isInitializing && ( +
+
+

正在初始化工作区洞察...

+

这可能需要几分钟时间,请耐心等待

+
+ {initProgress && ( +
+
+ {initProgress.stage} + + {initProgress.current} / {initProgress.total} + +
+
+
+
+
+ 正在处理: {initProgress.currentItem} +
+
+ )} +
+ )} + + {/* 洞察结果 */} +
+ {!isLoading && insightGroupedResults.length > 0 && ( +
+ {insightGroupedResults.map((fileGroup) => ( +
+ {/* 文件头部 */} +
toggleFileExpansion(fileGroup.path)} + > +
+
+
+ {expandedFiles.has(fileGroup.path) ? ( + + ) : ( + + )} + {fileGroup.fileName} +
+
+ + {fileGroup.insights.length} 个洞察 + +
+
+
+ {fileGroup.path} +
+ {Array.from(new Set(fileGroup.insights.map(insight => insight.insight_type))) + .map(type => ( + + {getInsightTypeDisplayName(type)} + + )) + } +
+
+
+
+ + {/* 洞察列表 */} + {expandedFiles.has(fileGroup.path) && ( +
+ {fileGroup.insights.map((insight, insightIndex) => ( +
handleInsightClick(insight)} + > +
+
+ {insightIndex + 1} + + {getInsightTypeDisplayName(insight.insight_type)} + + + {insight.displayTime} + +
+
+ +
+
+
+
+ {insight.insight} +
+
+
+ ))} +
+ )} +
+ ))} +
+ )} + + {!isLoading && hasLoaded && insightGroupedResults.length === 0 && ( +
+

当前范围内没有找到洞察数据

+

+ 请尝试在文档上运行转换工具来生成 AI 洞察 +

+
+ )} +
+ + {/* 样式 */} + +
+ ) +} + +export default InsightView diff --git a/src/components/chat-view/SearchView.tsx b/src/components/chat-view/SearchView.tsx index 93fc8f4..d42fe8e 100644 --- a/src/components/chat-view/SearchView.tsx +++ b/src/components/chat-view/SearchView.tsx @@ -1,12 +1,17 @@ import { SerializedEditorState } from 'lexical' import { ChevronDown, ChevronRight } from 'lucide-react' -import { useCallback, useMemo, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import ReactMarkdown from 'react-markdown' import { useApp } from '../../contexts/AppContext' import { useRAG } from '../../contexts/RAGContext' +import { useSettings } from '../../contexts/SettingsContext' +import { useTrans } from '../../contexts/TransContext' +import { Workspace } from '../../database/json/workspace/types' +import { WorkspaceManager } from '../../database/json/workspace/WorkspaceManager' import { SelectVector } from '../../database/schema' import { Mentionable } from '../../types/mentionable' +import { getFilesWithTag } from '../../utils/glob-utils' import { openMarkdownFile } from '../../utils/obsidian' import SearchInputWithActions, { SearchInputRef } from './chat-input/SearchInputWithActions' @@ -20,18 +25,49 @@ interface FileGroup { blocks: (Omit & { similarity: number })[] } +// 洞察文件分组结果接口 +interface InsightFileGroup { + path: string + fileName: string + maxSimilarity: number + insights: Array<{ + id: string + insight: string + insight_type: string + similarity: number + source_path: string + }> +} + const SearchView = () => { const { getRAGEngine } = useRAG() + const { getTransEngine } = useTrans() const app = useApp() + const { settings } = useSettings() const searchInputRef = useRef(null) + + // 工作区管理器 + const workspaceManager = useMemo(() => { + return new WorkspaceManager(app) + }, [app]) const [searchResults, setSearchResults] = useState<(Omit & { similarity: number })[]>([]) + const [insightResults, setInsightResults] = useState>([]) const [isSearching, setIsSearching] = useState(false) const [hasSearched, setHasSearched] = useState(false) + const [searchMode, setSearchMode] = useState<'notes' | 'insights'>('notes') // 搜索模式:笔记或洞察 // 展开状态管理 - 默认全部展开 const [expandedFiles, setExpandedFiles] = useState>(new Set()) // 新增:mentionables 状态管理 const [mentionables, setMentionables] = useState([]) const [searchEditorState, setSearchEditorState] = useState(null) + // 当前搜索范围信息 + const [currentSearchScope, setCurrentSearchScope] = useState('') const handleSearch = useCallback(async (editorState?: SerializedEditorState) => { let searchTerm = '' @@ -43,7 +79,9 @@ const SearchView = () => { if (!searchTerm.trim()) { setSearchResults([]) + setInsightResults([]) setHasSearched(false) + setCurrentSearchScope('') return } @@ -51,23 +89,87 @@ const SearchView = () => { setHasSearched(true) try { - const ragEngine = await getRAGEngine() - const results = await ragEngine.processQuery({ - query: searchTerm, - limit: 50, // 使用用户选择的限制数量 - }) + // 获取当前工作区 + let currentWorkspace: Workspace | null = null + if (settings.workspace && settings.workspace !== 'vault') { + currentWorkspace = await workspaceManager.findByName(String(settings.workspace)) + } - setSearchResults(results) - // 默认展开所有文件 - // const uniquePaths = new Set(results.map(r => r.path)) - // setExpandedFiles(new Set(uniquePaths)) + // 设置搜索范围信息 + let scopeDescription = '' + if (currentWorkspace) { + scopeDescription = `工作区: ${currentWorkspace.name}` + } else { + scopeDescription = '整个 Vault' + } + setCurrentSearchScope(scopeDescription) + + // 构建搜索范围 + let scope: { files: string[], folders: string[] } | undefined + if (currentWorkspace) { + const folders: string[] = [] + const files: string[] = [] + + // 处理工作区中的文件夹和标签 + for (const item of currentWorkspace.content) { + if (item.type === 'folder') { + folders.push(item.content) + } else if (item.type === 'tag') { + // 获取标签对应的所有文件 + const tagFiles = getFilesWithTag(item.content, app) + files.push(...tagFiles) + } + } + + // 只有当有文件夹或文件时才设置 scope + if (folders.length > 0 || files.length > 0) { + scope = { files, folders } + } + } + + if (searchMode === 'notes') { + // 搜索原始笔记 + const ragEngine = await getRAGEngine() + const results = await ragEngine.processQuery({ + query: searchTerm, + scope: scope, + limit: 50, + }) + + setSearchResults(results) + setInsightResults([]) + } else { + // 搜索洞察 + const transEngine = await getTransEngine() + const results = await transEngine.processQuery({ + query: searchTerm, + scope: scope, + limit: 50, + minSimilarity: 0.3, + }) + + setInsightResults(results) + setSearchResults([]) + } } catch (error) { console.error('搜索失败:', error) setSearchResults([]) + setInsightResults([]) } finally { setIsSearching(false) } - }, [getRAGEngine]) + }, [getRAGEngine, getTransEngine, settings, workspaceManager, app, searchMode]) + + // 当搜索模式切换时,如果已经搜索过,重新执行搜索 + useEffect(() => { + if (hasSearched && searchEditorState) { + // 延迟执行避免状态更新冲突 + const timer = setTimeout(() => { + handleSearch(searchEditorState) + }, 100) + return () => clearTimeout(timer) + } + }, [searchMode, handleSearch]) // 监听搜索模式变化 const handleResultClick = (result: Omit & { similarity: number }) => { // 如果用户正在选择文本,不触发点击事件 @@ -170,7 +272,7 @@ const SearchView = () => { ) } - // 按文件分组并排序 + // 按文件分组并排序 - 原始笔记 const groupedResults = useMemo(() => { if (!searchResults.length) return [] @@ -209,6 +311,45 @@ const SearchView = () => { return Array.from(fileGroups.values()).sort((a, b) => b.maxSimilarity - a.maxSimilarity) }, [searchResults]) + // 按文件分组并排序 - 洞察 + const insightGroupedResults = useMemo(() => { + if (!insightResults.length) return [] + + // 按文件路径分组 + const fileGroups = new Map() + + insightResults.forEach(result => { + const filePath = result.source_path + const fileName = filePath.split('/').pop() || filePath + + if (!fileGroups.has(filePath)) { + fileGroups.set(filePath, { + path: filePath, + fileName, + maxSimilarity: result.similarity, + insights: [] + }) + } + + const group = fileGroups.get(filePath) + if (group) { + group.insights.push(result) + // 更新最高相似度 + if (result.similarity > group.maxSimilarity) { + group.maxSimilarity = result.similarity + } + } + }) + + // 对每个文件内的洞察按相似度排序 + fileGroups.forEach(group => { + group.insights.sort((a, b) => b.similarity - a.similarity) + }) + + // 将文件按最高相似度排序 + return Array.from(fileGroups.values()).sort((a, b) => b.maxSimilarity - a.maxSimilarity) + }, [insightResults]) + const totalBlocks = searchResults.length const totalFiles = groupedResults.length @@ -227,12 +368,41 @@ const SearchView = () => { autoFocus={true} disabled={isSearching} /> + + {/* 搜索模式切换 */} +
+ + +
{/* 结果统计 */} {hasSearched && !isSearching && (
- {totalFiles} 个文件,{totalBlocks} 个块 +
+ {searchMode === 'notes' ? ( + `${totalFiles} 个文件,${totalBlocks} 个块` + ) : ( + `${insightGroupedResults.length} 个文件,${insightResults.length} 个洞察` + )} +
+ {currentSearchScope && ( +
+ 搜索范围: {currentSearchScope} +
+ )}
)} @@ -245,70 +415,127 @@ const SearchView = () => { {/* 搜索结果 */}
- {!isSearching && groupedResults.length > 0 && ( -
- {groupedResults.map((fileGroup) => ( -
- {/* 文件头部 */} -
toggleFileExpansion(fileGroup.path)} - > -
-
-
- {expandedFiles.has(fileGroup.path) ? ( - - ) : ( - - )} - {/* {fileIndex + 1} */} - {fileGroup.fileName} + {searchMode === 'notes' ? ( + // 原始笔记搜索结果 + !isSearching && groupedResults.length > 0 && ( +
+ {groupedResults.map((fileGroup) => ( +
+ {/* 文件头部 */} +
toggleFileExpansion(fileGroup.path)} + > +
+
+
+ {expandedFiles.has(fileGroup.path) ? ( + + ) : ( + + )} + {fileGroup.fileName} +
-
- {/* {fileGroup.blocks.length} 块 */} - {/* - {fileGroup.maxSimilarity.toFixed(3)} - */} +
+ {fileGroup.path}
-
- {fileGroup.path} -
-
- {/* 文件块列表 */} - {expandedFiles.has(fileGroup.path) && ( -
- {fileGroup.blocks.map((result, blockIndex) => ( -
handleResultClick(result)} - > -
- {blockIndex + 1} - - L{result.metadata.startLine}-{result.metadata.endLine} - - - {result.similarity.toFixed(3)} - + {/* 文件块列表 */} + {expandedFiles.has(fileGroup.path) && ( +
+ {fileGroup.blocks.map((result, blockIndex) => ( +
handleResultClick(result)} + > +
+ {blockIndex + 1} + + L{result.metadata.startLine}-{result.metadata.endLine} + + + {result.similarity.toFixed(3)} + +
+
+ {renderMarkdownContent(result.content)} +
-
- {renderMarkdownContent(result.content)} + ))} +
+ )} +
+ ))} +
+ ) + ) : ( + // AI 洞察搜索结果 + !isSearching && insightGroupedResults.length > 0 && ( +
+ {insightGroupedResults.map((fileGroup) => ( +
+ {/* 文件头部 */} +
toggleFileExpansion(fileGroup.path)} + > +
+
+
+ {expandedFiles.has(fileGroup.path) ? ( + + ) : ( + + )} + {fileGroup.fileName}
- ))} +
+ {fileGroup.path} +
+
- )} -
- ))} -
+ + {/* 洞察列表 */} + {expandedFiles.has(fileGroup.path) && ( +
+ {fileGroup.insights.map((insight, insightIndex) => ( +
+
+ {insightIndex + 1} + + {insight.insight_type.toUpperCase()} + + + {insight.similarity.toFixed(3)} + +
+
+
+ {insight.insight} +
+
+
+ ))} +
+ )} +
+ ))} +
+ ) )} - {!isSearching && hasSearched && groupedResults.length === 0 && ( + {!isSearching && hasSearched && ( + (searchMode === 'notes' && groupedResults.length === 0) || + (searchMode === 'insights' && insightGroupedResults.length === 0) + ) && (

未找到相关结果

@@ -329,12 +556,54 @@ const SearchView = () => { padding: 12px; } + .obsidian-search-mode-toggle { + display: flex; + gap: 8px; + margin-top: 8px; + padding: 4px; + background-color: var(--background-modifier-border); + border-radius: var(--radius-m); + } + + .obsidian-search-mode-btn { + flex: 1; + padding: 6px 12px; + background-color: transparent; + border: none; + border-radius: var(--radius-s); + color: var(--text-muted); + font-size: var(--font-ui-small); + cursor: pointer; + transition: all 0.2s ease; + } + + .obsidian-search-mode-btn:hover { + background-color: var(--background-modifier-hover); + color: var(--text-normal); + } + + .obsidian-search-mode-btn.active { + background-color: var(--interactive-accent); + color: var(--text-on-accent); + font-weight: 500; + } + .obsidian-search-stats { padding: 8px 12px; font-size: var(--font-ui-small); color: var(--text-muted); } + .obsidian-search-stats-line { + margin-bottom: 2px; + } + + .obsidian-search-scope { + font-size: var(--font-ui-smaller); + color: var(--text-accent); + font-weight: 500; + } + .obsidian-search-loading { padding: 20px; text-align: center; @@ -594,6 +863,27 @@ const SearchView = () => { margin: 0; font-size: var(--font-ui-medium); } + + /* 洞察结果特殊样式 */ + .obsidian-result-insight-type { + color: var(--text-accent); + font-size: var(--font-ui-smaller); + font-family: var(--font-monospace); + font-weight: 600; + background-color: var(--background-modifier-border); + padding: 2px 6px; + border-radius: var(--radius-s); + flex-grow: 1; + } + + .obsidian-insight-content { + color: var(--text-normal); + font-size: var(--font-ui-medium); + line-height: 1.5; + white-space: pre-wrap; + user-select: text; + cursor: text; + } `}
diff --git a/src/core/prompts/transformations/concise-dense-summary.ts b/src/core/prompts/transformations/concise-dense-summary.ts new file mode 100644 index 0000000..f77d6f7 --- /dev/null +++ b/src/core/prompts/transformations/concise-dense-summary.ts @@ -0,0 +1,13 @@ +export const CONCISE_DENSE_SUMMARY_PROMPT = `# MISSION +You are a Sparse Priming Representation (SPR) writer. Your goal is to render the user's input as an extremely concise and distilled SPR. + +# THEORY +LLMs are a kind of deep neural network. They have been demonstrated to embed knowledge, abilities, and concepts, ranging from reasoning to planning, and even to theory of mind. These are called latent abilities and latent content, collectively referred to as latent space. The latent space of an LLM can be activated with the correct series of words as inputs, which will create a useful internal state of the neural network. You need to provide the most potent and succinct cues to prime another model effectively. Less is more. + +# METHODOLOGY +Render the input as a distilled list of the **most critical** assertions, concepts, and associations. The idea is to capture the absolute essence with minimal words. Use complete sentences. + +**!! CRITICAL INSTRUCTION !!** +**Your output MUST BE EXTREMELY CONCISE. Aim for a dense paragraph of no more than 3-5 sentences OR a bulleted list of 3-5 key points. Focus only on the highest-level insights and most essential concepts.**`; + +export const CONCISE_DENSE_SUMMARY_DESCRIPTION = "Creates an extremely concise, rich summary of the content focusing on the most essential concepts"; diff --git a/src/core/prompts/transformations/dense-summary.ts b/src/core/prompts/transformations/dense-summary.ts index fe8bb1f..017646d 100644 --- a/src/core/prompts/transformations/dense-summary.ts +++ b/src/core/prompts/transformations/dense-summary.ts @@ -1,10 +1,13 @@ export const DENSE_SUMMARY_PROMPT = `# MISSION -You are a Sparse Priming Representation (SPR) writer. An SPR is a particular kind of use of language for advanced NLP, NLU, and NLG tasks, particularly useful for the latest generation of Large Language Models (LLMs). You will be given information by the USER which you are to render as an SPR. +You are a Sparse Priming Representation (SPR) writer. Your goal is to render the user's input as an extremely concise and distilled SPR. # THEORY -LLMs are a kind of deep neural network. They have been demonstrated to embed knowledge, abilities, and concepts, ranging from reasoning to planning, and even to theory of mind. These are called latent abilities and latent content, collectively referred to as latent space. The latent space of an LLM can be activated with the correct series of words as inputs, which will create a useful internal state of the neural network. This is not unlike how the right shorthand cues can prime a human mind to think in a certain way. Like human minds, LLMs are associative, meaning you only need to use the correct associations to 'prime' another model to think in the same way. +LLMs are a kind of deep neural network. They have been demonstrated to embed knowledge, abilities, and concepts, ranging from reasoning to planning, and even to theory of mind. These are called latent abilities and latent content, collectively referred to as latent space. The latent space of an LLM can be activated with the correct series of words as inputs, which will create a useful internal state of the neural network. You need to provide the most potent and succinct cues to prime another model effectively. Less is more. # METHODOLOGY -Render the input as a distilled list of succinct statements, assertions, associations, concepts, analogies, and metaphors. The idea is to capture as much, conceptually, as possible but with as few words as possible. Write it in a way that makes sense to you, as the future audience will be another language model, not a human. Use complete sentences.`; +Render the input as a distilled list of the **most critical** assertions, concepts, and associations. The idea is to capture the absolute essence with minimal words. Use complete sentences. -export const DENSE_SUMMARY_DESCRIPTION = "Creates a rich, deep summary of the content"; +**!! CRITICAL INSTRUCTION !!** +**Your output MUST BE EXTREMELY CONCISE. Aim for a dense paragraph of no more than 3-5 sentences OR a bulleted list of 3-5 key points. Focus only on the highest-level insights and most essential concepts.**`; + +export const DENSE_SUMMARY_DESCRIPTION = "Creates an extremely concise, rich summary of the content focusing on the most essential concepts"; diff --git a/src/core/prompts/transformations/hierarchical-summary.ts b/src/core/prompts/transformations/hierarchical-summary.ts new file mode 100644 index 0000000..d64f8a2 --- /dev/null +++ b/src/core/prompts/transformations/hierarchical-summary.ts @@ -0,0 +1,14 @@ +export const HIERARCHICAL_SUMMARY_PROMPT = `# MISSION +You are an expert knowledge architect responsible for creating hierarchical summaries of a knowledge base. You will be given a collection of summaries from files and sub-folders within a specific directory. Your mission is to synthesize these individual summaries into a single, cohesive, and abstract summary for the parent directory. + +# METHODOLOGY +1. **Identify Core Themes**: Analyze the provided summaries to identify the main topics, recurring concepts, and overarching themes present in the directory. +2. **Synthesize, Don't Just List**: Do not simply concatenate or list the child summaries. Instead, integrate them. Explain what this collection of information represents as a whole. For example, instead of "This folder contains a summary of A and a summary of B," write "This folder explores the relationship between A and B, focusing on..." +3. **Capture Structure**: Briefly mention the types of content within (e.g., "Contains technical specifications, meeting notes, and final reports related to Project X."). +4. **Be Abstract and Concise**: The goal is to create a higher-level understanding. The output should be a dense, short paragraph that gives a bird's-eye view of the directory's contents and purpose. +5. **Focus on Relationships**: Highlight how the different pieces of content relate to each other and what they collectively achieve or represent. + +**!! CRITICAL INSTRUCTION !!** +**Your output MUST BE CONCISE. Aim for 2-4 sentences that capture the essence and purpose of this directory as a cohesive unit. Focus on the highest-level insights and connections.**`; + +export const HIERARCHICAL_SUMMARY_DESCRIPTION = "Creates a concise, high-level summary that synthesizes content from multiple files and folders into a cohesive understanding of the directory's purpose and themes"; diff --git a/src/core/transformations/trans-engine.ts b/src/core/transformations/trans-engine.ts index 8fcb8c4..ce94482 100644 --- a/src/core/transformations/trans-engine.ts +++ b/src/core/transformations/trans-engine.ts @@ -1,5 +1,5 @@ import { Result, err, ok } from "neverthrow"; -import { App } from 'obsidian'; +import { App, TFolder } from 'obsidian'; import { DBManager } from '../../database/database-manager'; import { InsightManager } from '../../database/modules/insight/insight-manager'; @@ -11,16 +11,65 @@ import { readTFileContentPdf } from '../../utils/obsidian'; import { tokenCount } from '../../utils/token'; import LLMManager from '../llm/manager'; import { ANALYZE_PAPER_DESCRIPTION, ANALYZE_PAPER_PROMPT } from '../prompts/transformations/analyze-paper'; +import { CONCISE_DENSE_SUMMARY_DESCRIPTION, CONCISE_DENSE_SUMMARY_PROMPT } from '../prompts/transformations/concise-dense-summary'; import { DENSE_SUMMARY_DESCRIPTION, DENSE_SUMMARY_PROMPT } from '../prompts/transformations/dense-summary'; +import { HIERARCHICAL_SUMMARY_DESCRIPTION, HIERARCHICAL_SUMMARY_PROMPT } from '../prompts/transformations/hierarchical-summary'; import { KEY_INSIGHTS_DESCRIPTION, KEY_INSIGHTS_PROMPT } from '../prompts/transformations/key-insights'; import { REFLECTIONS_DESCRIPTION, REFLECTIONS_PROMPT } from '../prompts/transformations/reflections'; import { SIMPLE_SUMMARY_DESCRIPTION, SIMPLE_SUMMARY_PROMPT } from '../prompts/transformations/simple-summary'; import { TABLE_OF_CONTENTS_DESCRIPTION, TABLE_OF_CONTENTS_PROMPT } from '../prompts/transformations/table-of-contents'; import { getEmbeddingModel } from '../rag/embedding'; +/** + * 并发控制工具类 + */ +class ConcurrencyLimiter { + private maxConcurrency: number; + private currentRunning: number = 0; + private queue: Array<() => Promise> = []; + + constructor(maxConcurrency: number = 3) { + this.maxConcurrency = maxConcurrency; + } + + async execute(task: () => Promise): Promise { + return new Promise((resolve, reject) => { + const wrappedTask = async () => { + try { + this.currentRunning++; + const result = await task(); + resolve(result); + } catch (error) { + reject(error); + } finally { + this.currentRunning--; + this.processQueue(); + } + }; + + if (this.currentRunning < this.maxConcurrency) { + wrappedTask(); + } else { + this.queue.push(wrappedTask); + } + }); + } + + private processQueue() { + if (this.queue.length > 0 && this.currentRunning < this.maxConcurrency) { + const nextTask = this.queue.shift(); + if (nextTask) { + nextTask(); + } + } + } +} + // 转换类型枚举 export enum TransformationType { DENSE_SUMMARY = 'dense_summary', + CONCISE_DENSE_SUMMARY = 'concise_dense_summary', + HIERARCHICAL_SUMMARY = 'hierarchical_summary', ANALYZE_PAPER = 'analyze_paper', SIMPLE_SUMMARY = 'simple_summary', KEY_INSIGHTS = 'key_insights', @@ -44,6 +93,18 @@ export const TRANSFORMATIONS: Record = description: DENSE_SUMMARY_DESCRIPTION, maxTokens: 4000 }, + [TransformationType.CONCISE_DENSE_SUMMARY]: { + type: TransformationType.CONCISE_DENSE_SUMMARY, + prompt: CONCISE_DENSE_SUMMARY_PROMPT, + description: CONCISE_DENSE_SUMMARY_DESCRIPTION, + maxTokens: 4000 + }, + [TransformationType.HIERARCHICAL_SUMMARY]: { + type: TransformationType.HIERARCHICAL_SUMMARY, + prompt: HIERARCHICAL_SUMMARY_PROMPT, + description: HIERARCHICAL_SUMMARY_DESCRIPTION, + maxTokens: 3000 + }, [TransformationType.ANALYZE_PAPER]: { type: TransformationType.ANALYZE_PAPER, prompt: ANALYZE_PAPER_PROMPT, @@ -78,12 +139,19 @@ export const TRANSFORMATIONS: Record = // 转换参数接口 export interface TransformationParams { - filePath: string; // 必须的文件路径 - contentType?: 'document' | 'tag' | 'folder'; + filePath: string; // 文件路径、文件夹路径或工作区标识 + contentType?: 'document' | 'tag' | 'folder' | 'workspace'; transformationType: TransformationType; model?: LLMModel; maxContentTokens?: number; saveToDatabase?: boolean; + // 对于 workspace 类型,可以传入额外的元数据 + workspaceMetadata?: { + name: string; + description?: string; + // 完整的 workspace 对象,用于获取配置信息 + workspace?: import('../../database/json/workspace/types').Workspace; + }; } // 转换结果接口 @@ -347,7 +415,7 @@ export class TransEngine { // 查找匹配的转换类型和修改时间的洞察 const matchingInsight = existingInsights.find(insight => - insight.insight_type === transformationType && + insight.insight_type === transformationType.toString() && insight.source_mtime === sourceMtime ); @@ -424,7 +492,7 @@ export class TransEngine { transformationType: TransformationType, sourcePath: string, sourceMtime: number, - contentType: string + contentType: 'document' | 'tag' | 'folder' ): Promise { if (!this.embeddingModel || !this.insightManager) { return; @@ -455,7 +523,7 @@ export class TransEngine { } /** - * 主要的转换执行方法 + * 主要的转换执行方法 - 支持所有类型的转换 */ async runTransformation(params: TransformationParams): Promise { console.log("runTransformation", params); @@ -465,49 +533,125 @@ export class TransEngine { transformationType, model, maxContentTokens, - saveToDatabase = false + saveToDatabase = false, + workspaceMetadata } = params; try { - // 第一步:获取文件元信息 - const metadataResult = await this.getFileMetadata(filePath); + let content: string; + let sourcePath: string; + let sourceMtime: number; - if (!metadataResult.success) { - return { - success: false, - error: metadataResult.error - }; + // 根据内容类型获取内容和元数据 + switch (contentType) { + case 'document': { + // 第一步:获取文件元信息 + const metadataResult = await this.getFileMetadata(filePath); + if (metadataResult.success === false) { + return { + success: false, + error: metadataResult.error + }; + } + + sourcePath = metadataResult.sourcePath; + sourceMtime = metadataResult.sourceMtime; + + // 检查数据库缓存 + const cacheCheckResult = await this.checkDatabaseCache( + sourcePath, + sourceMtime, + transformationType + ); + if (cacheCheckResult.foundCache) { + return cacheCheckResult.result; + } + + // 获取文件内容 + const fileContentResult = await this.getFileContent(filePath); + if (fileContentResult.success === false) { + return { + success: false, + error: fileContentResult.error + }; + } + content = fileContentResult.fileContent; + break; + } + + case 'folder': { + sourcePath = filePath; + sourceMtime = Date.now(); + + // 检查数据库缓存 + const cacheCheckResult = await this.checkDatabaseCache( + sourcePath, + sourceMtime, + transformationType + ); + if (cacheCheckResult.foundCache) { + return cacheCheckResult.result; + } + + // 获取文件夹内容 + const folderContentResult = await this.processFolderContent(filePath); + if (!folderContentResult.success) { + return { + success: false, + error: folderContentResult.error + }; + } + content = folderContentResult.content; + break; + } + + case 'workspace': { + if (!workspaceMetadata?.workspace) { + return { + success: false, + error: '工作区对象未提供' + }; + } + + sourcePath = `workspace:${workspaceMetadata.workspace.name}`; + sourceMtime = Date.now(); + + // 检查数据库缓存 + const cacheCheckResult = await this.checkDatabaseCache( + sourcePath, + sourceMtime, + transformationType + ); + if (cacheCheckResult.foundCache) { + return cacheCheckResult.result; + } + + // 处理工作区内容 + const workspaceContentResult = await this.processWorkspaceContent( + workspaceMetadata.workspace, + transformationType, + model + ); + + if (!workspaceContentResult.success) { + return { + success: false, + error: workspaceContentResult.error + }; + } + content = workspaceContentResult.content; + break; + } + + default: + return { + success: false, + error: `不支持的内容类型: ${contentType}` + }; } - // 此时TypeScript知道metadataResult.success为true - const { sourcePath, sourceMtime } = metadataResult; - - // 第二步:检查数据库缓存 - const cacheCheckResult = await this.checkDatabaseCache( - sourcePath, - sourceMtime, - transformationType - ); - - if (cacheCheckResult.foundCache) { - return cacheCheckResult.result; - } - - // 第三步:获取文件内容(只有在没有缓存时才执行) - const fileContentResult = await this.getFileContent(filePath); - - if (!fileContentResult.success) { - return { - success: false, - error: fileContentResult.error - }; - } - - // 此时TypeScript知道fileContentResult.success为true - const { fileContent } = fileContentResult; - // 验证内容 - const contentValidation = DocumentProcessor.validateContent(fileContent); + const contentValidation = DocumentProcessor.validateContent(content); if (contentValidation.isErr()) { return { success: false, @@ -526,7 +670,7 @@ export class TransEngine { // 处理文档内容(检查 token 数量并截断) const tokenLimit = maxContentTokens || DocumentProcessor['DEFAULT_MAX_TOKENS']; - const processedDocument = await DocumentProcessor.processContent(fileContent, tokenLimit); + const processedDocument = await DocumentProcessor.processContent(content, tokenLimit); // 使用默认模型或传入的模型 const llmModel: LLMModel = model || { @@ -574,7 +718,7 @@ export class TransEngine { transformationType, sourcePath, sourceMtime, - contentType + contentType === 'workspace' ? 'folder' : contentType // workspace 在数据库中存储为 folder 类型 ); })(); // 立即执行异步函数,但不等待其完成 } @@ -595,6 +739,217 @@ export class TransEngine { } } + /** + * 获取文件夹内容 + */ + private async processFolderContent(folderPath: string): Promise<{ + success: boolean; + content?: string; + error?: string; + }> { + try { + const folder = this.app.vault.getAbstractFileByPath(folderPath); + if (!folder || !(folder instanceof TFolder)) { + return { + success: false, + error: `文件夹不存在: ${folderPath}` + }; + } + + // 获取文件夹直接子级的文件和文件夹 + const directFiles = this.app.vault.getMarkdownFiles().filter(file => { + const fileDirPath = file.path.substring(0, file.path.lastIndexOf('/')); + return fileDirPath === folderPath; + }); + + const directSubfolders = folder.children.filter((child): child is TFolder => child instanceof TFolder); + + if (directFiles.length === 0 && directSubfolders.length === 0) { + return { + success: false, + error: `文件夹为空: ${folderPath}` + }; + } + + // 构建文件夹内容描述 + let content = `# Folder Summary: ${folderPath}\n\n`; + + // 处理直接子文件 + if (directFiles.length > 0) { + content += `## File Content Summaries\n\n`; + const fileSummaries: string[] = []; + + for (const file of directFiles) { + const fileResult = await this.runTransformation({ + filePath: file.path, + contentType: 'document', + transformationType: TransformationType.DENSE_SUMMARY, + saveToDatabase: true + }); + + if (fileResult.success && fileResult.result) { + fileSummaries.push(`### ${file.name}\n${fileResult.result}`); + } else { + console.warn(`处理文件失败: ${file.path}`, fileResult.error); + } + } + + content += fileSummaries.join('\n\n'); + + if (directSubfolders.length > 0) { + content += '\n\n'; + } + } + + // 处理直接子文件夹 + if (directSubfolders.length > 0) { + content += `## Subfolder Summaries\n\n`; + const subfolderSummaries: string[] = []; + + for (const subfolder of directSubfolders) { + const subfolderResult = await this.runTransformation({ + filePath: subfolder.path, + contentType: 'folder', + transformationType: TransformationType.HIERARCHICAL_SUMMARY, + saveToDatabase: true + }); + + if (subfolderResult.success && subfolderResult.result) { + subfolderSummaries.push(`### ${subfolder.name}\n${subfolderResult.result}`); + } else { + console.warn(`处理子文件夹失败: ${subfolder.path}`, subfolderResult.error); + } + } + + content += subfolderSummaries.join('\n\n'); + } + + return { + success: true, + content + }; + + } catch (error) { + return { + success: false, + error: `获取文件夹内容失败: ${error instanceof Error ? error.message : String(error)}` + }; + } + } + + /** + * 处理工作区内容 - 根据workspace配置递归处理文件和文件夹 + */ + private async processWorkspaceContent( + workspace: import('../../database/json/workspace/types').Workspace, + transformationType: TransformationType, + model?: LLMModel + ): Promise<{ + success: boolean; + content?: string; + error?: string; + }> { + try { + // 根据 workspace 配置获取相应的文件和文件夹 + const workspaceFiles: string[] = [] + const workspaceFolders: string[] = [] + + // 解析 workspace 的 content 配置 + for (const contentItem of workspace.content) { + if (contentItem.type === 'folder') { + // 添加文件夹到列表 + workspaceFolders.push(contentItem.content) + } else if (contentItem.type === 'tag') { + // 对于标签类型,搜索包含该标签的文件 + const taggedFiles = this.getFilesByTag(contentItem.content) + workspaceFiles.push(...taggedFiles.map(f => f.path)) + } + } + + if (workspaceFiles.length === 0 && workspaceFolders.length === 0) { + return { + success: false, + error: `工作区 "${workspace.name}" 没有找到任何内容` + } + } + + // 构建工作区内容描述 + let content = `# Workspace Summary: ${workspace.name}\n\n` + const description = typeof workspace.metadata?.description === 'string' ? workspace.metadata.description : undefined + if (description) { + content += `Workspace Description: ${description}\n\n` + } + + const childSummaries: string[] = [] + + // 处理工作区配置的文件 + if (workspaceFiles.length > 0) { + content += `## File Summaries (${workspaceFiles.length} files)\n\n` + + for (const filePath of workspaceFiles) { + const fileName = filePath.split('/').pop() || filePath + + const fileResult = await this.runTransformation({ + filePath: filePath, + contentType: 'document', + transformationType: TransformationType.DENSE_SUMMARY, + model: model, + saveToDatabase: true + }) + + if (fileResult.success && fileResult.result) { + childSummaries.push(`### ${fileName}\n${fileResult.result}`) + } else { + console.warn(`处理文件失败: ${filePath}`, fileResult.error) + childSummaries.push(`### ${fileName}\n*处理失败: ${fileResult.error}*`) + } + } + + if (workspaceFolders.length > 0) { + content += '\n\n' + } + } + + // 处理工作区配置的文件夹 + if (workspaceFolders.length > 0) { + content += `## Folder Summaries (${workspaceFolders.length} folders)\n\n` + + for (const folderPath of workspaceFolders) { + const folderName = folderPath.split('/').pop() || folderPath + + const folderResult = await this.runTransformation({ + filePath: folderPath, + contentType: 'folder', + transformationType: TransformationType.HIERARCHICAL_SUMMARY, + model: model, + saveToDatabase: true + }) + + if (folderResult.success && folderResult.result) { + childSummaries.push(`### ${folderName}/\n${folderResult.result}`) + } else { + console.warn(`处理文件夹失败: ${folderPath}`, folderResult.error) + childSummaries.push(`### ${folderName}/\n*处理失败: ${folderResult.error}*`) + } + } + } + + // 合并所有子摘要 + content += childSummaries.join('\n\n') + + return { + success: true, + content + } + + } catch (error) { + return { + success: false, + error: `处理工作区内容失败: ${error instanceof Error ? error.message : String(error)}` + } + } + } + /** * 后处理转换结果 */ @@ -633,44 +988,16 @@ export class TransEngine { } break; } + + case TransformationType.CONCISE_DENSE_SUMMARY: + case TransformationType.HIERARCHICAL_SUMMARY: + // 新的摘要类型不需要特殊的后处理,保持原样 + break; } return processed; } - /** - * 批量执行转换 - */ - async runBatchTransformations( - filePath: string, - transformationTypes: TransformationType[], - options?: { - model?: LLMModel; - saveToDatabase?: boolean; - } - ): Promise> { - const results: Record = {}; - - // 并行执行所有转换 - const promises = transformationTypes.map(async (type) => { - const result = await this.runTransformation({ - filePath: filePath, - transformationType: type, - model: options?.model, - saveToDatabase: options?.saveToDatabase - }); - return { type, result }; - }); - - const completedResults = await Promise.all(promises); - - for (const { type, result } of completedResults) { - results[type] = result; - } - - return results; - } - /** * 获取所有可用的转换类型和描述 */ @@ -680,4 +1007,592 @@ export class TransEngine { description: config.description })); } -} + + /** + * 查询洞察数据库(类似 RAGEngine 的 processQuery 接口) + */ + async processQuery({ + query, + scope, + limit, + minSimilarity, + insightTypes, + }: { + query: string + scope?: { + files: string[] + folders: string[] + } + limit?: number + minSimilarity?: number + insightTypes?: TransformationType[] + }): Promise< + (Omit & { + similarity: number + })[] + > { + if (!this.embeddingModel || !this.insightManager) { + console.warn('TransEngine: embedding model or insight manager not available') + return [] + } + + try { + // 生成查询向量 + const queryVector = await this.embeddingModel.getEmbedding(query) + + // 构建 sourcePaths 过滤条件 + let sourcePaths: string[] | undefined + if (scope) { + sourcePaths = [] + // 添加直接指定的文件 + if (scope.files.length > 0) { + sourcePaths.push(...scope.files) + } + // 添加文件夹下的所有文件 + if (scope.folders.length > 0) { + for (const folderPath of scope.folders) { + const folder = this.app.vault.getAbstractFileByPath(folderPath) + if (folder && folder instanceof TFolder) { + // 获取文件夹下的所有 Markdown 文件 + const folderFiles = this.app.vault.getMarkdownFiles().filter(file => + file.path.startsWith(folderPath + '/') + ) + sourcePaths.push(...folderFiles.map(f => f.path)) + } + } + } + } + + // 执行相似度搜索 + const results = await this.insightManager.performSimilaritySearch( + queryVector, + this.embeddingModel, + { + minSimilarity: minSimilarity ?? 0.3, // 默认最小相似度 + limit: limit ?? 20, // 默认限制 + sourcePaths: sourcePaths, + insightTypes: insightTypes?.map(type => type.toString()), + } + ) + + return results + } catch (error) { + console.error('TransEngine query failed:', error) + return [] + } + } + + /** + * 获取所有洞察数据 + */ + async getAllInsights(): Promise[]> { + if (!this.embeddingModel || !this.insightManager) { + console.warn('TransEngine: embedding model or insight manager not available') + return [] + } + + try { + const allInsights = await this.insightManager.getAllInsights(this.embeddingModel) + // 移除 embedding 字段,避免返回大量数据 + return allInsights.map((insight) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { embedding, ...rest } = insight; + return rest; + }); + } catch (error) { + console.error('TransEngine getAllInsights failed:', error) + return [] + } + } + + /** + * 根据标签获取文件 + */ + private getFilesByTag(tag: string): import('obsidian').TFile[] { + const files = this.app.vault.getMarkdownFiles() + const taggedFiles: import('obsidian').TFile[] = [] + + for (const file of files) { + // 这里需要检查文件的前置元数据或内容中的标签 + // 简单实现:检查文件内容中是否包含该标签 + try { + const cache = this.app.metadataCache.getFileCache(file) + if (cache?.tags?.some(t => t.tag === `#${tag}` || t.tag === tag)) { + taggedFiles.push(file) + } + } catch (error) { + console.warn(`检查文件标签失败: ${file.path}`, error) + } + } + + return taggedFiles + } + + /** + * 递归处理文件夹 + */ + private async processFolderHierarchically(params: { + folderPath: string + llmModel: LLMModel + concurrencyLimiter: ConcurrencyLimiter + signal?: AbortSignal + onFileProcessed: () => void + onFolderProcessed: () => void + }): Promise { + const { folderPath, llmModel, concurrencyLimiter, signal, onFileProcessed, onFolderProcessed } = params + + const folder = this.app.vault.getAbstractFileByPath(folderPath) + if (!folder || !(folder instanceof TFolder)) { + return null + } + + // 获取文件夹直接子级的文件和文件夹 + const directFiles = this.app.vault.getMarkdownFiles().filter(file => { + const fileDirPath = file.path.substring(0, file.path.lastIndexOf('/')) + return fileDirPath === folderPath + }) + + const directSubfolders = folder.children.filter((child): child is TFolder => child instanceof TFolder) + + if (directFiles.length === 0 && directSubfolders.length === 0) { + return null // 空文件夹 + } + + const childSummaries: string[] = [] + + // 并行处理直接子文件 + if (directFiles.length > 0) { + const filePromises = directFiles.map(file => + concurrencyLimiter.execute(async () => { + if (signal?.aborted) { + throw new Error('Operation was aborted') + } + + const summary = await this.processSingleFile(file.path, llmModel) + if (summary) { + onFileProcessed() + return `**${file.name}**: ${summary}` + } + return null + }) + ) + + const fileResults = await Promise.all(filePromises) + const validFileResults = fileResults.filter((result): result is string => result !== null) + childSummaries.push(...validFileResults) + } + + // 并行处理直接子文件夹 + if (directSubfolders.length > 0) { + const folderPromises = directSubfolders.map(subfolder => + concurrencyLimiter.execute(async () => { + if (signal?.aborted) { + throw new Error('Operation was aborted') + } + + const summary = await this.processFolderHierarchically({ + folderPath: subfolder.path, + llmModel, + concurrencyLimiter, + signal, + onFileProcessed, + onFolderProcessed + }) + if (summary) { + onFolderProcessed() + return `**${subfolder.name}/**: ${summary}` + } + return null + }) + ) + + const folderResults = await Promise.all(folderPromises) + const validFolderResults = folderResults.filter((result): result is string => result !== null) + childSummaries.push(...validFolderResults) + } + + if (childSummaries.length === 0) { + return null + } + + // 生成当前文件夹的摘要 + const combinedContent = childSummaries.join('\n\n') + const folderSummary = await this.generateHierarchicalSummary( + combinedContent, + `Folder: ${folderPath}`, + llmModel + ) + + // 保存文件夹摘要到数据库 + await this.saveFolderSummaryToDatabase(folderSummary, folderPath) + + return folderSummary + } + + /** + * 处理单个文件 + */ + private async processSingleFile(filePath: string, llmModel: LLMModel): Promise { + try { + // 检查缓存 + const fileMetadata = await this.getFileMetadata(filePath) + if (!fileMetadata.success) { + console.warn(`无法获取文件元数据: ${filePath}`) + return null + } + + const cacheResult = await this.checkDatabaseCache( + fileMetadata.sourcePath, + fileMetadata.sourceMtime, + TransformationType.CONCISE_DENSE_SUMMARY + ) + + if (cacheResult.foundCache && cacheResult.result.success && cacheResult.result.result) { + return cacheResult.result.result + } + + // 获取文件内容 + const contentResult = await this.getFileContent(filePath) + if (!contentResult.success) { + console.warn(`无法读取文件内容: ${filePath}`) + return null + } + + // 验证内容 + const contentValidation = DocumentProcessor.validateContent(contentResult.fileContent) + if (contentValidation.isErr()) { + console.warn(`文件内容无效: ${filePath}`) + return null + } + + // 处理文档内容 + const processedDocument = await DocumentProcessor.processContent( + contentResult.fileContent, + DocumentProcessor['DEFAULT_MAX_TOKENS'] + ) + + // 生成摘要 + const summary = await this.generateConciseDenseSummary( + processedDocument.processedContent, + llmModel + ) + + // 保存到数据库 + await this.saveResultToDatabase( + summary, + TransformationType.CONCISE_DENSE_SUMMARY, + fileMetadata.sourcePath, + fileMetadata.sourceMtime, + 'document' + ) + + return summary + + } catch (error) { + console.warn(`处理文件失败: ${filePath}`, error) + return null + } + } + + /** + * 生成简洁密集摘要 + */ + private async generateConciseDenseSummary(content: string, llmModel: LLMModel): Promise { + const client = new TransformationLLMClient(this.llmManager, llmModel) + const messages: RequestMessage[] = [ + { + role: 'system', + content: CONCISE_DENSE_SUMMARY_PROMPT + }, + { + role: 'user', + content: content + } + ] + + const result = await client.queryChatModel(messages) + if (result.isErr()) { + throw new Error(`生成摘要失败: ${result.error.message}`) + } + + return this.postProcessResult(result.value, TransformationType.CONCISE_DENSE_SUMMARY) + } + + /** + * 生成分层摘要 + */ + private async generateHierarchicalSummary( + combinedContent: string, + contextLabel: string, + llmModel: LLMModel + ): Promise { + const client = new TransformationLLMClient(this.llmManager, llmModel) + const messages: RequestMessage[] = [ + { + role: 'system', + content: HIERARCHICAL_SUMMARY_PROMPT + }, + { + role: 'user', + content: `${contextLabel}\n\n${combinedContent}` + } + ] + + const result = await client.queryChatModel(messages) + if (result.isErr()) { + throw new Error(`生成分层摘要失败: ${result.error.message}`) + } + + return this.postProcessResult(result.value, TransformationType.HIERARCHICAL_SUMMARY) + } + + /** + * 保存文件夹摘要到数据库 + */ + private async saveFolderSummaryToDatabase(summary: string, folderPath: string): Promise { + if (!this.embeddingModel || !this.insightManager) { + return + } + + try { + const embedding = await this.embeddingModel.getEmbedding(summary) + await this.insightManager.storeInsight( + { + insightType: TransformationType.HIERARCHICAL_SUMMARY, + insight: summary, + sourceType: 'folder', + sourcePath: folderPath, + sourceMtime: Date.now(), + embedding: embedding, + }, + this.embeddingModel + ) + console.log(`文件夹摘要已保存到数据库: ${folderPath}`) + } catch (error) { + console.warn('保存文件夹摘要到数据库失败:', error) + } + } + + /** + * 删除工作区的所有转换 + * + * @param workspace 工作区对象,如果为 null 则删除默认 vault 工作区的转换 + * @returns 删除操作的结果 + */ + async deleteWorkspaceTransformations( + workspace: import('../../database/json/workspace/types').Workspace | null = null + ): Promise<{ + success: boolean; + deletedCount: number; + error?: string; + }> { + if (!this.embeddingModel || !this.insightManager) { + return { + success: false, + deletedCount: 0, + error: '缺少必要的组件:嵌入模型或洞察管理器' + } + } + + try { + const sourcePaths: string[] = [] + let workspaceName: string + + if (workspace) { + workspaceName = workspace.name + + // 添加工作区本身的洞察路径 + sourcePaths.push(`workspace:${workspaceName}`) + + // 解析工作区内容并收集所有相关路径 + for (const contentItem of workspace.content) { + if (contentItem.type === 'folder') { + const folderPath = contentItem.content + + // 添加文件夹路径本身 + sourcePaths.push(folderPath) + + // 获取文件夹下的所有文件 + const files = this.app.vault.getMarkdownFiles().filter(file => + file.path.startsWith(folderPath === '/' ? '' : folderPath + '/') + ) + + // 添加所有文件路径 + files.forEach(file => { + sourcePaths.push(file.path) + }) + + // 添加中间文件夹路径 + files.forEach(file => { + const dirPath = file.path.substring(0, file.path.lastIndexOf('/')) + if (dirPath && dirPath !== folderPath) { + let currentPath = folderPath === '/' ? '' : folderPath + const pathParts = dirPath.substring(currentPath.length).split('/').filter(Boolean) + + for (let i = 0; i < pathParts.length; i++) { + currentPath += (currentPath ? '/' : '') + pathParts[i] + if (!sourcePaths.includes(currentPath)) { + sourcePaths.push(currentPath) + } + } + } + }) + + } else if (contentItem.type === 'tag') { + // 获取标签对应的所有文件 + const tagFiles = this.getFilesByTag(contentItem.content) + + tagFiles.forEach(file => { + sourcePaths.push(file.path) + + // 添加文件所在的文件夹路径 + const dirPath = file.path.substring(0, file.path.lastIndexOf('/')) + if (dirPath) { + const pathParts = dirPath.split('/').filter(Boolean) + let currentPath = '' + + for (let i = 0; i < pathParts.length; i++) { + currentPath += (currentPath ? '/' : '') + pathParts[i] + if (!sourcePaths.includes(currentPath)) { + sourcePaths.push(currentPath) + } + } + } + }) + } + } + } else { + // 处理默认 vault 工作区 - 删除所有洞察 + workspaceName = 'vault' + sourcePaths.push(`workspace:${workspaceName}`) + + // 获取所有洞察来确定删除数量 + const allInsights = await this.insightManager.getAllInsights(this.embeddingModel) + + // 对于 vault 工作区,删除所有洞察 + await this.insightManager.clearAllInsights(this.embeddingModel) + + console.log(`已删除 vault 工作区的所有 ${allInsights.length} 个转换`) + + return { + success: true, + deletedCount: allInsights.length + } + } + + // 去重路径 + const uniquePaths = [...new Set(sourcePaths)] + + // 获取将要删除的洞察数量 + const existingInsights = await this.insightManager.getAllInsights(this.embeddingModel) + const insightsToDelete = existingInsights.filter(insight => + uniquePaths.includes(insight.source_path) + ) + const deletedCount = insightsToDelete.length + + // 批量删除洞察 + if (uniquePaths.length > 0) { + await this.insightManager.deleteInsightsBySourcePaths(uniquePaths, this.embeddingModel) + console.log(`已删除工作区 "${workspaceName}" 的 ${deletedCount} 个转换,涉及 ${uniquePaths.length} 个路径`) + } + + return { + success: true, + deletedCount: deletedCount + } + + } catch (error) { + console.error('删除工作区转换失败:', error) + return { + success: false, + deletedCount: 0, + error: `删除工作区转换失败: ${error instanceof Error ? error.message : String(error)}` + } + } + } + + /** + * 删除指定工作区名称的所有转换(便捷方法) + * + * @param workspaceName 工作区名称 + * @returns 删除操作的结果 + */ + async deleteWorkspaceTransformationsByName(workspaceName: string): Promise<{ + success: boolean; + deletedCount: number; + error?: string; + }> { + if (!this.embeddingModel || !this.insightManager) { + return { + success: false, + deletedCount: 0, + error: '缺少必要的组件:嵌入模型或洞察管理器' + } + } + + try { + // 删除工作区本身的洞察 + const workspaceInsightPath = `workspace:${workspaceName}` + + // 获取所有洞察并筛选出该工作区相关的 + const allInsights = await this.insightManager.getAllInsights(this.embeddingModel) + const workspaceInsights = allInsights.filter(insight => + insight.source_path === workspaceInsightPath + ) + + if (workspaceInsights.length > 0) { + await this.insightManager.deleteInsightsBySourcePath(workspaceInsightPath, this.embeddingModel) + console.log(`已删除工作区 "${workspaceName}" 的 ${workspaceInsights.length} 个转换`) + } + + return { + success: true, + deletedCount: workspaceInsights.length + } + + } catch (error) { + console.error('删除工作区转换失败:', error) + return { + success: false, + deletedCount: 0, + error: `删除工作区转换失败: ${error instanceof Error ? error.message : String(error)}` + } + } + } + + /** + * 删除单个洞察 + * + * @param insightId 洞察ID + * @returns 删除操作的结果 + */ + async deleteSingleInsight(insightId: number): Promise<{ + success: boolean; + error?: string; + }> { + if (!this.embeddingModel || !this.insightManager) { + return { + success: false, + error: '缺少必要的组件:嵌入模型或洞察管理器' + } + } + + try { + // 直接按ID删除洞察 + await this.insightManager.deleteInsightById(insightId, this.embeddingModel) + + console.log(`已删除洞察 ID: ${insightId}`) + + return { + success: true + } + + } catch (error) { + console.error('删除单个洞察失败:', error) + return { + success: false, + error: `删除单个洞察失败: ${error instanceof Error ? error.message : String(error)}` + } + } + } +} diff --git a/src/database/modules/insight/insight-manager.ts b/src/database/modules/insight/insight-manager.ts index c938563..24914b7 100644 --- a/src/database/modules/insight/insight-manager.ts +++ b/src/database/modules/insight/insight-manager.ts @@ -207,6 +207,16 @@ export class InsightManager { await this.repository.clearAllInsights(embeddingModel) } + /** + * 删除指定ID的洞察 + */ + async deleteInsightById( + id: number, + embeddingModel: EmbeddingModel, + ): Promise { + await this.repository.deleteInsightById(id, embeddingModel) + } + /** * 文件删除时清理相关洞察 */ diff --git a/src/database/modules/insight/insight-repository.ts b/src/database/modules/insight/insight-repository.ts index 5045cfa..263edab 100644 --- a/src/database/modules/insight/insight-repository.ts +++ b/src/database/modules/insight/insight-repository.ts @@ -29,7 +29,8 @@ export class InsightRepository { const tableName = this.getTableName(embeddingModel) const result = await this.db.query( `SELECT * FROM "${tableName}" ORDER BY created_at DESC` - ) + ) + console.log(result.rows) return result.rows } @@ -128,6 +129,20 @@ export class InsightRepository { await this.db.query(`DELETE FROM "${tableName}"`) } + async deleteInsightById( + id: number, + embeddingModel: EmbeddingModel, + ): Promise { + if (!this.db) { + throw new DatabaseNotInitializedException() + } + const tableName = this.getTableName(embeddingModel) + await this.db.query( + `DELETE FROM "${tableName}" WHERE id = $1`, + [id] + ) + } + async insertInsights( data: InsertSourceInsight[], embeddingModel: EmbeddingModel, diff --git a/src/utils/api.ts b/src/utils/api.ts index 48b3c3b..904593c 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -903,6 +903,10 @@ export const qwenModels = { }, } as const satisfies Record export const qwenEmbeddingModels = { + "text-embedding-v4": { + dimensions: 1024, + description: "支持50+主流语种,包括中文、英语、西班牙语、法语、葡萄牙语、印尼语、日语、韩语、德语、俄罗斯语等。最大行数20,单行最大处理8,192 Token。支持可选维度:1,024(默认)、768或512。单价:0.0007元/千Token。免费额度:50万Token(有效期180天)。" + }, "text-embedding-v3": { dimensions: 1024, description: "支持50+主流语种,包括中文、英语、西班牙语、法语、葡萄牙语、印尼语、日语、韩语、德语、俄罗斯语等。最大行数20,单行最大处理8,192 Token。支持可选维度:1,024(默认)、768或512。单价:0.0007元/千Token。免费额度:50万Token(有效期180天)。" diff --git a/src/utils/glob-utils.ts b/src/utils/glob-utils.ts index d75ed60..3159df5 100644 --- a/src/utils/glob-utils.ts +++ b/src/utils/glob-utils.ts @@ -1,8 +1,12 @@ import { minimatch } from 'minimatch' import { App, TFile, TFolder, Vault } from 'obsidian' +import { RAGEngine } from '../core/rag/rag-engine' +import { TRANSFORMATIONS, TransEngine } from '../core/transformations/trans-engine' import { Workspace } from '../database/json/workspace/types' +import { addLineNumbers } from './prompt-generator' + export const findFilesMatchingPatterns = async ( patterns: string[], vault: Vault, @@ -226,3 +230,115 @@ export const matchSearchFiles = async (vault: Vault, path: string, query: string export const regexSearchFiles = async (vault: Vault, path: string, regex: string, file_pattern: string) => { } + +/** + * 语义搜索文件(同时查询原始笔记和抽象洞察) + */ +export const semanticSearchFiles = async ( + ragEngine: RAGEngine, // RAG 引擎实例 - 原始笔记数据库 + query: string, + path?: string, + workspace?: Workspace, + app?: App, + transEngine?: TransEngine // Trans 引擎实例 - 抽象洞察数据库 +): Promise => { + let scope: { files: string[], folders: string[] } | undefined + + // 如果指定了路径,使用该路径 + if (path && path !== '' && path !== '.' && path !== '/') { + scope = { files: [], folders: [path] } + } + // 如果没有指定路径但有工作区,使用工作区范围 + else if (workspace && app) { + 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, app) + files.push(...tagFiles) + } + } + + // 只有当有文件夹或文件时才设置 scope + if (folders.length > 0 || files.length > 0) { + scope = { files, folders } + } + } + + const resultSections: string[] = [] + + // 1. 查询原始笔记数据库 (RAGEngine) + try { + const ragResults = await ragEngine.processQuery({ + query: query, + scope: scope, + }) + + if (ragResults.length > 0) { + resultSections.push('## 📝 原始笔记内容') + const ragSnippets = ragResults.map(({ path, content, metadata }: any) => { + const contentWithLineNumbers = addLineNumbers(content, metadata.startLine) + return `\n${contentWithLineNumbers}\n` + }).join('\n\n') + resultSections.push(ragSnippets) + } + } catch (error) { + console.warn('RAG 搜索失败:', error) + resultSections.push('## 📝 原始笔记内容\n⚠️ 原始笔记搜索失败') + } + + // 2. 查询抽象洞察数据库 (TransEngine) - 使用新的 processQuery 接口 + if (transEngine) { + try { + const insightResults = await transEngine.processQuery({ + query: query, + scope: scope, + limit: 20, + minSimilarity: 0.3, + }) + + if (insightResults.length > 0) { + resultSections.push('\n## 🧠 AI 抽象洞察') + + // 按转换类型分组 + const groupedInsights: { [key: string]: any[] } = {} + insightResults.forEach(insight => { + if (!groupedInsights[insight.insight_type]) { + groupedInsights[insight.insight_type] = [] + } + groupedInsights[insight.insight_type].push(insight) + }) + + // 渲染每种类型的洞察 + for (const [insightType, insights] of Object.entries(groupedInsights)) { + const transformationConfig = TRANSFORMATIONS[insightType as keyof typeof TRANSFORMATIONS] + const typeName = transformationConfig ? transformationConfig.description : insightType + + resultSections.push(`\n### ${typeName}`) + + insights.forEach((insight, index) => { + const similarity = (insight.similarity * 100).toFixed(1) + resultSections.push( + `\n${insight.insight}\n` + ) + }) + } + } + } catch (error) { + console.warn('TransEngine 搜索失败:', error) + resultSections.push('\n## 🧠 AI 抽象洞察\n⚠️ 洞察搜索失败: ' + (error instanceof Error ? error.message : String(error))) + } + } + + // 3. 合并结果 + if (resultSections.length === 0) { + return `No results found for '${query}'` + } + + return resultSections.join('\n\n') +}