mirror of
https://github.com/EthanMarti/infio-copilot.git
synced 2026-01-16 08:21:55 +00:00
Optimize the insight view component, add workspace insight initialization functionality, update internationalization support, improve user interaction prompts, enhance log output, and ensure better user experience and code readability.
This commit is contained in:
parent
932b2d3d7f
commit
51f8620815
@ -1286,7 +1286,7 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
{/* header view */}
|
||||
<div className="infio-chat-header">
|
||||
<div className="infio-chat-header-title">
|
||||
{t('workspace.shortTitle')}: <WorkspaceSelect />
|
||||
<WorkspaceSelect />
|
||||
</div>
|
||||
<div className="infio-chat-header-buttons">
|
||||
<button
|
||||
|
||||
@ -4,7 +4,7 @@ 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 { InitWorkspaceInsightResult } from '../../core/transformations/trans-engine'
|
||||
import { Workspace } from '../../database/json/workspace/types'
|
||||
import { WorkspaceManager } from '../../database/json/workspace/WorkspaceManager'
|
||||
import { SelectSourceInsight } from '../../database/schema'
|
||||
@ -45,13 +45,20 @@ const InsightView = () => {
|
||||
current: number
|
||||
total: number
|
||||
currentItem: string
|
||||
percentage?: number
|
||||
} | null>(null)
|
||||
const [initSuccess, setInitSuccess] = useState<{
|
||||
show: boolean
|
||||
result?: InitWorkspaceInsightResult
|
||||
workspaceName?: string
|
||||
}>({ show: false })
|
||||
|
||||
// 删除洞察状态
|
||||
const [isDeleting, setIsDeleting] = useState(false)
|
||||
const [deletingInsightId, setDeletingInsightId] = useState<number | null>(null)
|
||||
// 确认对话框状态
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
||||
const [showInitConfirm, setShowInitConfirm] = useState(false)
|
||||
|
||||
const loadInsights = useCallback(async () => {
|
||||
setIsLoading(true)
|
||||
@ -190,53 +197,63 @@ const InsightView = () => {
|
||||
|
||||
const transEngine = await getTransEngine()
|
||||
|
||||
// 设置初始进度状态
|
||||
setInitProgress({
|
||||
stage: t('insights.stage.preparing'),
|
||||
current: 0,
|
||||
total: 1,
|
||||
currentItem: currentWorkspace.name
|
||||
})
|
||||
|
||||
// 使用 runTransformation 处理工作区
|
||||
const result = await transEngine.runTransformation({
|
||||
filePath: currentWorkspace.name, // 工作区名称作为标识
|
||||
contentType: 'workspace',
|
||||
transformationType: TransformationType.HIERARCHICAL_SUMMARY, // 使用分层摘要类型
|
||||
// 使用新的 initWorkspaceInsight 方法
|
||||
const result = await transEngine.initWorkspaceInsight({
|
||||
workspace: currentWorkspace,
|
||||
model: {
|
||||
provider: settings.applyModelProvider,
|
||||
modelId: settings.applyModelId,
|
||||
},
|
||||
saveToDatabase: true,
|
||||
workspaceMetadata: {
|
||||
name: currentWorkspace.name,
|
||||
description: currentWorkspace.metadata?.description || '',
|
||||
workspace: currentWorkspace
|
||||
}
|
||||
})
|
||||
|
||||
// 更新进度为完成状态
|
||||
onProgress: (progress) => {
|
||||
setInitProgress({
|
||||
stage: t('insights.stage.completing'),
|
||||
current: 1,
|
||||
total: 1,
|
||||
currentItem: t('insights.stage.savingResults')
|
||||
stage: progress.stage,
|
||||
current: progress.current,
|
||||
total: progress.total,
|
||||
currentItem: progress.currentItem,
|
||||
percentage: progress.percentage
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
// 刷新洞察列表
|
||||
await loadInsights()
|
||||
|
||||
// 显示成功消息
|
||||
// 显示成功消息和统计信息
|
||||
console.log(t('insights.success.workspaceInitialized', { name: currentWorkspace.name }))
|
||||
console.log(`✅ 深度处理完成统计:`)
|
||||
console.log(`📁 文件: ${result.processedFiles} 个处理成功`)
|
||||
console.log(`📂 文件夹: ${result.processedFolders} 个处理成功`)
|
||||
console.log(`📊 总计: ${result.totalItems} 个项目(包含所有子项目)`)
|
||||
if (result.skippedItems > 0) {
|
||||
console.log(`⚠️ 跳过: ${result.skippedItems} 个项目`)
|
||||
}
|
||||
if (result.insightId) {
|
||||
console.log(`🔍 洞察ID: ${result.insightId}`)
|
||||
}
|
||||
console.log(`💡 工作区摘要仅使用顶层配置项目,避免内容重叠`)
|
||||
|
||||
// 显示成功状态
|
||||
setInitSuccess({
|
||||
show: true,
|
||||
result: result,
|
||||
workspaceName: currentWorkspace.name
|
||||
})
|
||||
|
||||
// 3秒后自动隐藏成功消息
|
||||
setTimeout(() => {
|
||||
setInitSuccess({ show: false })
|
||||
}, 5000)
|
||||
|
||||
} else {
|
||||
console.error(t('insights.error.initializationFailed'), result.error)
|
||||
throw new Error(result.error || t('insights.error.initializationFailed'))
|
||||
throw new Error(String(result.error || t('insights.error.initializationFailed')))
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(t('insights.error.initializationFailed'), error)
|
||||
setInsightResults([])
|
||||
setInitSuccess({ show: false }) // 清理成功状态
|
||||
} finally {
|
||||
setIsInitializing(false)
|
||||
setInitProgress(null)
|
||||
@ -248,6 +265,11 @@ const InsightView = () => {
|
||||
setShowDeleteConfirm(true)
|
||||
}, [])
|
||||
|
||||
// 确认初始化/更新洞察
|
||||
const handleInitWorkspaceInsights = useCallback(() => {
|
||||
setShowInitConfirm(true)
|
||||
}, [])
|
||||
|
||||
// 删除工作区洞察
|
||||
const deleteWorkspaceInsights = useCallback(async () => {
|
||||
setIsDeleting(true)
|
||||
@ -296,6 +318,17 @@ const InsightView = () => {
|
||||
setShowDeleteConfirm(false)
|
||||
}, [])
|
||||
|
||||
// 确认初始化洞察
|
||||
const confirmInitWorkspaceInsights = useCallback(async () => {
|
||||
setShowInitConfirm(false)
|
||||
await initializeWorkspaceInsights()
|
||||
}, [initializeWorkspaceInsights])
|
||||
|
||||
// 取消初始化确认
|
||||
const cancelInitConfirm = useCallback(() => {
|
||||
setShowInitConfirm(false)
|
||||
}, [])
|
||||
|
||||
// 删除单个洞察
|
||||
const deleteSingleInsight = useCallback(async (insightId: number) => {
|
||||
setDeletingInsightId(insightId)
|
||||
@ -490,12 +523,12 @@ const InsightView = () => {
|
||||
<h3>{t('insights.title')}</h3>
|
||||
<div className="obsidian-insight-actions">
|
||||
<button
|
||||
onClick={initializeWorkspaceInsights}
|
||||
onClick={handleInitWorkspaceInsights}
|
||||
disabled={isInitializing || isLoading || isDeleting}
|
||||
className="obsidian-insight-init-btn"
|
||||
title={t('insights.tooltips.initialize')}
|
||||
title={hasLoaded && insightResults.length > 0 ? t('insights.tooltips.update') : t('insights.tooltips.initialize')}
|
||||
>
|
||||
{isInitializing ? t('insights.initializing') : t('insights.initializeInsights')}
|
||||
{isInitializing ? t('insights.initializing') : (hasLoaded && insightResults.length > 0 ? t('insights.updateInsights') : t('insights.initializeInsights'))}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDeleteWorkspaceInsights}
|
||||
@ -518,26 +551,45 @@ const InsightView = () => {
|
||||
{/* 结果统计 */}
|
||||
{hasLoaded && !isLoading && (
|
||||
<div className="obsidian-insight-stats">
|
||||
<div className="obsidian-insight-stats-line">
|
||||
{t('insights.stats.itemsAndInsights', { items: insightGroupedResults.length, insights: insightResults.length })}
|
||||
<div className="obsidian-insight-stats-overview">
|
||||
<div className="obsidian-insight-stats-main">
|
||||
<span className="obsidian-insight-stats-number">{insightResults.length}</span>
|
||||
<span className="obsidian-insight-stats-label">个洞察</span>
|
||||
</div>
|
||||
<div className="obsidian-insight-stats-breakdown">
|
||||
{insightGroupedResults.length > 0 && (
|
||||
<span className="obsidian-insight-breakdown">
|
||||
{' '}(
|
||||
{insightGroupedResults.filter(g => g.groupType === 'workspace').length > 0 &&
|
||||
`${t('insights.stats.workspace', { count: insightGroupedResults.filter(g => g.groupType === 'workspace').length })} `}
|
||||
{insightGroupedResults.filter(g => g.groupType === 'folder').length > 0 &&
|
||||
`${t('insights.stats.folder', { count: insightGroupedResults.filter(g => g.groupType === 'folder').length })} `}
|
||||
{insightGroupedResults.filter(g => g.groupType === 'file').length > 0 &&
|
||||
`${t('insights.stats.file', { count: insightGroupedResults.filter(g => g.groupType === 'file').length })}`}
|
||||
)
|
||||
<div className="obsidian-insight-stats-items">
|
||||
{insightGroupedResults.filter(g => g.groupType === 'workspace').length > 0 && (
|
||||
<div className="obsidian-insight-stats-item">
|
||||
<span className="obsidian-insight-stats-item-icon">🌐</span>
|
||||
<span className="obsidian-insight-stats-item-value">
|
||||
{insightGroupedResults.filter(g => g.groupType === 'workspace').length}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{currentScope && (
|
||||
<div className="obsidian-insight-scope">
|
||||
{t('insights.stats.scopeLabel')} {currentScope}
|
||||
<span className="obsidian-insight-stats-item-label">工作区</span>
|
||||
</div>
|
||||
)}
|
||||
{insightGroupedResults.filter(g => g.groupType === 'folder').length > 0 && (
|
||||
<div className="obsidian-insight-stats-item">
|
||||
<span className="obsidian-insight-stats-item-icon">📂</span>
|
||||
<span className="obsidian-insight-stats-item-value">
|
||||
{insightGroupedResults.filter(g => g.groupType === 'folder').length}
|
||||
</span>
|
||||
<span className="obsidian-insight-stats-item-label">文件夹</span>
|
||||
</div>
|
||||
)}
|
||||
{insightGroupedResults.filter(g => g.groupType === 'file').length > 0 && (
|
||||
<div className="obsidian-insight-stats-item">
|
||||
<span className="obsidian-insight-stats-item-icon">📄</span>
|
||||
<span className="obsidian-insight-stats-item-value">
|
||||
{insightGroupedResults.filter(g => g.groupType === 'file').length}
|
||||
</span>
|
||||
<span className="obsidian-insight-stats-item-label">文件</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -568,18 +620,58 @@ const InsightView = () => {
|
||||
<div
|
||||
className="obsidian-insight-progress-fill"
|
||||
style={{
|
||||
width: `${(initProgress.current / Math.max(initProgress.total, 1)) * 100}%`
|
||||
width: `${initProgress.percentage !== undefined ? initProgress.percentage : (initProgress.current / Math.max(initProgress.total, 1)) * 100}%`
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
<div className="obsidian-insight-progress-details">
|
||||
<div className="obsidian-insight-progress-item">
|
||||
{t('insights.progress.current', { item: initProgress.currentItem })}
|
||||
{initProgress.currentItem}
|
||||
</div>
|
||||
<div className="obsidian-insight-progress-percentage">
|
||||
{initProgress.percentage !== undefined ? initProgress.percentage : Math.round((initProgress.current / Math.max(initProgress.total, 1)) * 100)}%
|
||||
</div>
|
||||
</div>
|
||||
{/* 进度日志 */}
|
||||
<div className="obsidian-insight-progress-log">
|
||||
<div className="obsidian-insight-progress-log-item">
|
||||
<span className="obsidian-insight-progress-log-label">阶段:</span>
|
||||
<span className="obsidian-insight-progress-log-value">{initProgress.stage}</span>
|
||||
</div>
|
||||
<div className="obsidian-insight-progress-log-item">
|
||||
<span className="obsidian-insight-progress-log-label">进度:</span>
|
||||
<span className="obsidian-insight-progress-log-value">{initProgress.current} / {initProgress.total}</span>
|
||||
</div>
|
||||
<div className="obsidian-insight-progress-log-item">
|
||||
<span className="obsidian-insight-progress-log-label">当前:</span>
|
||||
<span className="obsidian-insight-progress-log-value">{initProgress.currentItem}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 初始化成功消息 */}
|
||||
{initSuccess.show && initSuccess.result && (
|
||||
<div className="obsidian-insight-success">
|
||||
<div className="obsidian-insight-success-content">
|
||||
<span className="obsidian-insight-success-icon">✅</span>
|
||||
<div className="obsidian-insight-success-text">
|
||||
<span className="obsidian-insight-success-title">
|
||||
{t('insights.success.workspaceInitialized', { name: initSuccess.workspaceName })}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
className="obsidian-insight-success-close"
|
||||
onClick={() => setInitSuccess({ show: false })}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 确认删除对话框 */}
|
||||
{showDeleteConfirm && (
|
||||
<div className="obsidian-confirm-dialog-overlay">
|
||||
@ -616,6 +708,53 @@ const InsightView = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 确认初始化/更新对话框 */}
|
||||
{showInitConfirm && (
|
||||
<div className="obsidian-confirm-dialog-overlay">
|
||||
<div className="obsidian-confirm-dialog">
|
||||
<div className="obsidian-confirm-dialog-header">
|
||||
<h3>{hasLoaded && insightResults.length > 0 ? t('insights.initConfirm.updateTitle') : t('insights.initConfirm.initTitle')}</h3>
|
||||
</div>
|
||||
<div className="obsidian-confirm-dialog-body">
|
||||
<p>
|
||||
{hasLoaded && insightResults.length > 0 ? t('insights.initConfirm.updateMessage') : t('insights.initConfirm.initMessage')}
|
||||
</p>
|
||||
<div className="obsidian-confirm-dialog-info">
|
||||
<div className="obsidian-confirm-dialog-info-item">
|
||||
<strong>{t('insights.initConfirm.modelLabel')}</strong>
|
||||
<span className="obsidian-confirm-dialog-model">
|
||||
{settings.chatModelProvider} / {settings.chatModelId || t('insights.initConfirm.defaultModel')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="obsidian-confirm-dialog-info-item">
|
||||
<strong>{t('insights.initConfirm.workspaceLabel')}</strong>
|
||||
<span className="obsidian-confirm-dialog-workspace">
|
||||
{settings.workspace === 'vault' ? t('workspace.entireVault') : settings.workspace}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="obsidian-confirm-dialog-warning">
|
||||
{hasLoaded && insightResults.length > 0 ? t('insights.initConfirm.updateWarning') : t('insights.initConfirm.initWarning')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="obsidian-confirm-dialog-footer">
|
||||
<button
|
||||
onClick={cancelInitConfirm}
|
||||
className="obsidian-confirm-dialog-cancel-btn"
|
||||
>
|
||||
{t('insights.initConfirm.cancel')}
|
||||
</button>
|
||||
<button
|
||||
onClick={confirmInitWorkspaceInsights}
|
||||
className="obsidian-confirm-dialog-confirm-btn"
|
||||
>
|
||||
{hasLoaded && insightResults.length > 0 ? t('insights.initConfirm.updateConfirm') : t('insights.initConfirm.initConfirm')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 洞察结果 */}
|
||||
<div className="obsidian-insight-results">
|
||||
{!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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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<any>
|
||||
loadModel(modelId: string, useGpu: boolean): Promise<void>
|
||||
embed(text: string): Promise<{ vec: number[] }>
|
||||
embedBatch(texts: string[]): Promise<{ vec: number[] }[]>
|
||||
}
|
||||
@ -141,19 +141,12 @@ export const TRANSFORMATIONS: Record<TransformationType, TransformationConfig> =
|
||||
|
||||
// 转换参数接口
|
||||
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<string | null> {
|
||||
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<InitWorkspaceInsightResult> {
|
||||
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<string>();
|
||||
|
||||
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<Array<{
|
||||
type: 'file' | 'folder';
|
||||
path: string;
|
||||
name: string;
|
||||
mtime: number;
|
||||
}>> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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: {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user