mirror of
https://github.com/EthanMarti/infio-copilot.git
synced 2026-01-16 08:21:55 +00:00
update manage files
This commit is contained in:
parent
89bc10d16d
commit
fea5b382cf
@ -3,7 +3,7 @@ import * as path from 'path'
|
||||
import { BaseSerializedNode } from '@lexical/clipboard/clipboard'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { Box, CircleStop, History, NotebookPen, Plus, Search, Server, SquareSlash, Undo } from 'lucide-react'
|
||||
import { App, Notice, TFile, WorkspaceLeaf } from 'obsidian'
|
||||
import { App, Notice, TFile, TFolder, WorkspaceLeaf } from 'obsidian'
|
||||
import {
|
||||
forwardRef,
|
||||
useCallback,
|
||||
@ -902,6 +902,150 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
}
|
||||
};
|
||||
}
|
||||
} else if (toolArgs.type === 'manage_files') {
|
||||
try {
|
||||
const results: string[] = [];
|
||||
|
||||
// 处理每个文件操作
|
||||
for (const operation of toolArgs.operations) {
|
||||
switch (operation.action) {
|
||||
case 'create_folder':
|
||||
if (operation.path) {
|
||||
const folderExists = await app.vault.adapter.exists(operation.path);
|
||||
if (!folderExists) {
|
||||
await app.vault.adapter.mkdir(operation.path);
|
||||
results.push(`✅ 成功创建文件夹: ${operation.path}`);
|
||||
} else {
|
||||
results.push(`⚠️ 文件夹已存在: ${operation.path}`);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'move':
|
||||
if (operation.source_path && operation.destination_path) {
|
||||
// 使用 getAbstractFileByPath 而不是 getFileByPath,这样可以获取文件和文件夹
|
||||
const sourceFile = app.vault.getAbstractFileByPath(operation.source_path);
|
||||
if (sourceFile) {
|
||||
// 确保目标目录存在
|
||||
const destDir = path.dirname(operation.destination_path);
|
||||
if (destDir && destDir !== '.' && destDir !== '/') {
|
||||
const dirExists = await app.vault.adapter.exists(destDir);
|
||||
if (!dirExists) {
|
||||
await app.vault.adapter.mkdir(destDir);
|
||||
}
|
||||
}
|
||||
await app.vault.rename(sourceFile, operation.destination_path);
|
||||
const itemType = sourceFile instanceof TFile ? '文件' : '文件夹';
|
||||
results.push(`✅ 成功移动${itemType}: ${operation.source_path} → ${operation.destination_path}`);
|
||||
} else {
|
||||
results.push(`❌ 源文件或文件夹不存在: ${operation.source_path}`);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
if (operation.path) {
|
||||
// 使用 getAbstractFileByPath 而不是 getFileByPath
|
||||
const fileOrFolder = app.vault.getAbstractFileByPath(operation.path);
|
||||
if (fileOrFolder) {
|
||||
try {
|
||||
const isFolder = fileOrFolder instanceof TFolder;
|
||||
// 使用 trash 方法将文件/文件夹移到回收站,更安全
|
||||
// system: true 尝试使用系统回收站,失败则使用 Obsidian 本地回收站
|
||||
await app.vault.trash(fileOrFolder, true);
|
||||
const itemType = isFolder ? '文件夹' : '文件';
|
||||
results.push(`✅ 成功将${itemType}移到回收站: ${operation.path}`);
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error);
|
||||
results.push(`❌ 删除失败: ${operation.path} - ${error.message}`);
|
||||
}
|
||||
} else {
|
||||
results.push(`❌ 文件或文件夹不存在: ${operation.path}`);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'copy':
|
||||
if (operation.source_path && operation.destination_path) {
|
||||
// 文件夹复制比较复杂,需要递归处理
|
||||
const sourceFile = app.vault.getAbstractFileByPath(operation.source_path);
|
||||
if (sourceFile) {
|
||||
if (sourceFile instanceof TFile) {
|
||||
// 文件复制
|
||||
const destDir = path.dirname(operation.destination_path);
|
||||
if (destDir && destDir !== '.' && destDir !== '/') {
|
||||
const dirExists = await app.vault.adapter.exists(destDir);
|
||||
if (!dirExists) {
|
||||
await app.vault.adapter.mkdir(destDir);
|
||||
}
|
||||
}
|
||||
const content = await app.vault.read(sourceFile);
|
||||
await app.vault.create(operation.destination_path, content);
|
||||
results.push(`✅ 成功复制文件: ${operation.source_path} → ${operation.destination_path}`);
|
||||
} else if (sourceFile instanceof TFolder) {
|
||||
// 文件夹复制需要递归处理
|
||||
results.push(`❌ 文件夹复制功能暂未实现: ${operation.source_path}`);
|
||||
}
|
||||
} else {
|
||||
results.push(`❌ 源文件或文件夹不存在: ${operation.source_path}`);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'rename':
|
||||
if (operation.path && operation.new_name) {
|
||||
// 使用 getAbstractFileByPath 而不是 getFileByPath
|
||||
const file = app.vault.getAbstractFileByPath(operation.path);
|
||||
if (file) {
|
||||
const newPath = path.join(path.dirname(operation.path), operation.new_name);
|
||||
await app.vault.rename(file, newPath);
|
||||
const itemType = file instanceof TFile ? '文件' : '文件夹';
|
||||
results.push(`✅ 成功重命名${itemType}: ${operation.path} → ${newPath}`);
|
||||
} else {
|
||||
results.push(`❌ 文件或文件夹不存在: ${operation.path}`);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
results.push(`❌ 不支持的操作类型: ${operation.action}`);
|
||||
}
|
||||
}
|
||||
|
||||
const formattedContent = `[manage_files] 文件管理操作结果:\n${results.join('\n')}`;
|
||||
|
||||
return {
|
||||
type: 'manage_files',
|
||||
applyMsgId,
|
||||
applyStatus: ApplyStatus.Applied,
|
||||
returnMsg: {
|
||||
role: 'user',
|
||||
applyStatus: ApplyStatus.Idle,
|
||||
content: null,
|
||||
promptContent: formattedContent,
|
||||
id: uuidv4(),
|
||||
mentionables: [],
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('文件管理操作失败:', error);
|
||||
return {
|
||||
type: 'manage_files',
|
||||
applyMsgId,
|
||||
applyStatus: ApplyStatus.Failed,
|
||||
returnMsg: {
|
||||
role: 'user',
|
||||
applyStatus: ApplyStatus.Idle,
|
||||
content: null,
|
||||
promptContent: `[manage_files] 文件管理操作失败: ${error instanceof Error ? error.message : String(error)}`,
|
||||
id: uuidv4(),
|
||||
mentionables: [],
|
||||
}
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// 处理未知的工具类型
|
||||
throw new Error(`Unsupported tool type: ${toolArgs.type}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to apply changes', error)
|
||||
|
||||
124
src/components/chat-view/Markdown/MarkdownManageFilesBlock.tsx
Normal file
124
src/components/chat-view/Markdown/MarkdownManageFilesBlock.tsx
Normal file
@ -0,0 +1,124 @@
|
||||
import { Check, Copy, FileIcon, FolderPlus, Loader2, Move, Trash2, X } from 'lucide-react'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import { ApplyStatus, ManageFilesToolArgs } from "../../../types/apply"
|
||||
|
||||
interface ManageFilesOperation {
|
||||
action: 'create_folder' | 'move' | 'delete' | 'copy' | 'rename'
|
||||
path?: string
|
||||
source_path?: string
|
||||
destination_path?: string
|
||||
new_name?: string
|
||||
}
|
||||
|
||||
export default function MarkdownManageFilesBlock({
|
||||
applyStatus,
|
||||
onApply,
|
||||
operations,
|
||||
finish
|
||||
}: {
|
||||
applyStatus: ApplyStatus
|
||||
onApply: (args: ManageFilesToolArgs) => void
|
||||
operations: ManageFilesOperation[]
|
||||
finish: boolean
|
||||
}) {
|
||||
const [applying, setApplying] = useState(false)
|
||||
|
||||
const getOperationIcon = (action: string) => {
|
||||
switch (action) {
|
||||
case 'create_folder':
|
||||
return <FolderPlus size={14} className="infio-chat-code-block-header-icon" />
|
||||
case 'move':
|
||||
return <Move size={14} className="infio-chat-code-block-header-icon" />
|
||||
case 'delete':
|
||||
return <Trash2 size={14} className="infio-chat-code-block-header-icon" />
|
||||
case 'copy':
|
||||
return <Copy size={14} className="infio-chat-code-block-header-icon" />
|
||||
case 'rename':
|
||||
return <FileIcon size={14} className="infio-chat-code-block-header-icon" />
|
||||
default:
|
||||
return <FileIcon size={14} className="infio-chat-code-block-header-icon" />
|
||||
}
|
||||
}
|
||||
|
||||
const getOperationDescription = (operation: ManageFilesOperation) => {
|
||||
switch (operation.action) {
|
||||
case 'create_folder':
|
||||
return `创建文件夹:${operation.path}`
|
||||
case 'move':
|
||||
return `移动文件:${operation.source_path} → ${operation.destination_path}`
|
||||
case 'delete':
|
||||
return `删除:${operation.path}`
|
||||
case 'copy':
|
||||
return `复制:${operation.source_path} → ${operation.destination_path}`
|
||||
case 'rename':
|
||||
return `重命名:${operation.path} → ${operation.new_name}`
|
||||
default:
|
||||
return `未知操作`
|
||||
}
|
||||
}
|
||||
|
||||
const handleApply = async () => {
|
||||
if (applyStatus !== ApplyStatus.Idle) {
|
||||
return
|
||||
}
|
||||
setApplying(true)
|
||||
onApply({
|
||||
type: 'manage_files',
|
||||
operations: operations,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`infio-chat-code-block has-filename`}>
|
||||
<div className={'infio-chat-code-block-header'}>
|
||||
<div className={'infio-chat-code-block-header-filename'}>
|
||||
<FolderPlus size={14} className="infio-chat-code-block-header-icon" />
|
||||
文件管理操作 ({operations.length} 个操作)
|
||||
</div>
|
||||
<div className={'infio-chat-code-block-header-button'}>
|
||||
<button
|
||||
onClick={handleApply}
|
||||
className="infio-apply-button"
|
||||
disabled={applyStatus !== ApplyStatus.Idle || applying || !finish}
|
||||
>
|
||||
{
|
||||
!finish ? (
|
||||
<>
|
||||
<Loader2 className="spinner" size={14} /> 准备执行
|
||||
</>
|
||||
) : applyStatus === ApplyStatus.Idle ? (
|
||||
applying ? (
|
||||
<>
|
||||
<Loader2 className="spinner" size={14} /> 执行中
|
||||
</>
|
||||
) : (
|
||||
'执行操作'
|
||||
)
|
||||
) : applyStatus === ApplyStatus.Applied ? (
|
||||
<>
|
||||
<Check size={14} /> 已完成
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<X size={14} /> 执行失败
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="infio-chat-code-block-content">
|
||||
{operations.map((operation, index) => (
|
||||
<div key={index} className="manage-files-operation">
|
||||
<div className="operation-item">
|
||||
{getOperationIcon(operation.action)}
|
||||
<span className="operation-description">
|
||||
{getOperationDescription(operation)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -11,8 +11,8 @@ import MarkdownDataviewQueryBlock from './Markdown/MarkdownDataviewQueryBlock'
|
||||
import MarkdownEditFileBlock from './Markdown/MarkdownEditFileBlock'
|
||||
import MarkdownFetchUrlsContentBlock from './Markdown/MarkdownFetchUrlsContentBlock'
|
||||
import MarkdownListFilesBlock from './Markdown/MarkdownListFilesBlock'
|
||||
import MarkdownManageFilesBlock from './Markdown/MarkdownManageFilesBlock'
|
||||
import MarkdownMatchSearchFilesBlock from './Markdown/MarkdownMatchSearchFilesBlock'
|
||||
import MarkdownPlanBlock from './Markdown/MarkdownPlanBlock'
|
||||
import MarkdownReadFileBlock from './Markdown/MarkdownReadFileBlock'
|
||||
import MarkdownReasoningBlock from './Markdown/MarkdownReasoningBlock'
|
||||
import MarkdownRegexSearchFilesBlock from './Markdown/MarkdownRegexSearchFilesBlock'
|
||||
@ -44,21 +44,15 @@ function ReactMarkdown({
|
||||
return (
|
||||
<>
|
||||
{blocks.map((block, index) =>
|
||||
block.type === 'communication' ? (
|
||||
<RawMarkdownBlock
|
||||
key={"markdown-" + index}
|
||||
content={block.content}
|
||||
className="infio-markdown"
|
||||
/>
|
||||
) : block.type === 'think' ? (
|
||||
block.type === 'think' ? (
|
||||
<MarkdownReasoningBlock
|
||||
key={"reasoning-" + index}
|
||||
reasoningContent={block.content}
|
||||
/>
|
||||
) : block.type === 'thinking' ? (
|
||||
<MarkdownPlanBlock
|
||||
<RawMarkdownBlock
|
||||
key={"plan-" + index}
|
||||
planContent={block.content}
|
||||
content={block.content}
|
||||
/>
|
||||
) : block.type === 'write_to_file' ? (
|
||||
<MarkdownEditFileBlock
|
||||
@ -229,6 +223,14 @@ function ReactMarkdown({
|
||||
transformation={block.transformation}
|
||||
finish={block.finish}
|
||||
/>
|
||||
) : block.type === 'manage_files' ? (
|
||||
<MarkdownManageFilesBlock
|
||||
key={"manage-files-" + index}
|
||||
applyStatus={applyStatus}
|
||||
onApply={onApply}
|
||||
operations={block.operations}
|
||||
finish={block.finish}
|
||||
/>
|
||||
) : block.type === 'tool_result' ? (
|
||||
<MarkdownToolResult
|
||||
key={"tool-result-" + index}
|
||||
|
||||
@ -119,4 +119,16 @@ export type CallTransformationsToolArgs = {
|
||||
finish?: boolean;
|
||||
}
|
||||
|
||||
export type ToolArgs = ReadFileToolArgs | WriteToFileToolArgs | InsertContentToolArgs | SearchAndReplaceToolArgs | ListFilesToolArgs | MatchSearchFilesToolArgs | RegexSearchFilesToolArgs | SemanticSearchFilesToolArgs | SearchWebToolArgs | FetchUrlsContentToolArgs | SwitchModeToolArgs | ApplyDiffToolArgs | UseMcpToolArgs | DataviewQueryToolArgs | CallTransformationsToolArgs;
|
||||
export type ManageFilesToolArgs = {
|
||||
type: 'manage_files';
|
||||
operations: Array<{
|
||||
action: 'create_folder' | 'move' | 'delete' | 'copy' | 'rename';
|
||||
path?: string;
|
||||
source_path?: string;
|
||||
destination_path?: string;
|
||||
new_name?: string;
|
||||
}>;
|
||||
finish?: boolean;
|
||||
}
|
||||
|
||||
export type ToolArgs = ReadFileToolArgs | WriteToFileToolArgs | InsertContentToolArgs | SearchAndReplaceToolArgs | ListFilesToolArgs | MatchSearchFilesToolArgs | RegexSearchFilesToolArgs | SemanticSearchFilesToolArgs | SearchWebToolArgs | FetchUrlsContentToolArgs | SwitchModeToolArgs | ApplyDiffToolArgs | UseMcpToolArgs | DataviewQueryToolArgs | CallTransformationsToolArgs | ManageFilesToolArgs;
|
||||
|
||||
@ -105,6 +105,16 @@ export type ParsedMsgBlock =
|
||||
path: string
|
||||
transformation: string
|
||||
finish: boolean
|
||||
} | {
|
||||
type: 'manage_files'
|
||||
operations: Array<{
|
||||
action: 'create_folder' | 'move' | 'delete' | 'copy' | 'rename'
|
||||
path?: string
|
||||
source_path?: string
|
||||
destination_path?: string
|
||||
new_name?: string
|
||||
}>
|
||||
finish: boolean
|
||||
} | {
|
||||
type: 'tool_result'
|
||||
content: string
|
||||
@ -817,6 +827,76 @@ export function parseMsgBlocks(
|
||||
})
|
||||
}
|
||||
lastEndOffset = endOffset
|
||||
} else if (node.nodeName === 'manage_files') {
|
||||
if (!node.sourceCodeLocation) {
|
||||
throw new Error('sourceCodeLocation is undefined')
|
||||
}
|
||||
const startOffset = node.sourceCodeLocation.startOffset
|
||||
const endOffset = node.sourceCodeLocation.endOffset
|
||||
if (startOffset > lastEndOffset) {
|
||||
parsedResult.push({
|
||||
type: 'string',
|
||||
content: input.slice(lastEndOffset, startOffset),
|
||||
})
|
||||
}
|
||||
|
||||
let operations: Array<{
|
||||
action: 'create_folder' | 'move' | 'delete' | 'copy' | 'rename'
|
||||
path?: string
|
||||
source_path?: string
|
||||
destination_path?: string
|
||||
new_name?: string
|
||||
}> = []
|
||||
|
||||
// 检查是否有 operations 子标签
|
||||
for (const childNode of node.childNodes) {
|
||||
if (childNode.nodeName === 'operations' && childNode.childNodes.length > 0) {
|
||||
try {
|
||||
// 获取 operations 标签内的内容
|
||||
const operationsChildren = childNode.childNodes
|
||||
if (operationsChildren.length > 0) {
|
||||
const innerContentStartOffset = operationsChildren[0].sourceCodeLocation?.startOffset
|
||||
const innerContentEndOffset = operationsChildren[operationsChildren.length - 1].sourceCodeLocation?.endOffset
|
||||
|
||||
if (innerContentStartOffset && innerContentEndOffset) {
|
||||
const jsonContent = input.slice(innerContentStartOffset, innerContentEndOffset).trim()
|
||||
operations = JSON5.parse(jsonContent)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to parse operations JSON', error)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有找到 operations 子标签,尝试直接解析标签内容
|
||||
if (operations.length === 0) {
|
||||
const children = node.childNodes
|
||||
if (children.length > 0) {
|
||||
try {
|
||||
const innerContentStartOffset = children[0].sourceCodeLocation?.startOffset
|
||||
const innerContentEndOffset = children[children.length - 1].sourceCodeLocation?.endOffset
|
||||
|
||||
if (innerContentStartOffset && innerContentEndOffset) {
|
||||
const jsonContent = input.slice(innerContentStartOffset, innerContentEndOffset).trim()
|
||||
// 检查内容是否以 [ 开头(纯 JSON 数组)
|
||||
if (jsonContent.startsWith('[')) {
|
||||
operations = JSON5.parse(jsonContent)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to parse manage_files JSON', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parsedResult.push({
|
||||
type: 'manage_files',
|
||||
operations,
|
||||
finish: node.sourceCodeLocation.endTag !== undefined
|
||||
})
|
||||
lastEndOffset = endOffset
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
30
styles.css
30
styles.css
@ -2068,6 +2068,36 @@ button.infio-chat-input-model-select {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/*
|
||||
* Manage Files Block Styles
|
||||
*/
|
||||
.manage-files-operation {
|
||||
margin-bottom: var(--size-2-1);
|
||||
}
|
||||
|
||||
.operation-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--size-2-1);
|
||||
padding: var(--size-2-1) var(--size-4-1);
|
||||
border-radius: var(--radius-s);
|
||||
font-size: var(--font-ui-small);
|
||||
color: var(--text-normal);
|
||||
background-color: var(--background-secondary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.operation-item:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.operation-description {
|
||||
flex: 1;
|
||||
word-break: break-word;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.infio-chat-code-block-status-button {
|
||||
color: var(--color-green); /* 替换原来的 #008000 */
|
||||
background: none;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user