mirror of
https://github.com/EthanMarti/infio-copilot.git
synced 2026-01-16 08:21:55 +00:00
update release logs
This commit is contained in:
parent
fea5b382cf
commit
98bc810b86
@ -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<ChatRef, ChatProps>((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<ChatUserMessage>(() => {
|
||||
const newMessage = getNewInputMessage(app, settings.defaultMention)
|
||||
if (props.selectedBlock) {
|
||||
@ -618,8 +624,24 @@ const Chat = forwardRef<ChatRef, ChatProps>((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,
|
||||
|
||||
@ -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<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()
|
||||
// 直接使用 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<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()
|
||||
}, [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])
|
||||
|
||||
@ -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 : '未知错误'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化查询结果(备用方法)
|
||||
*/
|
||||
|
||||
@ -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<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 regexSearchFiles = async (vault: Vault, path: string, regex: string, file_pattern: string) => {
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -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)`
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user