diff --git a/src/components/chat-view/ChatView.tsx b/src/components/chat-view/ChatView.tsx index 15a6ed8..c3a2e09 100644 --- a/src/components/chat-view/ChatView.tsx +++ b/src/components/chat-view/ChatView.tsx @@ -36,6 +36,8 @@ import { LLMModelNotSetException, } from '../../core/llm/exception' import { TransformationType } from '../../core/transformations/trans-engine' +import { Workspace } from '../../database/json/workspace/types' +import { WorkspaceManager } from '../../database/json/workspace/WorkspaceManager' import { useChatHistory } from '../../hooks/use-chat-history' import { useCustomModes } from '../../hooks/use-custom-mode' import { t } from '../../lang/helpers' @@ -139,6 +141,10 @@ const Chat = forwardRef((props, ref) => { return new PromptGenerator(getRAGEngine, app, settings, diffStrategy, customModePrompts, customModeList, getMcpHub) }, [getRAGEngine, app, settings, diffStrategy, customModePrompts, customModeList, getMcpHub]) + const workspaceManager = useMemo(() => { + return new WorkspaceManager(app) + }, [app]) + const [inputMessage, setInputMessage] = useState(() => { const newMessage = getNewInputMessage(app, settings.defaultMention) if (props.selectedBlock) { @@ -618,8 +624,24 @@ const Chat = forwardRef((props, ref) => { } }; } else if (toolArgs.type === 'list_files') { - const files = await listFilesAndFolders(app.vault, toolArgs.filepath) - const formattedContent = `[list_files for '${toolArgs.filepath}'] Result:\n${files.join('\n')}\n`; + // 获取当前工作区 + let currentWorkspace: Workspace | null = null + if (settings.workspace && settings.workspace !== 'vault') { + currentWorkspace = await workspaceManager.findByName(String(settings.workspace)) + } + + const files = await listFilesAndFolders( + app.vault, + toolArgs.filepath, + toolArgs.recursive, + currentWorkspace || undefined, + app + ) + + const contextInfo = currentWorkspace + ? `workspace '${currentWorkspace.name}'` + : toolArgs.filepath || 'vault root' + const formattedContent = `[list_files for '${contextInfo}'] Result:\n${files.join('\n')}\n`; return { type: 'list_files', applyMsgId, diff --git a/src/components/chat-view/WorkspaceEditModal.tsx b/src/components/chat-view/WorkspaceEditModal.tsx index 8e6d2c0..f0e804f 100644 --- a/src/components/chat-view/WorkspaceEditModal.tsx +++ b/src/components/chat-view/WorkspaceEditModal.tsx @@ -1,10 +1,9 @@ import { ChevronDown, FolderOpen, Plus, Tag, Trash2, X } from 'lucide-react' -import { App, TFile, TFolder } from 'obsidian' +import { App, TFolder } from 'obsidian' import { useEffect, useRef, useState } from 'react' import { Workspace, WorkspaceContent } from '../../database/json/workspace/types' import { t } from '../../lang/helpers' -import { createDataviewManager } from '../../utils/dataview' interface WorkspaceEditModalProps { workspace?: Workspace @@ -22,10 +21,10 @@ const WorkspaceEditModal = ({ onSave }: WorkspaceEditModalProps) => { // 生成默认工作区名称 - const getDefaultWorkspaceName = () => { + const getDefaultWorkspaceName = (): string => { const now = new Date() const date = `${now.getFullYear()}-${(now.getMonth() + 1).toString().padStart(2, '0')}-${now.getDate().toString().padStart(2, '0')}` - return t('workspace.editModal.defaultName', { date }) + return String(t('workspace.editModal.defaultName', { date })) } const [name, setName] = useState(workspace?.name || getDefaultWorkspaceName()) @@ -51,11 +50,6 @@ const WorkspaceEditModal = ({ const folders: string[] = [] const addFolder = (folder: TFolder) => { folders.push(folder.path) - // folder.children.forEach(child => { - // if (child instanceof TFolder) { - // addFolder(child) - // } - // }) } app.vault.getAllFolders(false).forEach(folder => { @@ -64,68 +58,17 @@ const WorkspaceEditModal = ({ setAvailableFolders(folders.sort()) - // 使用 dataview 查询获取所有标签 - const dataviewManager = createDataviewManager(app) - - if (dataviewManager.isDataviewAvailable()) { - try { - const result = await dataviewManager.executeQuery('TABLE file.tags FROM ""') - - if (result.success && result.data) { - const tags = new Set() - - // 解析结果中的标签 - const lines = result.data.split('\n') - lines.forEach(line => { - if (line.includes('#')) { - const tagMatches = line.match(/#[a-zA-Z0-9\u4e00-\u9fa5_-]+/g) - if (tagMatches) { - tagMatches.forEach(tag => tags.add(tag)) - } - } - }) - - setAvailableTags(Array.from(tags).sort()) - } else { - // 回退到传统方法 - fallbackToTraditionalTagQuery() - } - } catch (error) { - console.error('Dataview 查询失败:', error) - // 回退到传统方法 - fallbackToTraditionalTagQuery() - } - } else { - // 回退到传统方法 - fallbackToTraditionalTagQuery() + // 直接使用 Obsidian 的内置接口获取所有标签 + try { + const tagsObject = app.metadataCache.getTags() // 获取所有标签 {'#tag1': 2, '#tag2': 4} + const tags = Object.keys(tagsObject).sort() + setAvailableTags(tags) + } catch (error) { + console.error('获取标签失败:', error) + setAvailableTags([]) } } - // 传统方法获取标签(作为回退方案) - const fallbackToTraditionalTagQuery = () => { - const tags = new Set() - app.vault.getAllLoadedFiles().forEach(file => { - if (file instanceof TFile) { - const cache = app.metadataCache.getFileCache(file) - if (cache?.tags) { - cache.tags.forEach(tag => { - tags.add(tag.tag) - }) - } - if (cache?.frontmatter?.tags) { - const frontmatterTags = cache.frontmatter.tags - if (Array.isArray(frontmatterTags)) { - frontmatterTags.forEach(tag => tags.add(`#${tag}`)) - } else if (typeof frontmatterTags === 'string') { - tags.add(`#${frontmatterTags}`) - } - } - } - }) - - setAvailableTags(Array.from(tags).sort()) - } - loadAvailableOptions() }, [isOpen, app]) @@ -161,9 +104,15 @@ const WorkspaceEditModal = ({ } }) - // 搜索匹配的标签 + // 搜索匹配的标签 availableTags.forEach(tag => { - if (tag.toLowerCase().includes(searchTerm)) { + // 改善搜索匹配逻辑,支持中文和更灵活的匹配 + const tagForSearch = tag.toLowerCase() + const shouldMatch = searchTerm.startsWith('#') + ? tagForSearch.includes(searchTerm.toLowerCase()) // 如果搜索词以#开头,直接匹配 + : tagForSearch.includes(searchTerm) || tagForSearch.includes(`#${searchTerm}`) // 否则同时匹配带#和不带#的情况 + + if (shouldMatch) { // 检查是否已存在 const exists = content.some(item => item.type === 'tag' && item.content === tag @@ -190,7 +139,7 @@ const WorkspaceEditModal = ({ }) } - setFilteredSuggestions(suggestions.slice(0, 10)) // 限制显示数量 + setFilteredSuggestions(suggestions.slice(0, 20)) // 限制显示数量 setShowSuggestions(suggestions.length > 0) setSelectedSuggestionIndex(-1) }, [inputValue, availableFolders, availableTags, content]) diff --git a/src/utils/dataview.ts b/src/utils/dataview.ts index ddf2be1..e8e3835 100644 --- a/src/utils/dataview.ts +++ b/src/utils/dataview.ts @@ -81,6 +81,40 @@ export class DataviewManager { } } + /** + * 执行 Dataview JS + */ + async executeJs(js: string): Promise { + const api = this.getAPI(); + if (!api) { + return { + success: false, + error: "Dataview 插件未安装或未启用" + }; + } + + try { + const result = await api.evaluate(js); + if (result.successful) { + return { + success: true, + data: result.value + }; + } else { + return { + success: false, + error: String(result.error || 'JS 查询失败') + }; + } + } catch (error) { + console.error('Dataview JS 执行失败:', error); + return { + success: false, + error: error instanceof Error ? error.message : '未知错误' + }; + } + } + /** * 格式化查询结果(备用方法) */ diff --git a/src/utils/glob-utils.ts b/src/utils/glob-utils.ts index 77c6489..d75ed60 100644 --- a/src/utils/glob-utils.ts +++ b/src/utils/glob-utils.ts @@ -1,5 +1,7 @@ import { minimatch } from 'minimatch' -import { TFile, TFolder, Vault } from 'obsidian' +import { App, TFile, TFolder, Vault } from 'obsidian' + +import { Workspace } from '../database/json/workspace/types' export const findFilesMatchingPatterns = async ( patterns: string[], @@ -11,27 +13,216 @@ export const findFilesMatchingPatterns = async ( }) } -export const listFilesAndFolders = async (vault: Vault, path: string) => { - const folder = vault.getAbstractFileByPath(path) - const childrenFiles: string[] = [] - const childrenFolders: string[] = [] - if (folder instanceof TFolder) { - folder.children.forEach((child) => { - if (child instanceof TFile) { - childrenFiles.push(child.path) - } else if (child instanceof TFolder) { - childrenFolders.push(child.path + "/") +/** + * 根据标签查找文件 + */ + +export const getFilesWithTag = (targetTag: string, app: App): string[] => { + // 确保输入的标签以 '#' 开头 + if (!targetTag.startsWith('#')) { + targetTag = '#' + targetTag; + } + + const filesWithTag: string[] = []; // 文件路径列表 + + // 1. 获取 Vault 中所有的 Markdown 文件 + const allFiles = app.vault.getMarkdownFiles(); + + // 2. 遍历所有文件 + for (const file of allFiles) { + // 3. 获取当前文件的元数据缓存 + // 这个操作非常快,因为它读取的是内存中的缓存 + const cache = app.metadataCache.getFileCache(file); + + // 检查缓存是否存在,以及缓存中是否有 tags 属性 + if (cache?.tags) { + // 4. 在文件的标签数组中查找目标标签 + // cache.tags 是一个 TagCache[] 数组,每个对象的格式为 { tag: string; position: Pos; } + const found = cache.tags.find(tagObj => tagObj.tag === targetTag); + if (found) { + filesWithTag.push(file.path); + } + } + } + + return filesWithTag; +} + +/** + * 列出工作区的文件和文件夹 + */ +export const listFilesAndFolders = async ( + vault: Vault, + path?: string, + recursive = false, + workspace?: Workspace, + app?: App +): Promise => { + const result: string[] = [] + + // 如果有工作区,使用工作区内容 + if (workspace && app) { + result.push(`[Workspace: ${workspace.name}]`) + result.push('') + + // 按类型分组处理工作区内容 + const folders = workspace.content.filter(c => c.type === 'folder') + const tags = workspace.content.filter(c => c.type === 'tag') + + // 处理文件夹 + if (folders.length > 0) { + result.push('=== FOLDERS ===') + for (const folderItem of folders) { + const folder = vault.getAbstractFileByPath(folderItem.content) + if (folder && folder instanceof TFolder) { + result.push(`├── ${folder.path}/`) + + if (recursive) { + // 递归显示文件夹内容 + const subContent = await listFolderContentsRecursively(folder, '│ ') + result.push(...subContent) + } else { + // 只显示第一层内容 + const subContent = await listFolderContentsFirstLevel(folder, '│ ') + result.push(...subContent) + } + } + } + + // 如果还有标签,添加空行分隔 + if (tags.length > 0) { + result.push('') + } + } + + // 处理标签(使用平铺格式,不使用树状结构) + if (tags.length > 0) { + result.push('=== TAGS ===') + for (const tagItem of tags) { + const files = getFilesWithTag(tagItem.content, app) + if (files.length > 0) { + result.push(`${tagItem.content} (${files.length} files):`) + + // 使用简单的列表格式显示文件 + files.forEach((file) => { + result.push(`${file}`) + }) + + // 在标签组之间添加空行 + result.push('') + } else { + result.push(`${tagItem.content} (0 files)`) + result.push('') + } + } + } + + return result + } + + // 原有的单个路径逻辑(保持向后兼容) + const startPath = path && path !== '' && path !== '.' && path !== '/' ? path : '' + const folder = startPath ? vault.getAbstractFileByPath(startPath) : vault.getRoot() + + if (!folder || !(folder instanceof TFolder)) { + return [] + } + + const listFolderContents = (currentFolder: TFolder, prefix = '') => { + const children = [...currentFolder.children].sort((a, b) => { + if (a instanceof TFolder && b instanceof TFile) return -1 + if (a instanceof TFile && b instanceof TFolder) return 1 + return a.name.localeCompare(b.name) + }) + + children.forEach((child, index) => { + const isLast = index === children.length - 1 + const currentPrefix = prefix + (isLast ? '└── ' : '├── ') + const nextPrefix = prefix + (isLast ? ' ' : '│ ') + + if (child instanceof TFolder) { + result.push(`${currentPrefix}${child.path}/`) + + if (recursive) { + listFolderContents(child, nextPrefix) + } + } else if (child instanceof TFile) { + result.push(`${currentPrefix}${child.path}`) } }) - return [...childrenFolders, ...childrenFiles] } - return [] + + if (startPath) { + result.push(`${folder.path}/`) + listFolderContents(folder, '') + } else { + result.push(`${vault.getName()}/`) + listFolderContents(folder, '') + } + + return result +} + +/** + * 递归列出文件夹内容 + */ +const listFolderContentsRecursively = async (folder: TFolder, prefix: string): Promise => { + const result: string[] = [] + + const children = [...folder.children].sort((a, b) => { + if (a instanceof TFolder && b instanceof TFile) return -1 + if (a instanceof TFile && b instanceof TFolder) return 1 + return a.name.localeCompare(b.name) + }) + + for (let i = 0; i < children.length; i++) { + const child = children[i] + const isLast = i === children.length - 1 + const currentPrefix = prefix + (isLast ? '└── ' : '├── ') + const nextPrefix = prefix + (isLast ? ' ' : '│ ') + + if (child instanceof TFolder) { + result.push(`${currentPrefix}${child.path}/`) + const subContent = await listFolderContentsRecursively(child, nextPrefix) + result.push(...subContent) + } else if (child instanceof TFile) { + result.push(`${currentPrefix}${child.path}`) + } + } + + return result +} + +/** + * 只列出文件夹第一层内容 + */ +const listFolderContentsFirstLevel = async (folder: TFolder, prefix: string): Promise => { + const result: string[] = [] + + const children = [...folder.children].sort((a, b) => { + if (a instanceof TFolder && b instanceof TFile) return -1 + if (a instanceof TFile && b instanceof TFolder) return 1 + return a.name.localeCompare(b.name) + }) + + children.forEach((child, index) => { + const isLast = index === children.length - 1 + const currentPrefix = prefix + (isLast ? '└── ' : '├── ') + + if (child instanceof TFolder) { + result.push(`${currentPrefix}${child.path}/`) + } else if (child instanceof TFile) { + result.push(`${currentPrefix}${child.path}`) + } + }) + + return result } export const matchSearchFiles = async (vault: Vault, path: string, query: string, file_pattern: string) => { - + } export const regexSearchFiles = async (vault: Vault, path: string, regex: string, file_pattern: string) => { - + } diff --git a/src/utils/prompt-generator.ts b/src/utils/prompt-generator.ts index a83a87b..c8a2d64 100644 --- a/src/utils/prompt-generator.ts +++ b/src/utils/prompt-generator.ts @@ -413,7 +413,16 @@ export class PromptGenerator { if (currentWorkspaceName && currentWorkspaceName !== 'vault') { const workspace = await this.workspaceManager.findByName(currentWorkspaceName) if (workspace) { - overview += `\n\n# Current Workspace\n${workspace.name}` + // 使用 listFilesAndFolders 获取详细的工作区结构 + const { listFilesAndFolders } = await import('./glob-utils') + const workspaceStructure = await listFilesAndFolders( + this.app.vault, + undefined, + false, // 非递归,只显示第一层 + workspace, + this.app + ) + overview += `\n\n# Current Workspace\n${workspaceStructure.join('\n')}` } } else { overview += `\n\n# Current Workspace\n${this.app.vault.getName()} (entire vault)`