diff --git a/src/components/chat-view/ChatView.tsx b/src/components/chat-view/ChatView.tsx index 9bc78f9..7251af3 100644 --- a/src/components/chat-view/ChatView.tsx +++ b/src/components/chat-view/ChatView.tsx @@ -1286,7 +1286,7 @@ const Chat = forwardRef((props, ref) => { {/* header view */}
- {t('workspace.shortTitle')}: +
+
-
- {t('insights.progress.current', { item: initProgress.currentItem })} +
+
+ {initProgress.currentItem} +
+
+ {initProgress.percentage !== undefined ? initProgress.percentage : Math.round((initProgress.current / Math.max(initProgress.total, 1)) * 100)}% +
+
+ {/* 进度日志 */} +
+
+ 阶段: + {initProgress.stage} +
+
+ 进度: + {initProgress.current} / {initProgress.total} +
+
+ 当前: + {initProgress.currentItem} +
)}
)} + {/* 初始化成功消息 */} + {initSuccess.show && initSuccess.result && ( +
+
+ +
+ + {t('insights.success.workspaceInitialized', { name: initSuccess.workspaceName })} + +
+ +
+
+ )} + {/* 确认删除对话框 */} {showDeleteConfirm && (
@@ -616,6 +708,53 @@ const InsightView = () => {
)} + {/* 确认初始化/更新对话框 */} + {showInitConfirm && ( +
+
+
+

{hasLoaded && insightResults.length > 0 ? t('insights.initConfirm.updateTitle') : t('insights.initConfirm.initTitle')}

+
+
+

+ {hasLoaded && insightResults.length > 0 ? t('insights.initConfirm.updateMessage') : t('insights.initConfirm.initMessage')} +

+
+
+ {t('insights.initConfirm.modelLabel')} + + {settings.chatModelProvider} / {settings.chatModelId || t('insights.initConfirm.defaultModel')} + +
+
+ {t('insights.initConfirm.workspaceLabel')} + + {settings.workspace === 'vault' ? t('workspace.entireVault') : settings.workspace} + +
+
+

+ {hasLoaded && insightResults.length > 0 ? t('insights.initConfirm.updateWarning') : t('insights.initConfirm.initWarning')} +

+
+
+ + +
+
+
+ )} + {/* 洞察结果 */}
{!isLoading && insightGroupedResults.length > 0 && ( @@ -812,23 +951,93 @@ const InsightView = () => { } .obsidian-insight-stats { + background-color: var(--background-secondary); + border: 1px solid var(--background-modifier-border); + border-radius: var(--radius-m); + padding: 12px; + } + + .obsidian-insight-stats-overview { + display: flex; + align-items: center; + justify-content: space-between; + } + + .obsidian-insight-stats-main { + display: flex; + align-items: baseline; + gap: 6px; + } + + .obsidian-insight-stats-number { + font-size: var(--font-ui-large); + font-weight: 700; + color: var(--text-accent); + font-family: var(--font-monospace); + } + + .obsidian-insight-stats-label { + font-size: var(--font-ui-medium); + color: var(--text-normal); + font-weight: 500; + } + + .obsidian-insight-stats-breakdown { + flex: 1; + display: flex; + justify-content: flex-end; + } + + .obsidian-insight-stats-items { + display: flex; + gap: 12px; + } + + .obsidian-insight-stats-item { + display: flex; + align-items: center; + gap: 4px; + padding: 4px 8px; + background-color: var(--background-modifier-border); + border-radius: var(--radius-s); + } + + .obsidian-insight-stats-item-icon { + font-size: 12px; + line-height: 1; + } + + .obsidian-insight-stats-item-value { font-size: var(--font-ui-small); + font-weight: 600; + color: var(--text-normal); + font-family: var(--font-monospace); + } + + .obsidian-insight-stats-item-label { + font-size: var(--font-ui-smaller); color: var(--text-muted); } - .obsidian-insight-stats-line { - margin-bottom: 2px; - } - - .obsidian-insight-breakdown { - color: var(--text-faint); - font-size: var(--font-ui-smaller); - } - .obsidian-insight-scope { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 8px; + background-color: var(--background-modifier-border-hover); + border-radius: var(--radius-s); + } + + .obsidian-insight-scope-label { + font-size: var(--font-ui-smaller); + color: var(--text-muted); + font-weight: 500; + } + + .obsidian-insight-scope-value { font-size: var(--font-ui-smaller); color: var(--text-accent); - font-weight: 500; + font-weight: 600; } .obsidian-insight-loading { @@ -906,12 +1115,137 @@ const InsightView = () => { transition: width 0.3s ease; } + .obsidian-insight-progress-details { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; + } + .obsidian-insight-progress-item { - color: var(--text-muted); + color: var(--text-normal); + font-size: var(--font-ui-small); + font-weight: 500; + flex: 1; + margin-right: 12px; + } + + .obsidian-insight-progress-percentage { + color: var(--text-accent); + font-size: var(--font-ui-small); + font-weight: 600; + font-family: var(--font-monospace); + flex-shrink: 0; + } + + .obsidian-insight-progress-log { + margin-top: 8px; + padding: 8px; + background-color: var(--background-modifier-border-hover); + border-radius: var(--radius-s); font-size: var(--font-ui-smaller); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + } + + .obsidian-insight-progress-log-item { + display: flex; + justify-content: space-between; + margin-bottom: 4px; + } + + .obsidian-insight-progress-log-item:last-child { + margin-bottom: 0; + } + + .obsidian-insight-progress-log-label { + color: var(--text-muted); + font-weight: 500; + flex-shrink: 0; + margin-right: 8px; + } + + .obsidian-insight-progress-log-value { + color: var(--text-normal); + font-family: var(--font-monospace); + text-align: right; + flex: 1; + word-break: break-all; + } + + .obsidian-insight-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-insight-success-content { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 16px; + } + + .obsidian-insight-success-icon { + font-size: 16px; + line-height: 1; + color: var(--color-green, #28a745); + flex-shrink: 0; + } + + .obsidian-insight-success-text { + display: flex; + flex-direction: column; + gap: 2px; + flex: 1; + min-width: 0; + } + + .obsidian-insight-success-title { + font-size: var(--font-ui-medium); + font-weight: 600; + color: var(--text-normal); + line-height: 1.3; + } + + .obsidian-insight-success-summary { + font-size: var(--font-ui-small); + color: var(--text-muted); + line-height: 1.3; + } + + .obsidian-insight-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-insight-success-close:hover { + background-color: var(--background-modifier-hover); + color: var(--text-normal); + } + + @keyframes slideInFromTop { + 0% { + transform: translateY(-100%); + opacity: 0; + } + 100% { + transform: translateY(0); + opacity: 1; + } } .obsidian-insight-results { @@ -1220,6 +1554,42 @@ const InsightView = () => { 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); diff --git a/src/components/chat-view/WorkspaceSelect.tsx b/src/components/chat-view/WorkspaceSelect.tsx index a50c543..39259e0 100644 --- a/src/components/chat-view/WorkspaceSelect.tsx +++ b/src/components/chat-view/WorkspaceSelect.tsx @@ -174,7 +174,7 @@ const WorkspaceSelect = () => { background-color: var(--background-modifier-hover); box-shadow: none; border: none; - padding: var(--size-4-1) var(--size-4-1); + padding: var(--size-4-1) var(--size-4-3); font-size: var(--font-small); font-weight: var(--font-medium); color: var(--text-muted); diff --git a/src/core/transformations/trans-engine.ts b/src/core/transformations/trans-engine.ts index 3d48ccd..8ceae41 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, TFolder, getLanguage } from 'obsidian'; +import { App, TFolder, getLanguage, normalizePath } from 'obsidian'; import { DBManager } from '../../database/database-manager'; import { InsightManager } from '../../database/modules/insight/insight-manager'; @@ -24,7 +24,7 @@ import { getEmbeddingModel } from '../rag/embedding'; type EmbeddingManager = { modelLoaded: boolean currentModel: string | null - loadModel(modelId: string, useGpu: boolean): Promise + loadModel(modelId: string, useGpu: boolean): Promise embed(text: string): Promise<{ vec: number[] }> embedBatch(texts: string[]): Promise<{ vec: number[] }[]> } @@ -141,19 +141,12 @@ export const TRANSFORMATIONS: Record = // 转换参数接口 export interface TransformationParams { - filePath: string; // 文件路径、文件夹路径或工作区标识 - contentType?: 'document' | 'tag' | 'folder' | 'workspace'; + filePath: string; // 文件路径、文件夹路径 + contentType?: 'document' | 'tag' | 'folder'; transformationType: TransformationType; model?: LLMModel; maxContentTokens?: number; saveToDatabase?: boolean; - // 对于 workspace 类型,可以传入额外的元数据 - workspaceMetadata?: { - name: string; - description?: string; - // 完整的 workspace 对象,用于获取配置信息 - workspace?: import('../../database/json/workspace/types').Workspace; - }; } // 转换结果接口 @@ -166,6 +159,33 @@ export interface TransformationResult { processedTokens?: number; } +// 工作区洞察初始化进度接口 +export interface WorkspaceInsightProgress { + stage: string; + current: number; + total: number; + currentItem: string; + percentage: number; +} + +// 工作区洞察初始化参数接口 +export interface InitWorkspaceInsightParams { + workspace: import('../../database/json/workspace/types').Workspace; + model?: LLMModel; + onProgress?: (progress: WorkspaceInsightProgress) => void; +} + +// 工作区洞察初始化结果接口 +export interface InitWorkspaceInsightResult { + success: boolean; + error?: string; + processedFiles: number; + processedFolders: number; + totalItems: number; + skippedItems: number; + insightId?: number; +} + /** * LLM 客户端类,用于与语言模型交互 */ @@ -371,7 +391,7 @@ export class TransEngine { error: string; } > { - const targetFile = this.app.vault.getFileByPath(filePath); + const targetFile = this.app.vault.getFileByPath(normalizePath(filePath)); if (!targetFile) { return { success: false, @@ -472,7 +492,7 @@ export class TransEngine { error: string; } > { - const targetFile = this.app.vault.getFileByPath(filePath); + const targetFile = this.app.vault.getFileByPath(normalizePath(filePath)); if (!targetFile) { return { success: false, @@ -548,8 +568,7 @@ export class TransEngine { transformationType, model, maxContentTokens, - saveToDatabase = false, - workspaceMetadata + saveToDatabase = false } = params; try { @@ -596,7 +615,16 @@ export class TransEngine { case 'folder': { sourcePath = filePath; - sourceMtime = Date.now(); + + // 计算文件夹的真实 mtime(基于所有子项目的最大 mtime) + const folderItems = await this.collectFolderItems(filePath); + let maxMtime = 0; + for (const item of folderItems) { + if (item.mtime > maxMtime) { + maxMtime = item.mtime; + } + } + sourceMtime = maxMtime > 0 ? maxMtime : 0; // 检查数据库缓存 const cacheCheckResult = await this.checkDatabaseCache( @@ -620,44 +648,6 @@ export class TransEngine { 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, @@ -733,7 +723,7 @@ export class TransEngine { transformationType, sourcePath, sourceMtime, - contentType === 'workspace' ? 'folder' : contentType // workspace 在数据库中存储为 folder 类型 + contentType ); })(); // 立即执行异步函数,但不等待其完成 } @@ -763,7 +753,7 @@ export class TransEngine { error?: string; }> { try { - const folder = this.app.vault.getAbstractFileByPath(folderPath); + const folder = this.app.vault.getAbstractFileByPath(normalizePath(folderPath)); if (!folder || !(folder instanceof TFolder)) { return { success: false, @@ -852,119 +842,6 @@ export class TransEngine { } } - /** - * 处理工作区内容 - 根据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)}` - } - } - } - /** * 后处理转换结果 */ @@ -1072,7 +949,7 @@ export class TransEngine { // 添加文件夹下的所有文件 if (scope.folders.length > 0) { for (const folderPath of scope.folders) { - const folder = this.app.vault.getAbstractFileByPath(folderPath) + const folder = this.app.vault.getAbstractFileByPath(normalizePath(folderPath)) if (folder && folder instanceof TFolder) { // 获取文件夹下的所有 Markdown 文件 const folderFiles = this.app.vault.getMarkdownFiles().filter(file => @@ -1167,7 +1044,7 @@ export class TransEngine { }): Promise { const { folderPath, llmModel, concurrencyLimiter, signal, onFileProcessed, onFolderProcessed } = params - const folder = this.app.vault.getAbstractFileByPath(folderPath) + const folder = this.app.vault.getAbstractFileByPath(normalizePath(folderPath)) if (!folder || !(folder instanceof TFolder)) { return null } @@ -1381,6 +1258,16 @@ export class TransEngine { } try { + // 获取文件夹的真实 mtime(基于所有子项目的最大 mtime) + const folderItems = await this.collectFolderItems(folderPath) + let maxMtime = 0 + for (const item of folderItems) { + if (item.mtime > maxMtime) { + maxMtime = item.mtime + } + } + const sourceMtime = maxMtime > 0 ? maxMtime : 0 + const embedding = await this.embeddingModel.getEmbedding(summary) await this.insightManager.storeInsight( { @@ -1388,7 +1275,7 @@ export class TransEngine { insight: summary, sourceType: 'folder', sourcePath: folderPath, - sourceMtime: Date.now(), + sourceMtime: sourceMtime, embedding: embedding, }, this.embeddingModel @@ -1621,4 +1508,480 @@ export class TransEngine { } } } + + /** + * 初始化工作区洞察 - 专门用于工作区洞察的初始化流程 + */ + async initWorkspaceInsight(params: InitWorkspaceInsightParams): Promise { + const { workspace, model, onProgress } = params; + + // 统计信息 + let processedFiles = 0; + let processedFolders = 0; + let skippedItems = 0; + + try { + // 1. 深度分析工作区内容,统计所有需要处理的项目 + onProgress?.({ + stage: '分析工作区内容', + current: 0, + total: 1, + currentItem: '深度扫描文件和文件夹...', + percentage: 0 + }); + + // 收集所有需要处理的项目(深度递归) + const allItems: Array<{ + type: 'file' | 'folder'; + path: string; + name: string; + mtime: number; + }> = []; + + // 收集工作区顶层配置的项目(仅用于最终摘要) + const topLevelFiles: Array<{ + path: string; + name: string; + }> = []; + + const topLevelFolders: Array<{ + path: string; + name: string; + }> = []; + + // 解析 workspace 的 content 配置 + const seenPaths = new Set(); + + for (const contentItem of workspace.content) { + if (contentItem.type === 'folder') { + const folderPath = contentItem.content; + const folderName = folderPath.split('/').pop() || folderPath; + + // 收集顶层文件夹(用于最终摘要) + topLevelFolders.push({ + path: folderPath, + name: folderName + }); + + // 深度遍历收集所有项目(用于进度统计和处理) + const items = await this.collectFolderItems(folderPath); + for (const item of items) { + if (!seenPaths.has(item.path)) { + seenPaths.add(item.path); + allItems.push(item); + } + } + } else if (contentItem.type === 'tag') { + // 收集标签对应的文件 + const taggedFiles = this.getFilesByTag(contentItem.content); + for (const file of taggedFiles) { + if (!seenPaths.has(file.path)) { + seenPaths.add(file.path); + // 添加到顶层文件(用于最终摘要) + topLevelFiles.push({ + path: file.path, + name: file.name + }); + // 添加到所有项目(用于处理) + allItems.push({ + type: 'file', + path: file.path, + name: file.name, + mtime: file.stat.mtime + }); + } + } + } + } + console.log('allItems', allItems); + if (allItems.length === 0) { + return { + success: false, + error: `工作区 "${workspace.name}" 没有找到任何内容`, + processedFiles: 0, + processedFolders: 0, + totalItems: 0, + skippedItems: 0 + }; + } + + // 分离文件和文件夹 + const files = allItems.filter(item => item.type === 'file'); + const folders = allItems.filter(item => item.type === 'folder'); + const totalItems = allItems.length; + + onProgress?.({ + stage: '分析完成', + current: 1, + total: 1, + currentItem: `深度扫描完成:${files.length} 个文件,${folders.length} 个文件夹`, + percentage: 5 + }); + + // 用于收集顶层摘要(仅用于工作区摘要) + const topLevelSummaries: string[] = []; + let currentProgress = 0; + + // 2. 处理所有文件(深度递归的结果) + for (const file of files) { + currentProgress++; + + onProgress?.({ + stage: '处理文件', + current: currentProgress, + total: totalItems, + currentItem: `📄 ${file.name}`, + percentage: Math.round((currentProgress / totalItems) * 90) + 5 // 5-95% + }); + + try { + const fileResult = await this.runTransformation({ + filePath: file.path, + contentType: 'document', + transformationType: TransformationType.DENSE_SUMMARY, + model: model, + saveToDatabase: true + }); + + if (fileResult.success && fileResult.result) { + // 检查是否是顶层文件(标签文件),如果是则添加到顶层摘要 + const isTopLevelFile = topLevelFiles.some(f => f.path === file.path); + if (isTopLevelFile) { + topLevelSummaries.push(`### 📄 ${file.name}\n${fileResult.result}`); + } + processedFiles++; + } else { + console.warn(`处理文件失败: ${file.path}`, fileResult.error); + const isTopLevelFile = topLevelFiles.some(f => f.path === file.path); + if (isTopLevelFile) { + topLevelSummaries.push(`### 📄 ${file.name}\n*处理失败: ${fileResult.error}*`); + } + skippedItems++; + } + } catch (error) { + console.error(`文件处理异常: ${file.path}`, error); + const isTopLevelFile = topLevelFiles.some(f => f.path === file.path); + if (isTopLevelFile) { + topLevelSummaries.push(`### 📄 ${file.name}\n*处理异常: ${error instanceof Error ? error.message : String(error)}*`); + } + skippedItems++; + } + } + + // 3. 处理所有文件夹(深度递归的结果,从最深层开始) + const sortedFolders = folders.sort((a, b) => { + const depthA = a.path.split('/').length; + const depthB = b.path.split('/').length; + return depthB - depthA; // 深度大的先处理 + }); + + for (const folder of sortedFolders) { + currentProgress++; + + onProgress?.({ + stage: '处理文件夹', + current: currentProgress, + total: totalItems, + currentItem: `📂 ${folder.name}`, + percentage: Math.round((currentProgress / totalItems) * 90) + 5 // 5-95% + }); + + try { + const folderResult = await this.runTransformation({ + filePath: folder.path, + contentType: 'folder', + transformationType: TransformationType.HIERARCHICAL_SUMMARY, + model: model, + saveToDatabase: true + }); + + if (folderResult.success && folderResult.result) { + // 检查是否是顶层文件夹,如果是则添加到顶层摘要 + const isTopLevelFolder = topLevelFolders.some(f => f.path === folder.path); + if (isTopLevelFolder) { + topLevelSummaries.push(`### 📂 ${folder.name}/\n${folderResult.result}`); + } + processedFolders++; + } else { + console.warn(`处理文件夹失败: ${folder.path}`, folderResult.error); + const isTopLevelFolder = topLevelFolders.some(f => f.path === folder.path); + if (isTopLevelFolder) { + topLevelSummaries.push(`### 📂 ${folder.name}/\n*处理失败: ${folderResult.error}*`); + } + skippedItems++; + } + } catch (error) { + console.error(`文件夹处理异常: ${folder.path}`, error); + const isTopLevelFolder = topLevelFolders.some(f => f.path === folder.path); + if (isTopLevelFolder) { + topLevelSummaries.push(`### 📂 ${folder.name}/\n*处理异常: ${error instanceof Error ? error.message : String(error)}*`); + } + skippedItems++; + } + } + + // 4. 生成工作区整体洞察 + onProgress?.({ + stage: '生成工作区洞察', + current: 1, + total: 1, + currentItem: '汇总分析工作区内容...', + percentage: 95 + }); + + // 构建工作区内容描述 + let workspaceContent = `# Workspace: ${workspace.name}\n\n`; + + // 只添加顶层摘要(避免重叠) + if (topLevelSummaries.length > 0) { + workspaceContent += topLevelSummaries.join('\n\n'); + } else { + workspaceContent += '*No top-level content summaries available.*'; + } + + // 5. 生成工作区的整体洞察 + const sourcePath = `workspace:${workspace.name}`; + + // 计算所有项目的最大 mtime + let maxMtime = 0; + for (const item of allItems) { + if (item.mtime > maxMtime) { + maxMtime = item.mtime; + } + } + console.log('maxMtime', maxMtime); + + // 如果没有找到任何有效的 mtime,使用当前时间 + const sourceMtime = maxMtime > 0 ? maxMtime : 0; + + // 验证内容 + const contentValidation = DocumentProcessor.validateContent(workspaceContent); + if (contentValidation.isErr()) { + return { + success: false, + error: `工作区内容验证失败: ${contentValidation.error.message}`, + processedFiles, + processedFolders, + totalItems, + skippedItems + }; + } + + // 处理文档内容(检查 token 数量并截断) + const processedDocument = await DocumentProcessor.processContent(workspaceContent); + + // 查询数据库中是否存在工作区洞察 + const cacheCheckResult = await this.checkDatabaseCache( + sourcePath, + sourceMtime, + TransformationType.HIERARCHICAL_SUMMARY + ); + + if (cacheCheckResult.foundCache && cacheCheckResult.result.success) { + // 找到缓存的工作区洞察,直接返回 + console.log(`使用缓存的工作区洞察: ${workspace.name}`); + + onProgress?.({ + stage: '使用缓存洞察', + current: 1, + total: 1, + currentItem: '已找到缓存的工作区洞察', + percentage: 100 + }); + + // 尝试获取洞察ID + let insightId: number | undefined; + if (this.insightManager) { + const recentInsights = await this.insightManager.getInsightsBySourcePath(sourcePath, this.embeddingModel); + const latestInsight = recentInsights.find(insight => + insight.insight_type === TransformationType.HIERARCHICAL_SUMMARY.toString() && + insight.source_mtime === sourceMtime + ); + insightId = latestInsight?.id; + } + + return { + success: true, + processedFiles, + processedFolders, + totalItems, + skippedItems, + insightId + }; + } + + // 使用默认模型或传入的模型 + const llmModel: LLMModel = model || { + provider: this.settings.applyModelProvider, + modelId: this.settings.applyModelId, + }; + + // 创建 LLM 客户端 + const client = new TransformationLLMClient(this.llmManager, llmModel); + + // 构建请求消息 + const transformationConfig = TRANSFORMATIONS[TransformationType.HIERARCHICAL_SUMMARY]; + const messages: RequestMessage[] = [ + { + role: 'system', + content: transformationConfig.prompt.replace('{userLanguage}', getFullLanguageName(getLanguage())) + }, + { + role: 'user', + content: processedDocument.processedContent + } + ]; + + // 调用 LLM 执行转换 + const result = await client.queryChatModel(messages); + + if (result.isErr()) { + return { + success: false, + error: `LLM 调用失败: ${result.error.message}`, + processedFiles, + processedFolders, + totalItems, + skippedItems + }; + } + + // 后处理结果 + const processedResult = this.postProcessResult(result.value, TransformationType.HIERARCHICAL_SUMMARY); + + // 6. 保存工作区洞察到数据库 + onProgress?.({ + stage: '保存洞察结果', + current: 1, + total: 1, + currentItem: '保存到数据库...', + percentage: 98 + }); + + let insightId: number | undefined; + + try { + await this.saveResultToDatabase( + processedResult, + TransformationType.HIERARCHICAL_SUMMARY, + sourcePath, + sourceMtime, + 'folder' // workspace 在数据库中存储为 folder 类型 + ); + + // 尝试获取刚保存的洞察ID(可选) + if (this.insightManager) { + const recentInsights = await this.insightManager.getInsightsBySourcePath(sourcePath, this.embeddingModel); + const latestInsight = recentInsights.find(insight => + insight.insight_type === TransformationType.HIERARCHICAL_SUMMARY.toString() && + insight.source_mtime === sourceMtime + ); + insightId = latestInsight?.id; + } + } catch (error) { + console.warn('保存洞察到数据库失败:', error); + // 不影响主流程,仅记录警告 + } + + // 7. 完成 + onProgress?.({ + stage: '完成', + current: 1, + total: 1, + currentItem: '工作区洞察初始化完成', + percentage: 100 + }); + + return { + success: true, + processedFiles, + processedFolders, + totalItems, + skippedItems, + insightId + }; + + } catch (error) { + return { + success: false, + error: `初始化工作区洞察失败: ${error instanceof Error ? error.message : String(error)}`, + processedFiles, + processedFolders, + totalItems: processedFiles + processedFolders + skippedItems, + skippedItems + }; + } + } + + /** + * 深度收集文件夹中的所有项目(文件和子文件夹) + */ + private async collectFolderItems(folderPath: string): Promise> { + const items: Array<{ + type: 'file' | 'folder'; + path: string; + name: string; + mtime: number; + }> = []; + + try { + const folder = this.app.vault.getAbstractFileByPath(normalizePath(folderPath)); + if (!folder || !(folder instanceof TFolder)) { + console.warn(`文件夹不存在或无法访问: ${folderPath}`); + return items; + } + + // 收集当前文件夹中的所有文件 + const allFiles = this.app.vault.getMarkdownFiles(); + const filesInFolder = allFiles.filter(file => { + const fileDirPath = file.path.substring(0, file.path.lastIndexOf('/')); + return fileDirPath === folderPath; + }); + + // 添加文件 + for (const file of filesInFolder) { + items.push({ + type: 'file', + path: file.path, + name: file.name, + mtime: file.stat.mtime + }); + } + + // 收集直接子文件夹 + const subfolders = folder.children.filter((child): child is TFolder => child instanceof TFolder); + + // 递归处理子文件夹 + for (const subfolder of subfolders) { + // 递归收集子文件夹中的内容(包含子文件夹本身) + const subItems = await this.collectFolderItems(subfolder.path); + items.push(...subItems); + } + + // 添加当前文件夹本身,其 mtime 为所有子项目的最大 mtime + let maxMtime = 0; + for (const item of items) { + if (item.mtime > maxMtime) { + maxMtime = item.mtime; + } + } + + items.push({ + type: 'folder', + path: folderPath, + name: folder.name, + mtime: maxMtime > 0 ? maxMtime : 0 + }); + + return items; + } catch (error) { + console.error(`收集文件夹项目时出错: ${folderPath}`, error); + return items; + } + } } diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index e0a8354..41f8304 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -497,6 +497,7 @@ export default { insights: { title: "AI Insights", initializeInsights: "Initialize Insights", + updateInsights: "Update Insights", clearInsights: "Clear Insights", refresh: "Refresh", initializing: "Initializing...", @@ -517,6 +518,20 @@ export default { cancel: "Cancel", confirm: "Confirm Delete" }, + initConfirm: { + initTitle: "Confirm Initialize Insights", + updateTitle: "Confirm Update Insights", + initMessage: "Are you sure you want to initialize insights for the current workspace? This will generate AI summaries and analysis.", + updateMessage: "Are you sure you want to update insights for the current workspace? This will generate AI summaries and analysis for modified or new files.", + modelLabel: "Using model:", + workspaceLabel: "Target workspace:", + defaultModel: "Default model", + initWarning: "⚠️ This process may take a long time and will incur API costs.", + updateWarning: "⚠️ This process may take some time and will incur API costs. Only modified or new files will be processed.", + cancel: "Cancel", + initConfirm: "Confirm Initialize", + updateConfirm: "Confirm Update" + }, stats: { itemsAndInsights: "{items} items, {insights} insights", workspace: "{count} workspace", @@ -542,6 +557,7 @@ export default { }, tooltips: { initialize: "Initialize insights for the current workspace, will recursively process all files and generate summaries", + update: "Update insights for the current workspace, generate summaries for modified or new files", clear: "Delete all transformations and insights for the current workspace" }, success: { diff --git a/src/lang/locale/zh-cn.ts b/src/lang/locale/zh-cn.ts index c0fac27..83f2a3d 100644 --- a/src/lang/locale/zh-cn.ts +++ b/src/lang/locale/zh-cn.ts @@ -499,6 +499,7 @@ export default { insights: { title: "AI 洞察", initializeInsights: "初始化洞察", + updateInsights: "更新洞察", clearInsights: "清除洞察", refresh: "刷新", initializing: "初始化中...", @@ -519,6 +520,20 @@ export default { cancel: "取消", confirm: "确认删除" }, + initConfirm: { + initTitle: "确认初始化洞察", + updateTitle: "确认更新洞察", + initMessage: "您确定要初始化当前工作区的洞察吗?这将生成 AI 摘要和分析。", + updateMessage: "您确定要更新当前工作区的洞察吗?这将为修改或新增的文件生成 AI 摘要和分析。", + modelLabel: "使用模型:", + workspaceLabel: "目标工作区:", + defaultModel: "默认模型", + initWarning: "⚠️ 这个过程可能需要较长时间,并会产生 API 费用。", + updateWarning: "⚠️ 这个过程可能需要一些时间,并会产生 API 费用。只会处理修改或新增的文件。", + cancel: "取消", + initConfirm: "确认初始化", + updateConfirm: "确认更新" + }, stats: { itemsAndInsights: "{items} 个项目,{insights} 个洞察", workspace: "{count}工作区", @@ -544,6 +559,7 @@ export default { }, tooltips: { initialize: "初始化当前工作区的洞察,会递归处理所有文件并生成摘要", + update: "更新当前工作区的洞察,为修改或新增的文件生成摘要", clear: "删除当前工作区的所有转换和洞察" }, success: {