update release logs

This commit is contained in:
duanfuxiang 2025-07-02 11:59:16 +08:00
parent fea5b382cf
commit 98bc810b86
5 changed files with 294 additions and 89 deletions

View File

@ -36,6 +36,8 @@ import {
LLMModelNotSetException, LLMModelNotSetException,
} from '../../core/llm/exception' } from '../../core/llm/exception'
import { TransformationType } from '../../core/transformations/trans-engine' 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 { useChatHistory } from '../../hooks/use-chat-history'
import { useCustomModes } from '../../hooks/use-custom-mode' import { useCustomModes } from '../../hooks/use-custom-mode'
import { t } from '../../lang/helpers' import { t } from '../../lang/helpers'
@ -139,6 +141,10 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
return new PromptGenerator(getRAGEngine, app, settings, diffStrategy, customModePrompts, customModeList, getMcpHub) return new PromptGenerator(getRAGEngine, app, settings, diffStrategy, customModePrompts, customModeList, getMcpHub)
}, [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<ChatUserMessage>(() => { const [inputMessage, setInputMessage] = useState<ChatUserMessage>(() => {
const newMessage = getNewInputMessage(app, settings.defaultMention) const newMessage = getNewInputMessage(app, settings.defaultMention)
if (props.selectedBlock) { if (props.selectedBlock) {
@ -618,8 +624,24 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
} }
}; };
} else if (toolArgs.type === 'list_files') { } 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 { return {
type: 'list_files', type: 'list_files',
applyMsgId, applyMsgId,

View File

@ -1,10 +1,9 @@
import { ChevronDown, FolderOpen, Plus, Tag, Trash2, X } from 'lucide-react' 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 { useEffect, useRef, useState } from 'react'
import { Workspace, WorkspaceContent } from '../../database/json/workspace/types' import { Workspace, WorkspaceContent } from '../../database/json/workspace/types'
import { t } from '../../lang/helpers' import { t } from '../../lang/helpers'
import { createDataviewManager } from '../../utils/dataview'
interface WorkspaceEditModalProps { interface WorkspaceEditModalProps {
workspace?: Workspace workspace?: Workspace
@ -22,10 +21,10 @@ const WorkspaceEditModal = ({
onSave onSave
}: WorkspaceEditModalProps) => { }: WorkspaceEditModalProps) => {
// 生成默认工作区名称 // 生成默认工作区名称
const getDefaultWorkspaceName = () => { const getDefaultWorkspaceName = (): string => {
const now = new Date() const now = new Date()
const date = `${now.getFullYear()}-${(now.getMonth() + 1).toString().padStart(2, '0')}-${now.getDate().toString().padStart(2, '0')}` 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()) const [name, setName] = useState(workspace?.name || getDefaultWorkspaceName())
@ -51,11 +50,6 @@ const WorkspaceEditModal = ({
const folders: string[] = [] const folders: string[] = []
const addFolder = (folder: TFolder) => { const addFolder = (folder: TFolder) => {
folders.push(folder.path) folders.push(folder.path)
// folder.children.forEach(child => {
// if (child instanceof TFolder) {
// addFolder(child)
// }
// })
} }
app.vault.getAllFolders(false).forEach(folder => { app.vault.getAllFolders(false).forEach(folder => {
@ -64,68 +58,17 @@ const WorkspaceEditModal = ({
setAvailableFolders(folders.sort()) setAvailableFolders(folders.sort())
// 使用 dataview 查询获取所有标签 // 直接使用 Obsidian 的内置接口获取所有标签
const dataviewManager = createDataviewManager(app) try {
const tagsObject = app.metadataCache.getTags() // 获取所有标签 {'#tag1': 2, '#tag2': 4}
if (dataviewManager.isDataviewAvailable()) { const tags = Object.keys(tagsObject).sort()
try { setAvailableTags(tags)
const result = await dataviewManager.executeQuery('TABLE file.tags FROM ""') } catch (error) {
console.error('获取标签失败:', error)
if (result.success && result.data) { setAvailableTags([])
const tags = new Set<string>()
// 解析结果中的标签
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()
} }
} }
// 传统方法获取标签(作为回退方案)
const fallbackToTraditionalTagQuery = () => {
const tags = new Set<string>()
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() loadAvailableOptions()
}, [isOpen, app]) }, [isOpen, app])
@ -161,9 +104,15 @@ const WorkspaceEditModal = ({
} }
}) })
// 搜索匹配的标签 // 搜索匹配的标签
availableTags.forEach(tag => { 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 => const exists = content.some(item =>
item.type === 'tag' && item.content === tag 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) setShowSuggestions(suggestions.length > 0)
setSelectedSuggestionIndex(-1) setSelectedSuggestionIndex(-1)
}, [inputValue, availableFolders, availableTags, content]) }, [inputValue, availableFolders, availableTags, content])

View File

@ -81,6 +81,40 @@ export class DataviewManager {
} }
} }
/**
* Dataview JS
*/
async executeJs(js: string): Promise<DataviewQueryResult> {
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 : '未知错误'
};
}
}
/** /**
* *
*/ */

View File

@ -1,5 +1,7 @@
import { minimatch } from 'minimatch' 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 ( export const findFilesMatchingPatterns = async (
patterns: string[], 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) { export const getFilesWithTag = (targetTag: string, app: App): string[] => {
folder.children.forEach((child) => { // 确保输入的标签以 '#' 开头
if (child instanceof TFile) { if (!targetTag.startsWith('#')) {
childrenFiles.push(child.path) targetTag = '#' + targetTag;
} else if (child instanceof TFolder) { }
childrenFolders.push(child.path + "/")
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<string[]> => {
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<string[]> => {
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<string[]> => {
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 matchSearchFiles = async (vault: Vault, path: string, query: string, file_pattern: string) => {
} }
export const regexSearchFiles = async (vault: Vault, path: string, regex: string, file_pattern: string) => { export const regexSearchFiles = async (vault: Vault, path: string, regex: string, file_pattern: string) => {
} }

View File

@ -413,7 +413,16 @@ export class PromptGenerator {
if (currentWorkspaceName && currentWorkspaceName !== 'vault') { if (currentWorkspaceName && currentWorkspaceName !== 'vault') {
const workspace = await this.workspaceManager.findByName(currentWorkspaceName) const workspace = await this.workspaceManager.findByName(currentWorkspaceName)
if (workspace) { 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 { } else {
overview += `\n\n# Current Workspace\n${this.app.vault.getName()} (entire vault)` overview += `\n\n# Current Workspace\n${this.app.vault.getName()} (entire vault)`