新增队列管理后台
This commit is contained in:
parent
d5a989860c
commit
d00601af23
2
.gitignore
vendored
2
.gitignore
vendored
@ -4,4 +4,4 @@ workspace
|
||||
__pycache__
|
||||
*/__pycache__
|
||||
models
|
||||
queue_data
|
||||
projects/queue_data
|
||||
|
||||
@ -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')"
|
||||
|
||||
@ -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')"
|
||||
|
||||
@ -571,7 +571,7 @@ qwen-agent/
|
||||
│ └── tools/ # 工具定义文件
|
||||
├── models/ # 模型文件
|
||||
├── projects/ # 项目目录
|
||||
├── queue_data/ # 队列数据
|
||||
│ └── queue_data/ # 队列数据
|
||||
├── public/ # 静态文件
|
||||
├── embedding/ # 嵌入模型模块
|
||||
├── prompt/ # 系统提示词
|
||||
|
||||
203
fastapi_app.py
203
fastapi_app.py
@ -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
2227
public/admin.html
Normal file
File diff suppressed because it is too large
Load Diff
@ -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>
|
||||
@ -8,7 +8,7 @@ echo "Starting Qwen Agent Application"
|
||||
echo "========================================="
|
||||
|
||||
# 创建必要的目录
|
||||
mkdir -p /app/queue_data
|
||||
mkdir -p /app/projects/queue_data
|
||||
|
||||
# 等待一下确保目录创建完成
|
||||
sleep 1
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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')
|
||||
}
|
||||
|
||||
# 尝试获取待处理任务数量
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user