新增队列管理后台

This commit is contained in:
朱潮 2025-11-09 10:31:23 +08:00
parent d5a989860c
commit d00601af23
12 changed files with 2382 additions and 1058 deletions

2
.gitignore vendored
View File

@ -4,4 +4,4 @@ workspace
__pycache__
*/__pycache__
models
queue_data
projects/queue_data

View File

@ -25,7 +25,6 @@ COPY . .
RUN mkdir -p /app/projects
RUN mkdir -p /app/public
RUN mkdir -p /app/models
RUN mkdir -p /app/queue_data
# 下载sentence-transformers模型到models目录
RUN python -c "from sentence_transformers import SentenceTransformer; model = SentenceTransformer('sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2'); model.save('/app/models/paraphrase-multilingual-MiniLM-L12-v2')"

View File

@ -26,7 +26,6 @@ RUN pip install --no-cache-dir -i https://mirrors.aliyun.com/pypi/simple/ models
RUN mkdir -p /app/projects
RUN mkdir -p /app/public
RUN mkdir -p /app/models
RUN mkdir -p /app/queue_data
# 从modelscope下载sentence-transformers模型到models目录
RUN python -c "from modelscope import snapshot_download; model_dir = snapshot_download('sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2'); import shutil; shutil.move(model_dir, '/app/models/paraphrase-multilingual-MiniLM-L12-v2')"

View File

@ -571,7 +571,7 @@ qwen-agent/
│ └── tools/ # 工具定义文件
├── models/ # 模型文件
├── projects/ # 项目目录
├── queue_data/ # 队列数据
│ └── queue_data/ # 队列数据
├── public/ # 静态文件
├── embedding/ # 嵌入模型模块
├── prompt/ # 系统提示词

View File

@ -449,6 +449,152 @@ async def cleanup_tasks(older_than_days: int = 7):
raise HTTPException(status_code=500, detail=f"清理任务记录失败: {str(e)}")
@app.get("/api/v1/projects")
async def list_all_projects():
"""获取所有项目列表"""
try:
# 获取机器人项目projects/robot
robot_dir = "projects/robot"
robot_projects = []
if os.path.exists(robot_dir):
for item in os.listdir(robot_dir):
item_path = os.path.join(robot_dir, item)
if os.path.isdir(item_path):
try:
# 读取机器人配置文件
config_path = os.path.join(item_path, "robot_config.json")
config_data = {}
if os.path.exists(config_path):
with open(config_path, 'r', encoding='utf-8') as f:
config_data = json.load(f)
# 统计文件数量
file_count = 0
if os.path.exists(os.path.join(item_path, "dataset")):
for root, dirs, files in os.walk(os.path.join(item_path, "dataset")):
file_count += len(files)
robot_projects.append({
"id": item,
"name": config_data.get("name", item),
"type": "robot",
"status": config_data.get("status", "active"),
"file_count": file_count,
"config": config_data,
"created_at": os.path.getctime(item_path),
"updated_at": os.path.getmtime(item_path)
})
except Exception as e:
print(f"Error reading robot project {item}: {str(e)}")
robot_projects.append({
"id": item,
"name": item,
"type": "robot",
"status": "unknown",
"file_count": 0,
"created_at": os.path.getctime(item_path),
"updated_at": os.path.getmtime(item_path)
})
# 获取数据集projects/data
data_dir = "projects/data"
datasets = []
if os.path.exists(data_dir):
for item in os.listdir(data_dir):
item_path = os.path.join(data_dir, item)
if os.path.isdir(item_path):
try:
# 读取处理日志
log_path = os.path.join(item_path, "processing_log.json")
log_data = {}
if os.path.exists(log_path):
with open(log_path, 'r', encoding='utf-8') as f:
log_data = json.load(f)
# 统计文件数量
file_count = 0
for root, dirs, files in os.walk(item_path):
file_count += len([f for f in files if not f.endswith('.pkl')])
# 获取状态
status = "active"
if log_data.get("status"):
status = log_data["status"]
elif os.path.exists(os.path.join(item_path, "processed")):
status = "completed"
datasets.append({
"id": item,
"name": f"数据集 - {item[:8]}...",
"type": "dataset",
"status": status,
"file_count": file_count,
"log_data": log_data,
"created_at": os.path.getctime(item_path),
"updated_at": os.path.getmtime(item_path)
})
except Exception as e:
print(f"Error reading dataset {item}: {str(e)}")
datasets.append({
"id": item,
"name": f"数据集 - {item[:8]}...",
"type": "dataset",
"status": "unknown",
"file_count": 0,
"created_at": os.path.getctime(item_path),
"updated_at": os.path.getmtime(item_path)
})
all_projects = robot_projects + datasets
return {
"success": True,
"message": "项目列表获取成功",
"total_projects": len(all_projects),
"robot_projects": robot_projects,
"datasets": datasets,
"projects": all_projects # 保持向后兼容
}
except Exception as e:
print(f"Error listing projects: {str(e)}")
raise HTTPException(status_code=500, detail=f"获取项目列表失败: {str(e)}")
@app.get("/api/v1/projects/robot")
async def list_robot_projects():
"""获取机器人项目列表"""
try:
response = await list_all_projects()
return {
"success": True,
"message": "机器人项目列表获取成功",
"total_projects": len(response["robot_projects"]),
"projects": response["robot_projects"]
}
except Exception as e:
print(f"Error listing robot projects: {str(e)}")
raise HTTPException(status_code=500, detail=f"获取机器人项目列表失败: {str(e)}")
@app.get("/api/v1/projects/datasets")
async def list_datasets():
"""获取数据集列表"""
try:
response = await list_all_projects()
return {
"success": True,
"message": "数据集列表获取成功",
"total_projects": len(response["datasets"]),
"projects": response["datasets"]
}
except Exception as e:
print(f"Error listing datasets: {str(e)}")
raise HTTPException(status_code=500, detail=f"获取数据集列表失败: {str(e)}")
@app.get("/api/v1/projects/{dataset_id}/tasks")
async def get_project_tasks(dataset_id: str):
"""获取指定项目的所有任务"""
@ -981,63 +1127,6 @@ async def reset_files_processing(dataset_id: str):
except Exception as e:
raise HTTPException(status_code=500, detail=f"重置文件处理状态失败: {str(e)}")
def build_directory_tree(path: str, relative_path: str = "") -> dict:
"""构建目录树结构"""
import os
if not os.path.exists(path):
return {}
tree = {
"name": os.path.basename(path) or "projects",
"path": relative_path,
"type": "directory",
"children": [],
"size": 0,
"modified_time": os.path.getmtime(path)
}
try:
entries = os.listdir(path)
entries.sort()
for entry in entries:
entry_path = os.path.join(path, entry)
entry_relative_path = os.path.join(relative_path, entry) if relative_path else entry
if os.path.isdir(entry_path):
tree["children"].append(build_directory_tree(entry_path, entry_relative_path))
else:
try:
file_size = os.path.getsize(entry_path)
file_modified = os.path.getmtime(entry_path)
tree["children"].append({
"name": entry,
"path": entry_relative_path,
"type": "file",
"size": file_size,
"modified_time": file_modified
})
tree["size"] += file_size
except (OSError, IOError):
tree["children"].append({
"name": entry,
"path": entry_relative_path,
"type": "file",
"size": 0,
"modified_time": 0
})
except (OSError, IOError) as e:
print(f"Error reading directory {path}: {e}")
return tree
# 注册文件管理API路由
app.include_router(file_manager_router)

2227
public/admin.html Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,990 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>文件管理器</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #f5f5f5;
color: #333;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.header {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.toolbar {
background: white;
padding: 15px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 20px;
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.file-list {
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
overflow: hidden;
}
.file-item {
display: flex;
align-items: center;
padding: 12px 20px;
border-bottom: 1px solid #eee;
cursor: pointer;
transition: background-color 0.2s;
}
.file-item:hover {
background-color: #f8f9fa;
}
.file-item.selected {
background-color: #e3f2fd;
}
.file-icon {
width: 24px;
height: 24px;
margin-right: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.file-info {
flex: 1;
}
.file-name {
font-weight: 500;
margin-bottom: 2px;
}
.file-meta {
font-size: 12px;
color: #666;
}
.file-actions {
display: flex;
gap: 8px;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s;
}
.btn-primary {
background-color: #1976d2;
color: white;
}
.btn-primary:hover {
background-color: #1565c0;
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-secondary:hover {
background-color: #5a6268;
}
.btn-danger {
background-color: #dc3545;
color: white;
}
.btn-danger:hover {
background-color: #c82333;
}
.btn-success {
background-color: #28a745;
color: white;
}
.btn-success:hover {
background-color: #218838;
}
.btn-info {
background-color: #17a2b8;
color: white;
}
.btn-info:hover {
background-color: #138496;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
}
.modal-content {
background-color: white;
margin: 15% auto;
padding: 20px;
border-radius: 8px;
width: 90%;
max-width: 500px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 500;
}
.form-group input {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.breadcrumb {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 15px;
font-size: 14px;
}
.breadcrumb-item {
cursor: pointer;
color: #1976d2;
}
.breadcrumb-item:hover {
text-decoration: underline;
}
.upload-area {
border: 2px dashed #ddd;
border-radius: 8px;
padding: 40px;
text-align: center;
cursor: pointer;
transition: border-color 0.2s;
}
.upload-area:hover {
border-color: #1976d2;
}
.upload-area.dragover {
border-color: #1976d2;
background-color: #f0f8ff;
}
.search-box {
flex: 1;
min-width: 200px;
}
.search-box input {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
}
@media (max-width: 768px) {
.container {
padding: 10px;
}
.toolbar {
flex-direction: column;
align-items: stretch;
}
.search-box {
min-width: auto;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📁 项目文件管理器</h1>
<p>管理您的项目文件,支持上传、下载、删除等操作</p>
</div>
<div class="toolbar">
<div class="search-box">
<input type="text" id="searchInput" placeholder="搜索文件..." />
</div>
<button class="btn btn-primary" onclick="showUploadModal()">📤 上传文件</button>
<button class="btn btn-success" onclick="showCreateFolderModal()">📁 新建文件夹</button>
<button class="btn btn-secondary" onclick="refreshFileList()">🔄 刷新</button>
<button class="btn btn-info" onclick="toggleBatchMode()" id="batchModeBtn">📦 批量操作</button>
<button class="btn btn-primary" onclick="selectAll()" id="selectAllBtn" style="display: none;">☑️ 全选</button>
<button class="btn btn-primary" onclick="downloadSelected()" id="downloadSelectedBtn" style="display: none;">📥 下载选中</button>
</div>
<div class="breadcrumb" id="breadcrumb">
<span class="breadcrumb-item" onclick="navigateToPath('')">🏠 根目录</span>
</div>
<div class="file-list" id="fileList">
<!-- 文件列表将在这里动态生成 -->
</div>
</div>
<!-- 上传文件模态框 -->
<div id="uploadModal" class="modal">
<div class="modal-content">
<h3>上传文件</h3>
<div class="upload-area" id="uploadArea">
<p>📁 拖拽文件到这里或点击选择文件</p>
<input type="file" id="fileInput" multiple style="display: none;" />
</div>
<div style="margin-top: 15px; text-align: right;">
<button class="btn btn-secondary" onclick="closeModal('uploadModal')">取消</button>
</div>
</div>
</div>
<!-- 新建文件夹模态框 -->
<div id="createFolderModal" class="modal">
<div class="modal-content">
<h3>新建文件夹</h3>
<div class="form-group">
<label>文件夹名称:</label>
<input type="text" id="folderNameInput" placeholder="输入文件夹名称" />
</div>
<div style="text-align: right;">
<button class="btn btn-secondary" onclick="closeModal('createFolderModal')">取消</button>
<button class="btn btn-primary" onclick="createFolder()">创建</button>
</div>
</div>
</div>
<!-- 重命名模态框 -->
<div id="renameModal" class="modal">
<div class="modal-content">
<h3>重命名</h3>
<div class="form-group">
<label>新名称:</label>
<input type="text" id="renameInput" placeholder="输入新名称" />
</div>
<div style="text-align: right;">
<button class="btn btn-secondary" onclick="closeModal('renameModal')">取消</button>
<button class="btn btn-primary" onclick="renameItem()">重命名</button>
</div>
</div>
</div>
<script>
const API_BASE = `${window.location.protocol}//${window.location.host}/api/v1/files`;
let currentPath = '';
let selectedItems = new Set();
let currentItemToRename = '';
let batchMode = false;
// 初始化
document.addEventListener('DOMContentLoaded', function() {
loadFileList();
setupEventListeners();
});
function setupEventListeners() {
// 搜索功能
document.getElementById('searchInput').addEventListener('input', function(e) {
if (e.target.value.trim()) {
searchFiles(e.target.value.trim());
} else {
loadFileList();
}
});
// 文件上传
const uploadArea = document.getElementById('uploadArea');
const fileInput = document.getElementById('fileInput');
uploadArea.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', handleFileSelect);
// 拖拽上传
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('dragover');
});
uploadArea.addEventListener('dragleave', () => {
uploadArea.classList.remove('dragover');
});
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.classList.remove('dragover');
handleFiles(e.dataTransfer.files);
});
// 模态框点击外部关闭
document.querySelectorAll('.modal').forEach(modal => {
modal.addEventListener('click', (e) => {
if (e.target === modal) {
modal.style.display = 'none';
}
});
});
}
async function loadFileList(path = '') {
try {
const response = await fetch(`${API_BASE}/list?path=${encodeURIComponent(path)}&recursive=false`);
const data = await response.json();
if (data.success) {
currentPath = path;
renderFileList(data.items);
updateBreadcrumb(path);
}
} catch (error) {
console.error('加载文件列表失败:', error);
showMessage('加载文件列表失败', 'error');
}
}
function renderFileList(items) {
const fileList = document.getElementById('fileList');
fileList.innerHTML = '';
// 返回上级目录
if (currentPath) {
const parentItem = createFileItem({
name: '..',
type: 'directory',
path: getParentPath(currentPath),
modified: Date.now() / 1000
});
fileList.appendChild(parentItem);
}
// 渲染文件和文件夹
items.sort((a, b) => {
if (a.type !== b.type) {
return a.type === 'directory' ? -1 : 1;
}
return a.name.localeCompare(b.name);
}).forEach(item => {
const fileItem = createFileItem(item);
fileList.appendChild(fileItem);
});
}
function createFileItem(item) {
const div = document.createElement('div');
div.className = 'file-item';
div.dataset.path = item.path;
const icon = item.type === 'directory' ? '📁' : getFileIcon(item.name);
const size = item.type === 'file' ? formatFileSize(item.size) : '-';
const date = new Date(item.modified * 1000).toLocaleDateString();
const isSelected = selectedItems.has(item.path);
// 构建复选框HTML
const checkboxHtml = batchMode ?
`<input type="checkbox" ${isSelected ? 'checked' : ''} onchange="toggleItemSelection('${item.path}')" style="margin-right: 12px;">` : '';
div.innerHTML = `
${checkboxHtml}
<div class="file-icon">${icon}</div>
<div class="file-info">
<div class="file-name">${item.name}</div>
<div class="file-meta">${size} • ${date}</div>
</div>
<div class="file-actions">
${item.type === 'file' ?
`<button class="btn btn-primary" onclick="downloadFile('${item.path}')">下载</button>` :
`<button class="btn btn-primary" onclick="downloadFolderAsZip('${item.path}')">下载ZIP</button>`
}
<button class="btn btn-secondary" onclick="showRenameModal('${item.path}')">重命名</button>
<button class="btn btn-danger" onclick="deleteItem('${item.path}')">删除</button>
</div>
`;
// 添加批量模式下的点击事件
div.addEventListener('click', (e) => {
if (e.target.closest('.file-actions') || e.target.type === 'checkbox') return;
if (batchMode) {
toggleItemSelection(item.path);
// 更新复选框状态
const checkbox = div.querySelector('input[type="checkbox"]');
if (checkbox) {
checkbox.checked = selectedItems.has(item.path);
}
} else {
if (item.type === 'directory') {
loadFileList(item.path);
} else {
window.open(`${API_BASE}/download/${item.path}`, '_blank');
}
}
});
// 添加选中状态的样式
if (isSelected) {
div.classList.add('selected');
}
return div;
}
function getFileIcon(fileName) {
const ext = fileName.split('.').pop().toLowerCase();
const iconMap = {
'md': '📄',
'txt': '📝',
'pdf': '📕',
'doc': '📘',
'docx': '📘',
'xls': '📗',
'xlsx': '📗',
'ppt': '📙',
'pptx': '📙',
'jpg': '🖼️',
'jpeg': '🖼️',
'png': '🖼️',
'gif': '🖼️',
'zip': '📦',
'rar': '📦',
'mp4': '🎬',
'mp3': '🎵',
'json': '📋',
'xml': '📋',
'html': '🌐',
'css': '🎨',
'js': '⚡',
'py': '🐍'
};
return iconMap[ext] || '📄';
}
function formatFileSize(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
}
function updateBreadcrumb(path) {
const breadcrumb = document.getElementById('breadcrumb');
breadcrumb.innerHTML = '<span class="breadcrumb-item" onclick="navigateToPath(\'\')">🏠 根目录</span>';
if (path) {
const parts = path.split('/');
let currentPath = '';
parts.forEach((part, index) => {
currentPath += (currentPath ? '/' : '') + part;
const isLast = index === parts.length - 1;
if (!isLast) {
breadcrumb.innerHTML += ` <span></span> <span class="breadcrumb-item" onclick="navigateToPath('${currentPath}')">${part}</span>`;
} else {
breadcrumb.innerHTML += ` <span></span> <span>${part}</span>`;
}
});
}
}
function navigateToPath(path) {
loadFileList(path);
}
function getParentPath(path) {
const parts = path.split('/');
return parts.slice(0, -1).join('/');
}
async function searchFiles(query) {
try {
const response = await fetch(`${API_BASE}/search?query=${encodeURIComponent(query)}&path=${encodeURIComponent(currentPath)}`);
const data = await response.json();
if (data.success) {
renderFileList(data.results.map(item => ({
...item,
modified: Date.now() / 1000,
size: 0
})));
}
} catch (error) {
console.error('搜索失败:', error);
showMessage('搜索失败', 'error');
}
}
function handleFileSelect(e) {
handleFiles(e.target.files);
}
async function handleFiles(files) {
for (let file of files) {
await uploadFile(file);
}
closeModal('uploadModal');
loadFileList();
}
async function uploadFile(file) {
const formData = new FormData();
formData.append('file', file);
formData.append('path', currentPath);
try {
const response = await fetch(`${API_BASE}/upload`, {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.success) {
showMessage(`文件 ${file.name} 上传成功`, 'success');
} else {
showMessage(`文件 ${file.name} 上传失败`, 'error');
}
} catch (error) {
console.error('上传失败:', error);
showMessage(`文件 ${file.name} 上传失败`, 'error');
}
}
async function deleteItem(path) {
if (!confirm(`确定要删除 ${path.split('/').pop()} 吗?`)) return;
try {
const response = await fetch(`${API_BASE}/delete?path=${encodeURIComponent(path)}`, {
method: 'DELETE'
});
const data = await response.json();
if (data.success) {
showMessage('删除成功', 'success');
loadFileList();
} else {
showMessage('删除失败', 'error');
}
} catch (error) {
console.error('删除失败:', error);
showMessage('删除失败', 'error');
}
}
async function createFolder() {
const folderName = document.getElementById('folderNameInput').value.trim();
if (!folderName) {
showMessage('请输入文件夹名称', 'error');
return;
}
try {
const response = await fetch(`${API_BASE}/create-folder`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
path: currentPath,
name: folderName
})
});
const data = await response.json();
if (data.success) {
showMessage('文件夹创建成功', 'success');
closeModal('createFolderModal');
loadFileList();
} else {
showMessage('文件夹创建失败', 'error');
}
} catch (error) {
console.error('创建文件夹失败:', error);
showMessage('创建文件夹失败', 'error');
}
}
async function renameItem() {
const newName = document.getElementById('renameInput').value.trim();
if (!newName) {
showMessage('请输入新名称', 'error');
return;
}
try {
const response = await fetch(`${API_BASE}/rename`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
old_path: currentItemToRename,
new_name: newName
})
});
const data = await response.json();
if (data.success) {
showMessage('重命名成功', 'success');
closeModal('renameModal');
loadFileList();
} else {
showMessage('重命名失败', 'error');
}
} catch (error) {
console.error('重命名失败:', error);
showMessage('重命名失败', 'error');
}
}
function downloadFile(path) {
window.open(`${API_BASE}/download/${path}`, '_blank');
}
async function downloadFolderAsZip(folderPath) {
const folderName = folderPath.split('/').pop();
const downloadButton = event.target;
// 禁用按钮,显示加载状态
const originalText = downloadButton.textContent;
downloadButton.disabled = true;
downloadButton.textContent = '压缩中...';
try {
// 显示压缩开始提示
showMessage(`正在压缩文件夹 "${folderName}"...`, 'info');
// 使用新的下载文件夹ZIP接口
const response = await fetch(`${API_BASE}/download-folder-zip`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ path: folderPath })
});
if (response.ok) {
// 获取文件名
const contentDisposition = response.headers.get('content-disposition');
let fileName = `${folderName}.zip`;
if (contentDisposition) {
const fileNameMatch = contentDisposition.match(/filename="?([^"]+)"?/);
if (fileNameMatch) {
fileName = fileNameMatch[1];
}
}
// 检查文件大小
const contentLength = response.headers.get('content-length');
const fileSize = contentLength ? parseInt(contentLength) : 0;
if (fileSize > 100 * 1024 * 1024) { // 大于100MB显示警告
showMessage(`文件较大 (${formatFileSize(fileSize)}),下载可能需要一些时间...`, 'warning');
}
downloadButton.textContent = '下载中...';
// 创建下载链接
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = fileName;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
showMessage(`文件夹 "${folderName}" 压缩下载成功`, 'success');
} else {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || `服务器错误: ${response.status}`);
}
} catch (error) {
console.error('下载文件夹失败:', error);
showMessage(`下载文件夹 "${folderName}" 失败: ${error.message}`, 'error');
} finally {
// 恢复按钮状态
downloadButton.disabled = false;
downloadButton.textContent = originalText;
}
}
function refreshFileList() {
loadFileList(currentPath);
}
function toggleBatchMode() {
batchMode = !batchMode;
const batchBtn = document.getElementById('batchModeBtn');
const downloadBtn = document.getElementById('downloadSelectedBtn');
const selectAllBtn = document.getElementById('selectAllBtn');
if (batchMode) {
batchBtn.textContent = '❌ 退出批量';
batchBtn.classList.remove('btn-info');
batchBtn.classList.add('btn-secondary');
downloadBtn.style.display = 'inline-block';
selectAllBtn.style.display = 'inline-block';
selectedItems.clear();
} else {
batchBtn.textContent = '📦 批量操作';
batchBtn.classList.remove('btn-secondary');
batchBtn.classList.add('btn-info');
downloadBtn.style.display = 'none';
selectAllBtn.style.display = 'none';
selectedItems.clear();
}
refreshFileList(); // 重新渲染以显示/隐藏复选框
}
function selectAll() {
const allItems = document.querySelectorAll('.file-item[data-path]');
const allPaths = Array.from(allItems).map(item => item.dataset.path);
if (selectedItems.size === allPaths.length) {
// 如果已全选,则取消全选
selectedItems.clear();
document.getElementById('selectAllBtn').textContent = '☑️ 全选';
} else {
// 全选
selectedItems.clear();
allPaths.forEach(path => selectedItems.add(path));
document.getElementById('selectAllBtn').textContent = '❌ 取消全选';
}
// 更新所有复选框和选中状态
allItems.forEach(item => {
const path = item.dataset.path;
const checkbox = item.querySelector('input[type="checkbox"]');
if (checkbox) {
checkbox.checked = selectedItems.has(path);
}
if (selectedItems.has(path)) {
item.classList.add('selected');
} else {
item.classList.remove('selected');
}
});
updateBatchUI();
}
function toggleItemSelection(path) {
if (selectedItems.has(path)) {
selectedItems.delete(path);
} else {
selectedItems.add(path);
}
updateBatchUI();
}
function updateBatchUI() {
const downloadBtn = document.getElementById('downloadSelectedBtn');
const count = selectedItems.size;
if (count > 0) {
downloadBtn.textContent = `📥 下载选中 (${count})`;
downloadBtn.classList.remove('btn-primary');
downloadBtn.classList.add('btn-success');
} else {
downloadBtn.textContent = '📥 下载选中';
downloadBtn.classList.remove('btn-success');
downloadBtn.classList.add('btn-primary');
}
}
async function downloadSelected() {
if (selectedItems.size === 0) {
showMessage('请先选择要下载的文件', 'warning');
return;
}
const items = Array.from(selectedItems);
const hasFolders = items.some(path => {
const fullPath = PROJECTS_DIR / path;
return fullPath.isDirectory();
});
if (hasFolders) {
showMessage('检测到文件夹,正在创建压缩包...', 'info');
await downloadItemsAsZip(items);
} else {
// 下载单个文件
for (const item of items) {
downloadFile(item);
}
}
}
async function downloadItemsAsZip(items) {
try {
showMessage(`正在压缩 ${items.length} 个项目...`, 'info');
const response = await fetch(`${API_BASE}/download-multiple-zip`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
paths: items,
filename: `batch_download_${new Date().getTime()}.zip`
})
});
if (response.ok) {
const contentDisposition = response.headers.get('content-disposition');
let fileName = `batch_download.zip`;
if (contentDisposition) {
const fileNameMatch = contentDisposition.match(/filename="?([^"]+)"?/);
if (fileNameMatch) {
fileName = fileNameMatch[1];
}
}
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = fileName;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
showMessage('批量下载成功', 'success');
} else {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || `服务器错误: ${response.status}`);
}
} catch (error) {
console.error('批量下载失败:', error);
showMessage(`批量下载失败: ${error.message}`, 'error');
}
}
function showUploadModal() {
document.getElementById('uploadModal').style.display = 'block';
document.getElementById('fileInput').value = '';
}
function showCreateFolderModal() {
document.getElementById('createFolderModal').style.display = 'block';
document.getElementById('folderNameInput').value = '';
}
function showRenameModal(path) {
currentItemToRename = path;
const fileName = path.split('/').pop();
document.getElementById('renameInput').value = fileName;
document.getElementById('renameModal').style.display = 'block';
}
function closeModal(modalId) {
document.getElementById(modalId).style.display = 'none';
}
function showMessage(message, type) {
// 消息提示,支持多种类型
const toast = document.createElement('div');
let bgColor = '#6c757d'; // 默认灰色
switch(type) {
case 'success':
bgColor = '#28a745';
break;
case 'error':
bgColor = '#dc3545';
break;
case 'info':
bgColor = '#17a2b8';
break;
case 'warning':
bgColor = '#ffc107';
break;
}
toast.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 12px 20px;
border-radius: 4px;
color: ${type === 'warning' ? '#212529' : 'white'};
z-index: 10000;
background-color: ${bgColor};
max-width: 300px;
word-wrap: break-word;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
`;
toast.textContent = message;
document.body.appendChild(toast);
// 自动移除
setTimeout(() => {
if (document.body.contains(toast)) {
document.body.removeChild(toast);
}
}, 4000);
}
</script>
</body>
</html>

View File

@ -8,7 +8,7 @@ echo "Starting Qwen Agent Application"
echo "========================================="
# 创建必要的目录
mkdir -p /app/queue_data
mkdir -p /app/projects/queue_data
# 等待一下确保目录创建完成
sleep 1

View File

@ -7,8 +7,8 @@ import os
from huey import SqliteHuey
from datetime import timedelta
# 确保queue_data目录存在
queue_data_dir = os.path.join(os.path.dirname(__file__), '..', 'queue_data')
# 确保projects/queue_data目录存在
queue_data_dir = os.path.join(os.path.dirname(__file__), '..', 'projects', 'queue_data')
os.makedirs(queue_data_dir, exist_ok=True)
# 初始化SqliteHuey

View File

@ -44,7 +44,7 @@ class QueueConsumer:
print(f"启动队列消费者...")
print(f"工作线程数: {self.workers}")
print(f"工作类型: {self.worker_type}")
print(f"数据库: {os.path.join(os.path.dirname(__file__), '..', 'queue_data', 'huey.db')}")
print(f"数据库: {os.path.join(os.path.dirname(__file__), '..', 'projects', 'queue_data', 'huey.db')}")
print("按 Ctrl+C 停止消费者")
self.running = True

View File

@ -20,7 +20,7 @@ class QueueManager:
def __init__(self):
self.huey = huey
print(f"队列管理器已初始化,使用数据库: {os.path.join(os.path.dirname(__file__), '..', 'queue_data', 'huey.db')}")
print(f"队列管理器已初始化,使用数据库: {os.path.join(os.path.dirname(__file__), '..', 'projects', 'queue_data', 'huey.db')}")
def enqueue_file(
self,
@ -192,7 +192,7 @@ class QueueManager:
"error_tasks": 0,
"scheduled_tasks": 0,
"recent_tasks": [],
"queue_database": os.path.join(os.path.dirname(__file__), '..', 'queue_data', 'huey.db')
"queue_database": os.path.join(os.path.dirname(__file__), '..', 'projects', 'queue_data', 'huey.db')
}
# 尝试获取待处理任务数量

View File

@ -14,7 +14,7 @@ from pathlib import Path
class TaskStatusStore:
"""基于SQLite的任务状态存储器"""
def __init__(self, db_path: str = "queue_data/task_status.db"):
def __init__(self, db_path: str = "projects/queue_data/task_status.db"):
self.db_path = db_path
# 确保目录存在
Path(db_path).parent.mkdir(parents=True, exist_ok=True)