优化guideline提示词,新增admin后台修改prompt功能
This commit is contained in:
parent
c5b68a91f3
commit
bb640e6d6e
@ -45,10 +45,12 @@
|
||||
- **限制条件**: [需要遵守的规则和约束]
|
||||
- **可用资源**: [可以利用的工具和资源]
|
||||
|
||||
### ⚡ 行动计划
|
||||
### ⚡ 执行步骤
|
||||
1. [第一步的具体行动]
|
||||
2. [第二步的具体行动]
|
||||
3. [第三步的具体行动]
|
||||
4. [第四步的具体行动]
|
||||
5. [第五步的具体行动]
|
||||
|
||||
## 输出语言 (Language)
|
||||
{language}
|
||||
|
||||
@ -477,6 +477,174 @@
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* 文件编辑器相关样式 */
|
||||
.editor-container {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 0.375rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.editor-wrapper {
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.line-numbers {
|
||||
background-color: #f8f9fa;
|
||||
border-right: 1px solid var(--border-color);
|
||||
color: #6c757d;
|
||||
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
padding: 12px 8px;
|
||||
text-align: right;
|
||||
user-select: none;
|
||||
min-width: 40px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.editor-container textarea {
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
resize: vertical;
|
||||
min-height: 500px;
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
tab-size: 4;
|
||||
}
|
||||
|
||||
.editor-container textarea:focus {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* 快捷键样式 */
|
||||
kbd {
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.25rem;
|
||||
color: #495057;
|
||||
display: inline-block;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
padding: 0.125rem 0.25rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Markdown 预览样式 */
|
||||
.markdown-preview {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: var(--text-primary);
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
padding: 1rem;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.markdown-preview h1, .markdown-preview h2, .markdown-preview h3,
|
||||
.markdown-preview h4, .markdown-preview h5, .markdown-preview h6 {
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.markdown-preview h1 { font-size: 2rem; border-bottom: 2px solid #eee; padding-bottom: 0.5rem; }
|
||||
.markdown-preview h2 { font-size: 1.5rem; border-bottom: 1px solid #eee; padding-bottom: 0.3rem; }
|
||||
.markdown-preview h3 { font-size: 1.25rem; }
|
||||
.markdown-preview h4 { font-size: 1rem; }
|
||||
.markdown-preview h5 { font-size: 0.875rem; }
|
||||
.markdown-preview h6 { font-size: 0.75rem; }
|
||||
|
||||
.markdown-preview p {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.markdown-preview ul, .markdown-preview ol {
|
||||
margin-bottom: 1rem;
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
.markdown-preview li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.markdown-preview code {
|
||||
background-color: rgba(13, 110, 253, 0.1);
|
||||
color: #d63384;
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
.markdown-preview pre {
|
||||
background-color: #212529;
|
||||
color: #f8f9fa;
|
||||
padding: 1rem;
|
||||
border-radius: 0.375rem;
|
||||
overflow-x: auto;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.markdown-preview pre code {
|
||||
background-color: transparent;
|
||||
color: inherit;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.markdown-preview blockquote {
|
||||
border-left: 4px solid var(--primary-color);
|
||||
padding-left: 1rem;
|
||||
margin: 1rem 0;
|
||||
color: var(--text-secondary);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.markdown-preview table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.markdown-preview th, .markdown-preview td {
|
||||
border: 1px solid #dee2e6;
|
||||
padding: 0.75rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.markdown-preview th {
|
||||
background-color: #f8f9fa;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 文件编辑状态指示器 */
|
||||
.edit-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.edit-status.modified {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
.edit-status.saved {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.edit-status.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@ -814,6 +982,99 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文件编辑模态框 -->
|
||||
<div class="modal fade" id="editFileModal" tabindex="-1" aria-labelledby="editFileModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="editFileModalLabel">
|
||||
<i class="bi bi-file-earmark-code me-2"></i>编辑文件
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="editFileName" class="form-label">文件路径:</label>
|
||||
<input type="text" class="form-control" id="editFileName" readonly />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<label for="fileContent" class="form-label">文件内容:</label>
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="toggleWordWrap()">
|
||||
<i class="bi bi-text-wrap me-1"></i>换行
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="toggleLineNumbers()">
|
||||
<i class="bi bi-list-ol me-1"></i>行号
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="formatMarkdown()">
|
||||
<i class="bi bi-markdown me-1"></i>格式化
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="insertTab()">
|
||||
<i class="bi bi-indent me-1"></i>Tab
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="editor-container">
|
||||
<div class="editor-wrapper">
|
||||
<div class="line-numbers" id="lineNumbers" style="display: none;">1</div>
|
||||
<textarea class="form-control font-monospace" id="fileContent" rows="25" placeholder="文件内容将在这里显示..." style="min-height: 500px; resize: vertical;" spellcheck="false"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="text-muted">
|
||||
<small id="editFileInfo">准备编辑...</small>
|
||||
</div>
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-outline-warning" onclick="previewFile()">
|
||||
<i class="bi bi-eye me-1"></i>预览
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" onclick="resetContent()">
|
||||
<i class="bi bi-arrow-clockwise me-1"></i>重置
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="text-start">
|
||||
<small class="text-muted">
|
||||
快捷键: <kbd>Ctrl+S</kbd> 保存 | <kbd>Ctrl+P</kbd> 预览 | <kbd>Ctrl+L</kbd> 行号 | <kbd>Ctrl+W</kbd> 换行 | <kbd>Ctrl+/</kbd> 注释 | <kbd>Ctrl+D</kbd> 复制行 | <kbd>Tab</kbd> 缩进
|
||||
</small>
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||||
<button type="button" class="btn btn-primary" onclick="saveFile()">
|
||||
<i class="bi bi-save me-1"></i>保存文件
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文件预览模态框 -->
|
||||
<div class="modal fade" id="previewFileModal" tabindex="-1" aria-labelledby="previewFileModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="previewFileModalLabel">
|
||||
<i class="bi bi-eye me-2"></i>文件预览
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="previewContent" class="markdown-preview">
|
||||
<!-- Markdown 预览内容将在这里显示 -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 全局变量
|
||||
const API_BASE = `${window.location.protocol}//${window.location.host}/api/v1`;
|
||||
@ -828,6 +1089,12 @@
|
||||
let currentItemToRename = '';
|
||||
let batchMode = false;
|
||||
|
||||
// 文件编辑相关变量
|
||||
let currentEditingFile = '';
|
||||
let originalFileContent = '';
|
||||
let fileEditStatus = 'ready'; // ready, modified, saving, error
|
||||
let showLineNumbers = false;
|
||||
|
||||
// 初始化
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 检查登录状态
|
||||
@ -1503,13 +1770,15 @@
|
||||
// 加载文件列表
|
||||
async function loadFileList(path = '') {
|
||||
try {
|
||||
const response = await fetch(`${FILE_API_BASE}/list?path=${encodeURIComponent(path)}&recursive=false`);
|
||||
// 清理路径,确保发送相对路径
|
||||
const cleanPath = sanitizePath(path);
|
||||
const response = await fetch(`${FILE_API_BASE}/list?path=${encodeURIComponent(cleanPath)}&recursive=false`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
currentPath = path;
|
||||
currentPath = cleanPath;
|
||||
renderFileList(data.items);
|
||||
updateBreadcrumb(path);
|
||||
updateBreadcrumb(cleanPath);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载文件列表失败:', error);
|
||||
@ -1542,11 +1811,31 @@
|
||||
}
|
||||
return a.name.localeCompare(b.name);
|
||||
}).forEach(item => {
|
||||
const fileItem = createFileItem(item);
|
||||
// 确保路径是相对路径格式
|
||||
const cleanItem = {
|
||||
...item,
|
||||
path: sanitizePath(item.path)
|
||||
};
|
||||
const fileItem = createFileItem(cleanItem);
|
||||
fileList.appendChild(fileItem);
|
||||
});
|
||||
}
|
||||
|
||||
// 清理路径,确保使用相对路径
|
||||
function sanitizePath(path) {
|
||||
// 如果是绝对路径,转换为相对路径
|
||||
if (path.startsWith('/')) {
|
||||
const pathParts = path.split('/');
|
||||
const qwenAgentIndex = pathParts.indexOf('qwen-agent');
|
||||
if (qwenAgentIndex !== -1) {
|
||||
return pathParts.slice(qwenAgentIndex + 1).join('/');
|
||||
}
|
||||
// 如果找不到 qwen-agent,取最后两部分
|
||||
return pathParts.slice(-2).join('/');
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
// 创建文件项
|
||||
function createFileItem(item) {
|
||||
const div = document.createElement('div');
|
||||
@ -1572,11 +1861,15 @@
|
||||
<div class="file-actions">
|
||||
<div class="btn-group" role="group">
|
||||
${item.type === 'file' ?
|
||||
`<button class="btn btn-outline-primary btn-sm" onclick="downloadFile('${item.path}')"><i class="bi bi-download"></i></button>` :
|
||||
`<button class="btn btn-outline-primary btn-sm" onclick="downloadFolderAsZip('${item.path}')"><i class="bi bi-file-zip"></i></button>`
|
||||
(isEditableFile(item.name) ?
|
||||
`<button class="btn btn-outline-success btn-sm" onclick="editFile('${item.path}')" title="编辑文件"><i class="bi bi-file-earmark-code"></i></button>
|
||||
<button class="btn btn-outline-primary btn-sm" onclick="downloadFile('${item.path}')" title="下载"><i class="bi bi-download"></i></button>` :
|
||||
`<button class="btn btn-outline-primary btn-sm" onclick="downloadFile('${item.path}')" title="下载"><i class="bi bi-download"></i></button>`
|
||||
) :
|
||||
`<button class="btn btn-outline-primary btn-sm" onclick="downloadFolderAsZip('${item.path}')" title="压缩下载"><i class="bi bi-file-zip"></i></button>`
|
||||
}
|
||||
<button class="btn btn-outline-secondary btn-sm" onclick="showRenameModal('${item.path}')"><i class="bi bi-pencil"></i></button>
|
||||
<button class="btn btn-outline-danger btn-sm" onclick="deleteItem('${item.path}')"><i class="bi bi-trash"></i></button>
|
||||
<button class="btn btn-outline-secondary btn-sm" onclick="showRenameModal('${item.path}')" title="重命名"><i class="bi bi-pencil"></i></button>
|
||||
<button class="btn btn-outline-danger btn-sm" onclick="deleteItem('${item.path}')" title="删除"><i class="bi bi-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@ -1697,7 +1990,8 @@
|
||||
// 搜索文件
|
||||
async function searchFiles(query) {
|
||||
try {
|
||||
const response = await fetch(`${FILE_API_BASE}/search?query=${encodeURIComponent(query)}&path=${encodeURIComponent(currentPath)}`);
|
||||
const cleanPath = sanitizePath(currentPath);
|
||||
const response = await fetch(`${FILE_API_BASE}/search?query=${encodeURIComponent(query)}&path=${encodeURIComponent(cleanPath)}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
@ -2219,9 +2513,598 @@
|
||||
showMessage('重命名失败', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// ========== 文件编辑功能 ==========
|
||||
|
||||
// 判断文件是否可编辑
|
||||
function isEditableFile(fileName) {
|
||||
const editableExtensions = [
|
||||
'md', 'txt', 'json', 'xml', 'yaml', 'yml',
|
||||
'js', 'ts', 'jsx', 'tsx', 'html', 'css', 'scss', 'sass',
|
||||
'py', 'java', 'cpp', 'c', 'h', 'hpp', 'cs', 'php', 'rb', 'go',
|
||||
'sh', 'bash', 'zsh', 'fish', 'ps1', 'bat', 'cmd',
|
||||
'sql', 'log', 'conf', 'ini', 'cfg', 'toml', 'env',
|
||||
'gitignore', 'dockerfile', 'makefile', 'readme', 'license'
|
||||
];
|
||||
|
||||
const ext = fileName.split('.').pop().toLowerCase();
|
||||
const name = fileName.toLowerCase();
|
||||
|
||||
// 检查扩展名
|
||||
if (editableExtensions.includes(ext)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查特殊文件名
|
||||
const specialFiles = ['readme', 'license', 'dockerfile', 'makefile', 'gitignore'];
|
||||
if (specialFiles.some(special => name.includes(special))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// 编辑文件
|
||||
async function editFile(filePath) {
|
||||
try {
|
||||
currentEditingFile = filePath;
|
||||
fileEditStatus = 'loading';
|
||||
|
||||
// 显示编辑模态框
|
||||
const modal = new bootstrap.Modal(document.getElementById('editFileModal'));
|
||||
|
||||
// 设置文件路径
|
||||
document.getElementById('editFileName').value = filePath;
|
||||
|
||||
// 显示加载状态
|
||||
document.getElementById('fileContent').value = '加载文件内容中...';
|
||||
document.getElementById('editFileInfo').innerHTML = '<span class="edit-status"><i class="bi bi-hourglass-split me-1"></i>正在加载...</span>';
|
||||
|
||||
modal.show();
|
||||
|
||||
// 获取文件内容
|
||||
const response = await fetch(`${FILE_API_BASE}/read?path=${encodeURIComponent(filePath)}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
originalFileContent = data.content || '';
|
||||
document.getElementById('fileContent').value = originalFileContent;
|
||||
|
||||
// 更新文件信息
|
||||
const size = data.size ? formatFileSize(data.size) : '未知';
|
||||
const modified = data.modified ? formatDate(data.modified * 1000) : '未知';
|
||||
document.getElementById('editFileInfo').innerHTML =
|
||||
`<span class="edit-status saved"><i class="bi bi-check-circle me-1"></i>已加载 • 大小: ${size} • 修改时间: ${modified}</span>`;
|
||||
|
||||
fileEditStatus = 'ready';
|
||||
|
||||
// 监听内容变化
|
||||
setupContentChangeListener();
|
||||
|
||||
} else {
|
||||
throw new Error(data.detail || '读取文件失败');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载文件失败:', error);
|
||||
document.getElementById('fileContent').value = `加载文件失败: ${error.message}`;
|
||||
document.getElementById('editFileInfo').innerHTML =
|
||||
`<span class="edit-status error"><i class="bi bi-exclamation-triangle me-1"></i>加载失败</span>`;
|
||||
fileEditStatus = 'error';
|
||||
showMessage(`加载文件失败: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 设置内容变化监听器
|
||||
function setupContentChangeListener() {
|
||||
const textarea = document.getElementById('fileContent');
|
||||
|
||||
// 移除旧的监听器(如果存在)
|
||||
textarea.removeEventListener('input', handleContentChange);
|
||||
textarea.removeEventListener('scroll', handleEditorScroll);
|
||||
textarea.removeEventListener('keydown', handleEditorKeydown);
|
||||
|
||||
// 添加新的监听器
|
||||
textarea.addEventListener('input', handleContentChange);
|
||||
textarea.addEventListener('scroll', handleEditorScroll);
|
||||
textarea.addEventListener('keydown', handleEditorKeydown);
|
||||
|
||||
// 初始化行号
|
||||
updateLineNumbers();
|
||||
}
|
||||
|
||||
// 处理内容变化
|
||||
function handleContentChange() {
|
||||
const currentContent = document.getElementById('fileContent').value;
|
||||
const isModified = currentContent !== originalFileContent;
|
||||
|
||||
// 更新行号
|
||||
updateLineNumbers();
|
||||
|
||||
if (isModified && fileEditStatus === 'ready') {
|
||||
fileEditStatus = 'modified';
|
||||
document.getElementById('editFileInfo').innerHTML =
|
||||
'<span class="edit-status modified"><i class="bi bi-pencil-square me-1"></i>已修改</span>';
|
||||
} else if (!isModified && fileEditStatus === 'modified') {
|
||||
fileEditStatus = 'ready';
|
||||
const size = formatFileSize(new Blob([currentContent]).size);
|
||||
document.getElementById('editFileInfo').innerHTML =
|
||||
`<span class="edit-status saved"><i class="bi bi-check-circle me-1"></i>已保存 • 大小: ${size}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理编辑器滚动
|
||||
function handleEditorScroll() {
|
||||
const textarea = document.getElementById('fileContent');
|
||||
const lineNumbers = document.getElementById('lineNumbers');
|
||||
|
||||
if (showLineNumbers && lineNumbers) {
|
||||
lineNumbers.scrollTop = textarea.scrollTop;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理编辑器键盘事件
|
||||
function handleEditorKeydown(e) {
|
||||
// Tab 键处理
|
||||
if (e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
insertTab();
|
||||
return;
|
||||
}
|
||||
|
||||
// Ctrl+Z 撤销 (浏览器原生支持)
|
||||
// Ctrl+Y 重做 (浏览器原生支持)
|
||||
|
||||
// 其他快捷键在全局事件处理器中处理
|
||||
}
|
||||
|
||||
// 更新行号
|
||||
function updateLineNumbers() {
|
||||
if (!showLineNumbers) return;
|
||||
|
||||
const textarea = document.getElementById('fileContent');
|
||||
const lineNumbers = document.getElementById('lineNumbers');
|
||||
|
||||
if (!lineNumbers) return;
|
||||
|
||||
const lines = textarea.value.split('\n').length;
|
||||
let lineNumbersHtml = '';
|
||||
|
||||
for (let i = 1; i <= lines; i++) {
|
||||
lineNumbersHtml += i + '\n';
|
||||
}
|
||||
|
||||
lineNumbers.textContent = lineNumbersHtml;
|
||||
}
|
||||
|
||||
// 切换行号显示
|
||||
function toggleLineNumbers() {
|
||||
showLineNumbers = !showLineNumbers;
|
||||
const lineNumbers = document.getElementById('lineNumbers');
|
||||
const textarea = document.getElementById('fileContent');
|
||||
|
||||
if (showLineNumbers) {
|
||||
lineNumbers.style.display = 'block';
|
||||
updateLineNumbers();
|
||||
showMessage('已启用行号', 'info');
|
||||
} else {
|
||||
lineNumbers.style.display = 'none';
|
||||
showMessage('已禁用行号', 'info');
|
||||
}
|
||||
}
|
||||
|
||||
// 插入 Tab
|
||||
function insertTab() {
|
||||
const textarea = document.getElementById('fileContent');
|
||||
const start = textarea.selectionStart;
|
||||
const end = textarea.selectionEnd;
|
||||
const value = textarea.value;
|
||||
|
||||
// 插入4个空格作为Tab
|
||||
const tabText = ' ';
|
||||
|
||||
textarea.value = value.substring(0, start) + tabText + value.substring(end);
|
||||
textarea.selectionStart = textarea.selectionEnd = start + tabText.length;
|
||||
|
||||
// 触发内容变化事件
|
||||
handleContentChange();
|
||||
|
||||
// 设置焦点
|
||||
textarea.focus();
|
||||
}
|
||||
|
||||
// 保存文件
|
||||
async function saveFile() {
|
||||
if (!currentEditingFile) {
|
||||
showMessage('没有正在编辑的文件', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
fileEditStatus = 'saving';
|
||||
|
||||
const content = document.getElementById('fileContent').value;
|
||||
|
||||
// 显示保存状态
|
||||
document.getElementById('editFileInfo').innerHTML =
|
||||
'<span class="edit-status"><i class="bi bi-hourglass-split me-1"></i>正在保存...</span>';
|
||||
|
||||
// 禁用保存按钮
|
||||
const saveButton = document.querySelector('#editFileModal .btn-primary');
|
||||
const originalText = saveButton.innerHTML;
|
||||
saveButton.disabled = true;
|
||||
saveButton.innerHTML = '<i class="bi bi-hourglass-split me-1"></i>保存中...';
|
||||
|
||||
// 发送保存请求
|
||||
const response = await fetch(`${FILE_API_BASE}/save`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
path: currentEditingFile,
|
||||
content: content
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
originalFileContent = content;
|
||||
fileEditStatus = 'ready';
|
||||
|
||||
const size = formatFileSize(new Blob([content]).size);
|
||||
document.getElementById('editFileInfo').innerHTML =
|
||||
`<span class="edit-status saved"><i class="bi bi-check-circle me-1"></i>已保存 • 大小: ${size} • ${new Date().toLocaleTimeString()}</span>`;
|
||||
|
||||
showMessage('文件保存成功', 'success');
|
||||
|
||||
} else {
|
||||
throw new Error(data.detail || '保存文件失败');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('保存文件失败:', error);
|
||||
fileEditStatus = 'error';
|
||||
document.getElementById('editFileInfo').innerHTML =
|
||||
`<span class="edit-status error"><i class="bi bi-exclamation-triangle me-1"></i>保存失败: ${error.message}</span>`;
|
||||
showMessage(`保存文件失败: ${error.message}`, 'error');
|
||||
} finally {
|
||||
// 恢复保存按钮
|
||||
const saveButton = document.querySelector('#editFileModal .btn-primary');
|
||||
saveButton.disabled = false;
|
||||
saveButton.innerHTML = '<i class="bi bi-save me-1"></i>保存文件';
|
||||
}
|
||||
}
|
||||
|
||||
// 重置内容
|
||||
function resetContent() {
|
||||
if (fileEditStatus === 'modified') {
|
||||
if (!confirm('确定要重置文件内容吗?所有未保存的修改将丢失。')) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('fileContent').value = originalFileContent;
|
||||
fileEditStatus = 'ready';
|
||||
|
||||
const size = formatFileSize(new Blob([originalFileContent]).size);
|
||||
document.getElementById('editFileInfo').innerHTML =
|
||||
`<span class="edit-status saved"><i class="bi bi-check-circle me-1"></i>已重置 • 大小: ${size}</span>`;
|
||||
}
|
||||
|
||||
// 切换自动换行
|
||||
function toggleWordWrap() {
|
||||
const textarea = document.getElementById('fileContent');
|
||||
const currentWrap = textarea.style.whiteSpace || 'pre-wrap';
|
||||
|
||||
if (currentWrap === 'pre-wrap') {
|
||||
textarea.style.whiteSpace = 'pre';
|
||||
textarea.style.overflowX = 'auto';
|
||||
} else {
|
||||
textarea.style.whiteSpace = 'pre-wrap';
|
||||
textarea.style.overflowX = 'hidden';
|
||||
}
|
||||
|
||||
showMessage(`已${currentWrap === 'pre-wrap' ? '禁用' : '启用'}自动换行`, 'info');
|
||||
}
|
||||
|
||||
// 简单的 Markdown 格式化函数
|
||||
function formatMarkdown() {
|
||||
const textarea = document.getElementById('fileContent');
|
||||
let content = textarea.value;
|
||||
|
||||
// 基本的 Markdown 格式化
|
||||
content = content.replace(/\r\n/g, '\n'); // 统一换行符
|
||||
content = content.replace(/\n{3,}/g, '\n\n'); // 减少多余空行
|
||||
content = content.replace(/([^\n])\n([^\n#*-])/g, '$1\n\n$2'); // 在段落间添加空行
|
||||
|
||||
textarea.value = content;
|
||||
handleContentChange(); // 更新修改状态
|
||||
|
||||
showMessage('已应用基本格式化', 'info');
|
||||
}
|
||||
|
||||
// 预览文件
|
||||
function previewFile() {
|
||||
const content = document.getElementById('fileContent').value;
|
||||
const previewContainer = document.getElementById('previewContent');
|
||||
|
||||
// Markdown 到 HTML 转换
|
||||
let html = markdownToHtml(content);
|
||||
previewContainer.innerHTML = html;
|
||||
|
||||
// 应用代码高亮
|
||||
if (typeof Prism !== 'undefined') {
|
||||
Prism.highlightAllUnder(previewContainer);
|
||||
}
|
||||
|
||||
// 显示预览模态框
|
||||
const modal = new bootstrap.Modal(document.getElementById('previewFileModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
// 改进的 Markdown 解析器,支持代码高亮
|
||||
function markdownToHtml(markdown) {
|
||||
if (!markdown) return '<p>空文件</p>';
|
||||
|
||||
let html = markdown;
|
||||
|
||||
// 代码块(支持语言标识)
|
||||
html = html.replace(/```(\w+)?\n([\s\S]*?)```/g, function(match, lang, code) {
|
||||
const language = lang || 'text';
|
||||
const cleanCode = code.trim();
|
||||
return `<pre><code class="language-${language}">${escapeHtml(cleanCode)}</code></pre>`;
|
||||
});
|
||||
|
||||
// 标题
|
||||
html = html.replace(/^#### (.*$)/gim, '<h4>$1</h4>');
|
||||
html = html.replace(/^### (.*$)/gim, '<h3>$1</h3>');
|
||||
html = html.replace(/^## (.*$)/gim, '<h2>$1</h2>');
|
||||
html = html.replace(/^# (.*$)/gim, '<h1>$1</h1>');
|
||||
|
||||
// 粗体和斜体
|
||||
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
||||
html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
|
||||
|
||||
// 行内代码
|
||||
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
|
||||
|
||||
// 链接
|
||||
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
|
||||
|
||||
// 引用块
|
||||
html = html.replace(/^> (.+)$/gim, '<blockquote>$1</blockquote>');
|
||||
|
||||
// 水平分割线
|
||||
html = html.replace(/^---$/gim, '<hr>');
|
||||
|
||||
// 有序列表
|
||||
html = html.replace(/^\d+\. (.+)$/gim, '<li>$1</li>');
|
||||
|
||||
// 无序列表
|
||||
html = html.replace(/^[\*\-\+] (.+)$/gim, '<li>$1</li>');
|
||||
|
||||
// 合并连续列表项
|
||||
html = html.replace(/(<li>.*<\/li>)\s*(<li>)/g, '$1$2');
|
||||
html = html.replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>');
|
||||
|
||||
// 段落处理
|
||||
const lines = html.split('\n');
|
||||
let result = [];
|
||||
let inParagraph = false;
|
||||
|
||||
for (let line of lines) {
|
||||
const trimmed = line.trim();
|
||||
|
||||
// 如果是空行,结束当前段落
|
||||
if (!trimmed) {
|
||||
if (inParagraph) {
|
||||
result.push('</p>');
|
||||
inParagraph = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 如果是HTML标签块,直接添加
|
||||
if (trimmed.match(/^<(h[1-6]|ul|ol|li|pre|blockquote|hr)/)) {
|
||||
if (inParagraph) {
|
||||
result.push('</p>');
|
||||
inParagraph = false;
|
||||
}
|
||||
result.push(trimmed);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 如果是段落结束标签
|
||||
if (trimmed.match(/^<\/(ul|ol|pre|blockquote)>/)) {
|
||||
result.push(trimmed);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 普通文本行
|
||||
if (!inParagraph) {
|
||||
result.push('<p>');
|
||||
inParagraph = true;
|
||||
}
|
||||
result.push(trimmed + ' ');
|
||||
}
|
||||
|
||||
if (inParagraph) {
|
||||
result.push('</p>');
|
||||
}
|
||||
|
||||
html = result.join('\n');
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
// HTML转义函数
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// 添加快捷键支持
|
||||
document.addEventListener('keydown', function(e) {
|
||||
// 只在编辑模态框打开时处理编辑器快捷键
|
||||
const editModal = bootstrap.Modal.getInstance(document.getElementById('editFileModal'));
|
||||
const isEditing = editModal && editModal._isShown;
|
||||
|
||||
// Ctrl+S 保存
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
||||
e.preventDefault();
|
||||
if (currentEditingFile && fileEditStatus !== 'saving') {
|
||||
saveFile();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Ctrl+W 切换换行
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'w') {
|
||||
e.preventDefault();
|
||||
if (currentEditingFile) {
|
||||
toggleWordWrap();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Ctrl+L 切换行号
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'l') {
|
||||
e.preventDefault();
|
||||
if (currentEditingFile) {
|
||||
toggleLineNumbers();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Ctrl+P 预览
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'p') {
|
||||
e.preventDefault();
|
||||
if (currentEditingFile) {
|
||||
previewFile();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Ctrl+Shift+F 格式化
|
||||
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'F') {
|
||||
e.preventDefault();
|
||||
if (currentEditingFile) {
|
||||
formatMarkdown();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Escape 关闭预览
|
||||
if (e.key === 'Escape') {
|
||||
const previewModal = bootstrap.Modal.getInstance(document.getElementById('previewFileModal'));
|
||||
if (previewModal) {
|
||||
previewModal.hide();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 编辑器快捷键(只在编辑时处理)
|
||||
if (isEditing && e.target.id === 'fileContent') {
|
||||
// Tab 键已在 handleEditorKeydown 中处理
|
||||
|
||||
// Ctrl+/ 注释/取消注释
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === '/') {
|
||||
e.preventDefault();
|
||||
toggleComment();
|
||||
return;
|
||||
}
|
||||
|
||||
// Ctrl+D 复制行
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'd') {
|
||||
e.preventDefault();
|
||||
duplicateLine();
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 切换注释(简单实现)
|
||||
function toggleComment() {
|
||||
const textarea = document.getElementById('fileContent');
|
||||
const start = textarea.selectionStart;
|
||||
const end = textarea.selectionEnd;
|
||||
const value = textarea.value;
|
||||
|
||||
// 获取选中的行
|
||||
const lines = value.substring(0, start).split('\n').length - 1;
|
||||
const endLines = value.substring(0, end).split('\n').length - 1;
|
||||
|
||||
const allLines = value.split('\n');
|
||||
let hasComment = true;
|
||||
|
||||
// 检查是否所有行都已注释
|
||||
for (let i = lines; i <= endLines; i++) {
|
||||
if (allLines[i] && !allLines[i].trim().startsWith('#')) {
|
||||
hasComment = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 切换注释
|
||||
if (hasComment) {
|
||||
// 取消注释
|
||||
for (let i = lines; i <= endLines; i++) {
|
||||
if (allLines[i] && allLines[i].trim().startsWith('#')) {
|
||||
allLines[i] = allLines[i].replace(/^(\s*)#\s?/, '$1');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 添加注释
|
||||
for (let i = lines; i <= endLines; i++) {
|
||||
if (allLines[i] && !allLines[i].trim().startsWith('#')) {
|
||||
allLines[i] = '# ' + allLines[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
textarea.value = allLines.join('\n');
|
||||
handleContentChange();
|
||||
textarea.focus();
|
||||
}
|
||||
|
||||
// 复制当前行
|
||||
function duplicateLine() {
|
||||
const textarea = document.getElementById('fileContent');
|
||||
const start = textarea.selectionStart;
|
||||
const end = textarea.selectionEnd;
|
||||
const value = textarea.value;
|
||||
|
||||
// 找到当前行的开始和结束
|
||||
const lineStart = value.lastIndexOf('\n', start - 1) + 1;
|
||||
const lineEnd = value.indexOf('\n', end);
|
||||
const actualLineEnd = lineEnd === -1 ? value.length : lineEnd;
|
||||
|
||||
const currentLine = value.substring(lineStart, actualLineEnd);
|
||||
const newContent = value.substring(0, actualLineEnd) + '\n' + currentLine + value.substring(actualLineEnd);
|
||||
|
||||
textarea.value = newContent;
|
||||
const newCursorPos = actualLineEnd + currentLine.length + 1;
|
||||
textarea.selectionStart = textarea.selectionEnd = newCursorPos;
|
||||
|
||||
handleContentChange();
|
||||
textarea.focus();
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Bootstrap JS Bundle -->
|
||||
<!-- Prism.js for code highlighting -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css" rel="stylesheet" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-core.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js"></script>
|
||||
|
||||
<!-- Bootstrap JS Bundle -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -22,8 +22,46 @@ import math
|
||||
|
||||
router = APIRouter(prefix="/api/v1/file-manager", tags=["file_management"])
|
||||
|
||||
PROJECTS_DIR = Path("projects")
|
||||
PROJECTS_DIR.mkdir(exist_ok=True)
|
||||
# 支持多个目录:projects 和 prompt
|
||||
PROJECTS_DIR = Path(".")
|
||||
SUPPORTED_DIRECTORIES = ["projects", "prompt"]
|
||||
|
||||
|
||||
def resolve_path(path: str) -> Path:
|
||||
"""解析路径,确保在允许的目录内"""
|
||||
# 如果路径为空,默认显示支持的目录列表
|
||||
if not path:
|
||||
return PROJECTS_DIR
|
||||
|
||||
# 确定路径是否以支持的目录开头
|
||||
path_parts = Path(path).parts
|
||||
|
||||
if not path_parts or path_parts[0] not in SUPPORTED_DIRECTORIES:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"路径必须以以下目录之一开头: {', '.join(SUPPORTED_DIRECTORIES)}"
|
||||
)
|
||||
|
||||
target_path = PROJECTS_DIR / path
|
||||
|
||||
# 规范化路径,防止目录遍历攻击
|
||||
try:
|
||||
resolved_path = target_path.resolve()
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="无效路径")
|
||||
|
||||
# 检查路径是否在允许的目录内
|
||||
try:
|
||||
allowed_root = PROJECTS_DIR.resolve()
|
||||
# 检查解析后的路径是否仍在允许的目录内
|
||||
try:
|
||||
resolved_path.relative_to(allowed_root)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=403, detail="访问被拒绝")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=403, detail="访问被拒绝")
|
||||
|
||||
return resolved_path
|
||||
|
||||
|
||||
@router.get("/list")
|
||||
@ -36,7 +74,30 @@ async def list_files(path: str = "", recursive: bool = False):
|
||||
recursive: 是否递归列出所有子目录
|
||||
"""
|
||||
try:
|
||||
target_path = PROJECTS_DIR / path
|
||||
# 如果路径为空,返回支持的目录列表
|
||||
if not path:
|
||||
items = []
|
||||
for dir_name in SUPPORTED_DIRECTORIES:
|
||||
dir_path = PROJECTS_DIR / dir_name
|
||||
if dir_path.exists() and dir_path.is_dir():
|
||||
stat = dir_path.stat()
|
||||
items.append({
|
||||
"name": dir_name,
|
||||
"path": dir_name,
|
||||
"type": "directory",
|
||||
"size": 0,
|
||||
"modified": stat.st_mtime,
|
||||
"created": stat.st_ctime
|
||||
})
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"path": "",
|
||||
"items": items,
|
||||
"total": len(items)
|
||||
}
|
||||
|
||||
target_path = resolve_path(path)
|
||||
|
||||
if not target_path.exists():
|
||||
raise HTTPException(status_code=404, detail="路径不存在")
|
||||
@ -52,7 +113,13 @@ async def list_files(path: str = "", recursive: bool = False):
|
||||
if item.name.startswith('.'):
|
||||
continue
|
||||
|
||||
relative_path = item.relative_to(base_path)
|
||||
# 计算相对于项目根目录的相对路径
|
||||
try:
|
||||
relative_path = item.relative_to(PROJECTS_DIR)
|
||||
except ValueError:
|
||||
# 如果无法计算相对路径,使用绝对路径
|
||||
relative_path = item
|
||||
|
||||
stat = item.stat()
|
||||
|
||||
item_info = {
|
||||
@ -74,7 +141,7 @@ async def list_files(path: str = "", recursive: bool = False):
|
||||
pass
|
||||
return items
|
||||
|
||||
items = scan_directory(target_path)
|
||||
items = scan_directory(target_path, Path("."))
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
@ -94,13 +161,15 @@ async def upload_file(file: UploadFile = File(...), path: str = Form("")):
|
||||
|
||||
Args:
|
||||
file: 上传的文件
|
||||
path: 目标路径(相对于projects目录)
|
||||
path: 目标路径(相对于支持目录)
|
||||
"""
|
||||
try:
|
||||
target_dir = PROJECTS_DIR / path
|
||||
target_dir.mkdir(parents=True, exist_ok=True)
|
||||
target_path = resolve_path(path) if path else resolve_path("projects")
|
||||
if not target_path.exists() or not target_path.is_dir():
|
||||
target_path = resolve_path("projects")
|
||||
target_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
file_path = target_dir / file.filename
|
||||
file_path = target_path / file.filename
|
||||
|
||||
# 如果文件已存在,检查是否覆盖
|
||||
if file_path.exists():
|
||||
@ -132,7 +201,7 @@ async def download_file(file_path: str):
|
||||
file_path: 文件相对路径
|
||||
"""
|
||||
try:
|
||||
target_path = PROJECTS_DIR / file_path
|
||||
target_path = resolve_path(file_path)
|
||||
|
||||
if not target_path.exists():
|
||||
raise HTTPException(status_code=404, detail="文件不存在")
|
||||
@ -164,7 +233,7 @@ async def delete_item(path: str):
|
||||
path: 要删除的路径
|
||||
"""
|
||||
try:
|
||||
target_path = PROJECTS_DIR / path
|
||||
target_path = resolve_path(path)
|
||||
|
||||
if not target_path.exists():
|
||||
raise HTTPException(status_code=404, detail="路径不存在")
|
||||
@ -194,7 +263,7 @@ async def create_folder(path: str, name: str):
|
||||
name: 新文件夹名称
|
||||
"""
|
||||
try:
|
||||
parent_path = PROJECTS_DIR / path
|
||||
parent_path = resolve_path(path) if path else resolve_path("projects")
|
||||
parent_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
new_folder = parent_path / name
|
||||
@ -224,7 +293,7 @@ async def rename_item(old_path: str, new_name: str):
|
||||
new_name: 新名称
|
||||
"""
|
||||
try:
|
||||
old_full_path = PROJECTS_DIR / old_path
|
||||
old_full_path = resolve_path(old_path)
|
||||
|
||||
if not old_full_path.exists():
|
||||
raise HTTPException(status_code=404, detail="文件或目录不存在")
|
||||
@ -236,11 +305,16 @@ async def rename_item(old_path: str, new_name: str):
|
||||
|
||||
old_full_path.rename(new_full_path)
|
||||
|
||||
try:
|
||||
new_relative_path = new_full_path.relative_to(PROJECTS_DIR)
|
||||
except ValueError:
|
||||
new_relative_path = new_full_path
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "重命名成功",
|
||||
"old_path": old_path,
|
||||
"new_path": str(new_full_path.relative_to(PROJECTS_DIR))
|
||||
"new_path": str(new_relative_path)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
@ -257,8 +331,8 @@ async def move_item(source_path: str, target_path: str):
|
||||
target_path: 目标路径
|
||||
"""
|
||||
try:
|
||||
source_full_path = PROJECTS_DIR / source_path
|
||||
target_full_path = PROJECTS_DIR / target_path
|
||||
source_full_path = resolve_path(source_path)
|
||||
target_full_path = resolve_path(target_path)
|
||||
|
||||
if not source_full_path.exists():
|
||||
raise HTTPException(status_code=404, detail="源文件或目录不存在")
|
||||
@ -289,8 +363,8 @@ async def copy_item(source_path: str, target_path: str):
|
||||
target_path: 目标路径
|
||||
"""
|
||||
try:
|
||||
source_full_path = PROJECTS_DIR / source_path
|
||||
target_full_path = PROJECTS_DIR / target_path
|
||||
source_full_path = resolve_path(source_path)
|
||||
target_full_path = resolve_path(target_path)
|
||||
|
||||
if not source_full_path.exists():
|
||||
raise HTTPException(status_code=404, detail="源文件或目录不存在")
|
||||
@ -325,7 +399,7 @@ async def search_files(query: str, path: str = "", file_type: Optional[str] = No
|
||||
file_type: 文件类型过滤
|
||||
"""
|
||||
try:
|
||||
search_path = PROJECTS_DIR / path
|
||||
search_path = resolve_path(path) if path else Path(".")
|
||||
|
||||
if not search_path.exists():
|
||||
raise HTTPException(status_code=404, detail="搜索路径不存在")
|
||||
@ -341,17 +415,24 @@ async def search_files(query: str, path: str = "", file_type: Optional[str] = No
|
||||
# 检查文件名是否包含关键词
|
||||
if query.lower() in item.name.lower():
|
||||
# 检查文件类型过滤
|
||||
# 计算相对于项目根目录的相对路径
|
||||
try:
|
||||
relative_path = item.relative_to(PROJECTS_DIR)
|
||||
except ValueError:
|
||||
# 如果无法计算相对路径,使用绝对路径
|
||||
relative_path = item
|
||||
|
||||
if file_type:
|
||||
if item.suffix.lower() == file_type.lower():
|
||||
results.append({
|
||||
"name": item.name,
|
||||
"path": str(item.relative_to(PROJECTS_DIR)),
|
||||
"path": str(relative_path),
|
||||
"type": "directory" if item.is_dir() else "file"
|
||||
})
|
||||
else:
|
||||
results.append({
|
||||
"name": item.name,
|
||||
"path": str(item.relative_to(PROJECTS_DIR)),
|
||||
"path": str(relative_path),
|
||||
"type": "directory" if item.is_dir() else "file"
|
||||
})
|
||||
|
||||
@ -376,6 +457,160 @@ async def search_files(query: str, path: str = "", file_type: Optional[str] = No
|
||||
raise HTTPException(status_code=500, detail=f"搜索失败: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/read")
|
||||
async def read_file(path: str = Query(...)):
|
||||
"""
|
||||
读取文件内容
|
||||
|
||||
Args:
|
||||
path: 文件相对路径
|
||||
"""
|
||||
try:
|
||||
target_path = resolve_path(path)
|
||||
|
||||
if not target_path.exists():
|
||||
raise HTTPException(status_code=404, detail="文件不存在")
|
||||
|
||||
if not target_path.is_file():
|
||||
raise HTTPException(status_code=400, detail="路径不是文件")
|
||||
|
||||
# 检查文件大小,限制读取10MB以内的文件
|
||||
file_size = target_path.stat().st_size
|
||||
max_size = 10 * 1024 * 1024 # 10MB
|
||||
|
||||
if file_size > max_size:
|
||||
raise HTTPException(
|
||||
status_code=413,
|
||||
detail=f"文件过大 ({formatFileSize(file_size)}),最大支持 {formatFileSize(max_size)}"
|
||||
)
|
||||
|
||||
# 尝试不同的编码方式
|
||||
content = None
|
||||
encoding_used = None
|
||||
|
||||
for encoding in ['utf-8', 'gbk', 'gb2312', 'latin-1']:
|
||||
try:
|
||||
with open(target_path, 'r', encoding=encoding) as f:
|
||||
content = f.read()
|
||||
encoding_used = encoding
|
||||
break
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
|
||||
if content is None:
|
||||
# 如果所有编码都失败,尝试以二进制方式读取并转换为可打印字符
|
||||
try:
|
||||
with open(target_path, 'rb') as f:
|
||||
binary_content = f.read()
|
||||
content = binary_content.decode('utf-8', errors='replace')
|
||||
encoding_used = 'utf-8 (with replacement)'
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=f"无法读取文件内容: {str(e)}")
|
||||
|
||||
# 获取文件信息
|
||||
stat = target_path.stat()
|
||||
mime_type, _ = mimetypes.guess_type(str(target_path))
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"content": content,
|
||||
"path": path,
|
||||
"size": file_size,
|
||||
"modified": stat.st_mtime,
|
||||
"encoding": encoding_used,
|
||||
"mime_type": mime_type or "unknown"
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"读取文件失败: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/save")
|
||||
async def save_file(request: Dict[str, str]):
|
||||
"""
|
||||
保存文件内容
|
||||
|
||||
Args:
|
||||
request: 包含path和content字段的JSON对象
|
||||
"""
|
||||
try:
|
||||
logger.info(f"收到保存请求: {request}")
|
||||
|
||||
path = request.get("path")
|
||||
content = request.get("content")
|
||||
|
||||
logger.info(f"解析参数: path={path}, content length={len(content) if content else 'None'}")
|
||||
|
||||
if not path or content is None:
|
||||
raise HTTPException(status_code=400, detail="缺少必需的参数: path 和 content")
|
||||
|
||||
target_path = resolve_path(path)
|
||||
|
||||
# 确保父目录存在
|
||||
target_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 检查文件大小,限制保存5MB以内的内容
|
||||
content_size = len(content.encode('utf-8'))
|
||||
max_size = 5 * 1024 * 1024 # 5MB
|
||||
|
||||
if content_size > max_size:
|
||||
raise HTTPException(
|
||||
status_code=413,
|
||||
detail=f"内容过大 ({formatFileSize(content_size)}),最大支持 {formatFileSize(max_size)}"
|
||||
)
|
||||
|
||||
# 创建备份(如果文件已存在)
|
||||
backup_path = None
|
||||
if target_path.exists():
|
||||
import datetime
|
||||
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
backup_path = target_path.with_suffix(f".{timestamp}.bak")
|
||||
try:
|
||||
shutil.copy2(target_path, backup_path)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to create backup: {e}")
|
||||
|
||||
# 写入文件
|
||||
try:
|
||||
with open(target_path, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
except Exception as e:
|
||||
# 如果写入失败,恢复备份
|
||||
if backup_path and backup_path.exists():
|
||||
try:
|
||||
shutil.copy2(backup_path, target_path)
|
||||
backup_path.unlink() # 删除备份文件
|
||||
except:
|
||||
pass
|
||||
raise HTTPException(status_code=500, detail=f"保存文件失败: {str(e)}")
|
||||
|
||||
# 如果保存成功,删除备份文件
|
||||
if backup_path and backup_path.exists():
|
||||
try:
|
||||
backup_path.unlink()
|
||||
except:
|
||||
logger.warning(f"Failed to remove backup file: {backup_path}")
|
||||
|
||||
# 获取保存后的文件信息
|
||||
stat = target_path.stat()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "文件保存成功",
|
||||
"path": path,
|
||||
"size": stat.st_size,
|
||||
"modified": stat.st_mtime,
|
||||
"encoding": "utf-8"
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"保存文件失败: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/info/{file_path:path}")
|
||||
async def get_file_info(file_path: str):
|
||||
"""
|
||||
@ -385,7 +620,7 @@ async def get_file_info(file_path: str):
|
||||
file_path: 文件路径
|
||||
"""
|
||||
try:
|
||||
target_path = PROJECTS_DIR / file_path
|
||||
target_path = resolve_path(file_path)
|
||||
|
||||
if not target_path.exists():
|
||||
raise HTTPException(status_code=404, detail="路径不存在")
|
||||
@ -439,7 +674,7 @@ async def download_folder_as_zip(request: Dict[str, str]):
|
||||
if not folder_path:
|
||||
raise HTTPException(status_code=400, detail="路径不能为空")
|
||||
|
||||
target_path = PROJECTS_DIR / folder_path
|
||||
target_path = resolve_path(folder_path)
|
||||
|
||||
if not target_path.exists():
|
||||
raise HTTPException(status_code=404, detail="文件夹不存在")
|
||||
@ -541,7 +776,7 @@ async def download_multiple_items_as_zip(request: Dict[str, Any]):
|
||||
file_count = 0
|
||||
|
||||
for path in paths:
|
||||
target_path = PROJECTS_DIR / path
|
||||
target_path = resolve_path(path)
|
||||
|
||||
if not target_path.exists():
|
||||
continue # 跳过不存在的文件
|
||||
|
||||
Loading…
Reference in New Issue
Block a user