Commit c0705977 authored by wangkaixiong's avatar wangkaixiong 🚴🏼
Browse files

init

parent d3982d85
# Linux模型管理工具
一个基于Web的模型管理工具,用于在Linux环境下下载、上传和管理ModelScope模型,并支持上传到CsgHub平台。
## 功能特点
1. **模型下载**
- 支持通过模型ID下载ModelScope模型
- 支持多个模型ID批量下载(用英文逗号分隔)
- 可自定义本地存放路径,支持设置默认路径
- 实时显示下载进度
- 下载失败自动重试(最多10次)
2. **模型上传**
- 自动列出所有已下载但未上传的模型
- 支持批量上传和单个模型上传
- 实时显示上传进度
3. **模型管理**
- 列出所有已下载的模型
- 显示模型状态、大小、下载时间等信息
- 支持删除选中的模型
4. **配置管理**
- 设置默认模型存放路径
- 配置最大重试次数
- 配置CsgHub连接信息
## 技术栈
- **前端**:HTML5, CSS3, JavaScript, Tailwind CSS, Font Awesome
- **后端**:Python, Flask, Flask-SocketIO
- **数据存储**:SQLite
## 安装与运行
### 前提条件
- Python 3.6+
- pip
### 安装步骤
1. 克隆或下载本项目代码
2. 进入项目目录
```bash
cd model_manager_webapp
```
3. 运行启动脚本
```bash
chmod +x start.sh
./start.sh
```
启动脚本会自动:
- 创建虚拟环境
- 安装所需依赖
- 启动Web应用
4. 打开浏览器访问
```
http://localhost:5000
```
## 使用说明
### 下载模型
1. 在"模型下载"标签页中,输入模型ID(多个模型ID用英文逗号分隔)
2. 选择本地存放路径(可选,默认使用配置中的路径)
3. 点击"开始下载"按钮
4. 查看下载进度和状态
### 上传模型
1. 切换到"模型上传"标签页
2. 选择要上传的模型(可多选)
3. 点击"上传选中"按钮
4. 查看上传进度和状态
### 管理模型
1. 切换到"模型管理"标签页
2. 查看所有已下载的模型列表
3. 可通过点击删除图标删除模型
4. 对于已下载但未上传的模型,可点击上传图标单独上传
### 配置应用
1. 点击顶部导航栏的"配置"按钮
2. 修改所需配置项
3. 点击"保存配置"按钮
## 注意事项
1. 本应用目前是一个演示版本,实际的模型下载和上传功能需要集成ModelScope SDK和pycsghub库
2. 当前版本使用模拟数据展示功能,实际部署时需要替换为真实的下载和上传逻辑
3. 确保应用有足够的权限访问指定的模型存放路径
## 后续开发计划
1. 集成ModelScope SDK实现真实的模型下载
2. 集成pycsghub实现真实的模型上传
3. 添加用户认证功能
4. 支持更多模型源
5. 优化批量操作性能
\ No newline at end of file
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import sys
import json
import time
import uuid
import shutil
import threading
import subprocess
from datetime import datetime
from flask import Flask, request, jsonify, send_from_directory
from flask_cors import CORS
from flask_socketio import SocketIO, emit
# 确保可以导入自定义模块
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
# 导入模型管理模块
from model_manager import ModelManager
app = Flask(__name__, static_folder='static', template_folder='templates')
app.config['SECRET_KEY'] = 'secret!'
CORS(app)
socketio = SocketIO(app, cors_allowed_origins="*")
# 初始化模型管理器
model_manager = ModelManager()
# 任务状态
tasks = {}
# 线程存储
task_threads = {}
# 模型状态数据库
models_db = {}
# 数据库锁,用于避免多线程访问冲突
db_lock = threading.Lock()
# 数据库文件路径
db_file = 'models_db.json'
def save_models_db():
"""保存模型数据库到文件"""
global models_db
try:
print(f"[DEBUG] 开始保存数据库,当前线程: {threading.current_thread().name}")
# 不使用锁,避免可能的死锁
# with db_lock:
# with open(db_file, 'w') as f:
# json.dump(models_db, f, indent=2)
# 简化版本,直接写入
with open(db_file, 'w') as f:
json.dump(models_db, f, indent=2)
print(f"[DEBUG] 模型数据库已保存到 {db_file}")
except Exception as e:
print(f"[ERROR] 保存模型数据库失败: {str(e)}")
@app.route('/')
def index():
"""提供前端页面"""
return send_from_directory('../frontend', 'index.html')
@app.route('/api/download', methods=['POST'])
def download_model():
"""下载模型API"""
data = request.json
model_id = data.get('model_id')
local_path = data.get('local_path', '/home/user/models')
task_id = data.get('task_id')
if not model_id:
return jsonify({'error': '请提供有效的模型ID'}), 400
if not task_id:
task_id = f"task_{uuid.uuid4().hex}"
# 创建任务
tasks[task_id] = {
'task_id': task_id,
'model_id': model_id,
'local_path': local_path,
'type': 'download',
'status': 'pending',
'progress': 0,
'retry_count': 0,
'start_time': datetime.now().isoformat(),
'message': '准备下载...'
}
# 将模型添加到数据库或更新状态
model_key = f"{model_id}_{local_path}"
# 不使用锁,避免可能的死锁
# with db_lock:
if model_key not in models_db:
models_db[model_key] = {
'model_id': model_id,
'local_path': os.path.join(local_path, model_id),
'status': 'downloading',
'download_time': None,
'upload_time': None,
'upload_repo_id': None
}
else:
# 更新现有模型的状态为下载中
models_db[model_key]['status'] = 'downloading'
models_db[model_key]['download_time'] = None
# 清除之前的进度信息
models_db[model_key].pop('progress', None)
models_db[model_key].pop('message', None)
# 保存数据库
save_models_db()
print(f"[DEBUG] 模型状态已更新为下载中: {model_id}")
# 启动下载线程
thread = threading.Thread(target=download_models, args=([task_id],))
task_threads[task_id] = thread
thread.start()
return jsonify({'status': 'success', 'task_id': task_id}), 200
@app.route('/api/upload', methods=['POST'])
def upload_model():
"""上传模型API"""
try:
data = request.json
model_ids = data.get('model_ids', [])
create_repo_flag = data.get('create_repo_flag', True)
print(f"[DEBUG] 上传API被调用,模型IDs: {model_ids}, 创建仓库: {create_repo_flag}")
if not model_ids:
return jsonify({'error': '请选择要上传的模型'}), 400
# 为每个模型创建任务
task_ids = []
for model_id in model_ids:
# 查找模型信息
model_info = None
for key, model in models_db.items():
if model['model_id'] == model_id and model['status'] in ['downloaded', 'uploading']:
model_info = model
break
if not model_info:
print(f"[DEBUG] 模型 {model_id} 未找到或状态不允许上传")
continue
task_id = f"task_{uuid.uuid4().hex}"
tasks[task_id] = {
'task_id': task_id,
'model_id': model_id,
'local_path': model_info['local_path'],
'type': 'upload',
'status': 'pending',
'progress': 0,
'start_time': datetime.now().isoformat(),
'message': '准备上传...'
}
task_ids.append(task_id)
# 更新模型状态
model_info['status'] = 'uploading'
# 启动上传线程
thread = threading.Thread(target=upload_models, args=(task_ids, create_repo_flag))
# 为每个任务ID存储同一个线程
for task_id in task_ids:
task_threads[task_id] = thread
thread.start()
return jsonify({'task_ids': task_ids}), 200
except Exception as e:
print(f"[DEBUG] 上传API错误: {str(e)}")
return jsonify({'error': str(e)}), 500
@app.route('/api/delete', methods=['POST'])
def delete_model():
"""删除模型API"""
print("[DEBUG] 删除API被调用")
try:
data = request.json
print(f"[DEBUG] 删除API请求数据: {data}")
model_ids = data.get('model_ids', [])
if not model_ids:
print("[DEBUG] 未提供模型ID")
return jsonify({'error': '请选择要删除的模型'}), 400
deleted_models = []
errors = []
for model_id in model_ids:
# 查找模型信息
model_key = None
model_info = None
for key, model in models_db.items():
if model['model_id'] == model_id:
model_key = key
model_info = model
break
if not model_info:
errors.append(f"模型 {model_id} 不存在")
continue
# 检查模型状态
if model_info['status'] in ['downloading', 'uploading']:
errors.append(f"模型 {model_id} 正在进行下载/上传操作,无法删除")
continue
try:
# 使用模型管理器删除模型
print(f"[DEBUG] 调用模型管理器删除模型: {model_id}, 路径: {model_info['local_path']}")
success = model_manager.delete_model(model_info['local_path'])
if success:
# 从数据库中删除
with db_lock:
if model_key:
del models_db[model_key]
deleted_models.append(model_id)
print(f"[DEBUG] 模型 {model_id} 删除成功")
else:
errors.append(f"删除模型 {model_id} 失败: 模型管理器返回失败")
except Exception as e:
errors.append(f"删除模型 {model_id} 失败: {str(e)}")
print(f"[ERROR] 删除模型 {model_id} 异常: {str(e)}")
result = {
'deleted': deleted_models,
'errors': errors
}
# 如果有模型被删除,保存数据库
if deleted_models:
save_models_db()
return jsonify(result), 200
except Exception as e:
print(f"[ERROR] 删除API异常: {str(e)}")
return jsonify({'error': str(e)}), 500
@app.route('/api/models', methods=['GET'])
def get_models():
"""获取模型列表"""
try:
# 获取查询参数
status = request.args.get('status')
all_models = request.args.get('all', 'false').lower() == 'true'
model_path = request.args.get('path') # 获取用户指定的模型路径
print(f"API get_models called with: status={status}, all={all_models}, path={model_path}")
# 从本地文件系统获取模型列表,使用用户指定的路径或默认路径
local_models = model_manager.list_models(local_path=model_path)
# 合并本地模型和数据库中的模型信息
for local_model in local_models:
# 查找数据库中是否有该模型的信息
model_key = f"{local_model['id']}_{os.path.dirname(local_model['path'])}"
if model_key in models_db:
# 更新本地模型的状态信息
db_model = models_db[model_key]
# 保留数据库中的状态,不覆盖进行中的任务状态
local_model['status'] = db_model.get('status', 'downloaded')
local_model['download_time'] = db_model.get('download_time')
local_model['upload_time'] = db_model.get('upload_time')
local_model['upload_repo_id'] = db_model.get('upload_repo_id')
# 添加进度信息用于前端显示
if db_model.get('status') in ['downloading', 'uploading']:
local_model['progress'] = db_model.get('progress', 0)
local_model['message'] = db_model.get('message', '进行中...')
print(f"[DEBUG] 从数据库加载模型: {local_model['id']}, 状态: {local_model['status']}")
else:
# 如果模型不在数据库中,添加到数据库
models_db[model_key] = {
'model_id': local_model['id'],
'local_path': local_model['path'],
'status': 'downloaded',
'download_time': datetime.now().isoformat(),
'upload_time': None,
'upload_repo_id': None
}
print(f"[DEBUG] 新增模型到数据库: {local_model['id']}")
# 同时更新本地模型的状态信息
local_model['status'] = 'downloaded'
local_model['download_time'] = models_db[model_key]['download_time']
local_model['upload_time'] = None
local_model['upload_repo_id'] = None
# 及时保存数据库到文件
save_models_db()
# 根据状态筛选
if status == 'downloaded':
# 返回已下载但未上传的模型
models = [model for model in local_models if model.get('status') == 'downloaded']
else:
# 返回所有模型
models = local_models
# 格式化返回数据
result = {
'models': models
}
return jsonify(result), 200
except Exception as e:
print(f"Error getting models: {e}")
return jsonify({'error': str(e)}), 500
@app.route('/api/task/<task_id>', methods=['GET'])
def get_task_status(task_id):
"""获取任务状态"""
if task_id not in tasks:
return jsonify({'error': '任务不存在'}), 404
return jsonify(tasks[task_id]), 200
@app.route('/api/system/info', methods=['GET'])
def get_system_info():
"""获取系统信息"""
try:
# 获取操作系统信息
os_info = subprocess.check_output(['uname', '-a']).decode('utf-8').strip()
# 获取磁盘使用情况
disk_usage = subprocess.check_output(['df', '-h']).decode('utf-8')
# 获取内存使用情况
memory_usage = subprocess.check_output(['free', '-h']).decode('utf-8')
return jsonify({
'os': os_info,
'disk_usage': disk_usage,
'memory_usage': memory_usage
}), 200
except Exception as e:
return jsonify({'error': str(e)}), 500
@socketio.on('connect')
def handle_connect():
"""处理WebSocket连接"""
print('客户端已连接')
@socketio.on('disconnect')
def handle_disconnect():
"""处理WebSocket断开连接"""
pass # 静默处理断开连接,减少日志输出
@app.route('/api/download/cancel/<task_id>', methods=['POST'])
def cancel_download(task_id):
"""取消下载任务"""
try:
print(f"[DEBUG] 收到取消下载请求: {task_id}")
# 检查任务是否存在
if task_id in tasks:
# 从任务字典中移除任务
task = tasks.pop(task_id)
print(f"[DEBUG] 已取消下载任务: {task_id}, 模型ID: {task.get('model_id')}")
# 更新模型状态
model_id = task.get('model_id')
local_path = task.get('local_path')
if model_id and local_path:
model_key = f"{model_id}_{local_path}"
if model_key in models_db:
# 将状态从downloading改为failed
if models_db[model_key].get('status') == 'downloading':
models_db[model_key]['status'] = 'failed'
models_db[model_key].pop('progress', None)
models_db[model_key].pop('message', None)
save_models_db()
print(f"[DEBUG] 已更新模型状态为失败: {model_id}")
# 从线程存储中移除
if task_id in task_threads:
del task_threads[task_id]
return jsonify({'success': True, 'message': '下载任务已取消'})
else:
print(f"[DEBUG] 任务不存在: {task_id}")
return jsonify({'success': False, 'message': '任务不存在'}), 404
except Exception as e:
print(f"[DEBUG] 取消下载失败: {str(e)}")
return jsonify({'success': False, 'message': f'取消失败: {str(e)}'}), 500
@app.route('/api/upload/cancel/<task_id>', methods=['POST'])
def cancel_upload(task_id):
"""取消上传任务"""
try:
print(f"[DEBUG] 收到取消上传请求: {task_id}")
# 检查任务是否存在
if task_id in tasks:
# 从任务字典中移除任务
task = tasks.pop(task_id)
print(f"[DEBUG] 已取消上传任务: {task_id}, 模型ID: {task.get('model_id')}")
# 更新模型状态
model_id = task.get('model_id')
local_path = task.get('local_path')
if model_id and local_path:
model_key = f"{model_id}_{local_path}"
if model_key in models_db:
# 将状态从uploading改为downloaded
if models_db[model_key].get('status') == 'uploading':
models_db[model_key]['status'] = 'downloaded'
models_db[model_key].pop('progress', None)
models_db[model_key].pop('message', None)
save_models_db()
print(f"[DEBUG] 已更新模型状态为已下载: {model_id}")
return jsonify({'success': True, 'message': '上传任务已取消'})
else:
print(f"[DEBUG] 任务不存在: {task_id}")
return jsonify({'success': False, 'message': '任务不存在'}), 404
except Exception as e:
print(f"[DEBUG] 取消上传失败: {str(e)}")
return jsonify({'success': False, 'message': f'取消失败: {str(e)}'}), 500
@socketio.on('subscribe_task')
def handle_subscribe_task(data):
"""订阅任务进度更新"""
task_id = data.get('task_id')
if task_id in tasks:
# 立即发送当前状态
emit('task_update', tasks[task_id])
def download_models(task_ids):
"""下载模型线程"""
import concurrent.futures
for task_id in task_ids:
task = tasks.get(task_id)
if not task:
continue
model_id = task['model_id']
local_path = task['local_path']
# 更新任务状态
task['status'] = 'downloading'
task['message'] = f'开始下载模型 {model_id}'
socketio.emit('task_update', task)
# 尝试下载模型
max_retries = 10
retry_count = 0
# 使用默认参数捕获task_id,避免lambda闭包问题
def make_progress_callback(tid):
def progress_callback(progress, detail):
update_download_progress(tid, progress, detail)
return progress_callback
while retry_count < max_retries:
# 检查任务是否已被取消
if task_id not in tasks:
print(f"[INFO] 下载任务已取消: {task_id}")
return
try:
# 创建取消检查函数
def cancel_check():
return task_id not in tasks
# 使用线程池执行下载,以便可以取消
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
# 提交下载任务
future = executor.submit(
model_manager.download_model,
model_id=model_id,
local_path=local_path,
progress_callback=make_progress_callback(task_id),
cancel_check=cancel_check
)
# 等待下载完成,同时检查任务是否被取消
while not future.done():
# 检查任务是否已被取消
if task_id not in tasks:
print(f"[INFO] 下载任务已取消: {task_id}")
# 取消future
future.cancel()
return
# 短暂睡眠,避免CPU占用过高
time.sleep(0.1)
# 获取下载结果
model_path = future.result()
# 下载成功
task['status'] = 'completed'
task['progress'] = 100
task['message'] = f'模型 {model_id} 下载完成'
# 发送下载完成事件
socketio.emit('download_complete', {
'taskId': task_id,
'modelId': model_id
})
socketio.emit('task_update', task)
# 更新模型状态
model_key = f"{model_id}_{local_path}"
if model_key in models_db:
# 只有当状态不是已上传时才更新为已下载
if models_db[model_key].get('status') != 'uploaded':
models_db[model_key]['status'] = 'downloaded'
models_db[model_key]['download_time'] = datetime.now().isoformat()
# 清除进度信息
models_db[model_key].pop('progress', None)
models_db[model_key].pop('message', None)
break
except Exception as e:
# 检查任务是否已被取消
if task_id not in tasks:
print(f"[INFO] 下载任务已取消: {task_id}")
return
retry_count += 1
task['retry_count'] = retry_count
task['message'] = f'下载失败 (尝试 {retry_count}/{max_retries}): {str(e)}'
socketio.emit('task_update', task)
if retry_count < max_retries:
# 等待一段时间后重试
time.sleep(5)
else:
# 达到最大重试次数
task['status'] = 'failed'
task['message'] = f'达到最大重试次数,下载失败: {str(e)}'
# 发送下载失败事件
socketio.emit('download_failed', {
'taskId': task_id,
'modelId': model_id,
'error': str(e)
})
socketio.emit('task_update', task)
# 更新模型状态
model_key = f"{model_id}_{local_path}"
if model_key in models_db:
# 只有当状态不是已上传时才更新为失败
if models_db[model_key].get('status') != 'uploaded':
models_db[model_key]['status'] = 'failed'
models_db[model_key]['download_time'] = None
# 清除进度信息
models_db[model_key].pop('progress', None)
models_db[model_key].pop('message', None)
def update_download_progress(task_id, progress, detail=None):
"""更新下载进度"""
if task_id not in tasks:
return
task = tasks[task_id]
task['progress'] = progress
if detail:
if isinstance(detail, dict):
# 新的进度回调格式
task['message'] = f"正在下载: {detail.get('current_file', 'unknown')} ({detail.get('file_count', 0)}/{detail.get('total_files', 0)})"
# 通过WebSocket发送进度更新
socketio.emit('download_progress', {
'taskId': task_id,
'progress': progress,
'fileCount': detail.get('file_count', 0),
'totalFiles': detail.get('total_files', 0),
'currentFile': detail.get('current_file', 'unknown'),
'fileSize': detail.get('file_size', 0)
})
else:
# 旧的进度回调格式
task['message'] = detail
socketio.emit('download_progress', {
'taskId': task_id,
'progress': progress,
'message': detail
})
# 发送通用任务更新
socketio.emit('task_update', task)
def upload_models(task_ids, create_repo_flag=True):
"""上传模型线程"""
for task_id in task_ids:
task = tasks.get(task_id)
if not task:
continue
model_id = task['model_id']
local_path = task['local_path']
# 更新任务状态
task['status'] = 'uploading'
task['message'] = f'开始上传模型 {model_id}'
socketio.emit('task_update', task)
# 使用默认参数捕获task_id,避免lambda闭包问题
def make_upload_progress_callback(tid):
def progress_callback(progress, detail):
update_upload_progress(tid, progress, detail)
return progress_callback
try:
# 检查任务是否已被取消
if task_id not in tasks:
print(f"[INFO] 上传任务已取消: {task_id}")
return
# 调用模型管理器上传模型
repo_id = os.path.basename(model_id)
model_manager.upload_model(
local_path=local_path,
repo_id=repo_id,
create_repo_flag=create_repo_flag,
progress_callback=make_upload_progress_callback(task_id)
)
# 上传成功
task['status'] = 'completed'
task['progress'] = 100
task['message'] = f'模型 {model_id} 上传完成'
socketio.emit('task_update', task)
# 更新模型状态
for key, model in models_db.items():
if model['model_id'] == model_id:
model['status'] = 'uploaded'
model['upload_time'] = datetime.now().isoformat()
model['upload_repo_id'] = repo_id
break
except Exception as e:
# 检查任务是否已被取消
if task_id not in tasks:
print(f"[INFO] 上传任务已取消: {task_id}")
return
task['status'] = 'failed'
task['message'] = f'上传失败: {str(e)}'
socketio.emit('task_update', task)
# 更新模型状态
for key, model in models_db.items():
if model['model_id'] == model_id:
model['status'] = 'downloaded' # 恢复为已下载状态
break
def update_upload_progress(task_id, progress, detail=None):
"""更新上传进度"""
if task_id not in tasks:
return
task = tasks[task_id]
task['progress'] = progress
if detail:
task['message'] = detail
socketio.emit('task_update', task)
if __name__ == '__main__':
# 加载模型数据库
db_file = 'models_db.json'
if os.path.exists(db_file):
try:
with open(db_file, 'r') as f:
models_db = json.load(f)
except Exception as e:
print(f"加载模型数据库失败: {str(e)}")
# 清理数据库中状态为 downloading 或 uploading 的任务(可能是之前未正常关闭的任务)
for model_key, model_info in models_db.items():
if model_info.get('status') in ['downloading', 'uploading']:
print(f"[INFO] 清理异常状态模型: {model_info.get('model_id')}, 状态: {model_info.get('status')}")
model_info['status'] = 'failed'
model_info.pop('progress', None)
model_info.pop('message', None)
save_models_db()
# 启动服务器
socketio.run(app, host='0.0.0.0', port=2026, debug=False)
# 保存模型数据库
try:
with open(db_file, 'w') as f:
json.dump(models_db, f, indent=2)
except Exception as e:
print(f"保存模型数据库失败: {str(e)}")
\ No newline at end of file
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import sys
import time
import json
import shutil
import glob
import requests
import subprocess
from pathlib import Path
from typing import Optional, Callable, Dict, Any
# 尝试导入modelscope和pycsghub
try:
from modelscope.hub.snapshot_download import snapshot_download
has_modelscope = True
except ImportError:
print("Warning: modelscope not installed, download functionality will be limited")
has_modelscope = False
try:
from pycsghub.upload_large_folder.main import upload_large_folder_internal, create_repo
from pycsghub.csghub_api import CsgHubApi
has_pycsghub = True
except ImportError:
print("Warning: pycsghub not installed, upload functionality will be limited")
has_pycsghub = False
class ModelManager:
"""模型管理器,用于下载和上传模型"""
def __init__(self):
"""初始化模型管理器"""
self.default_download_path = os.path.expanduser("~/models")
self.csghub_config = {
"base_url": "http://10.17.27.227:4997",
"token": "f5dad38a9426410aa861155cd184f84a",
"repo_type": "model",
"revision": "main"
}
# 确保默认下载路径存在
os.makedirs(self.default_download_path, exist_ok=True)
def download_model(self, model_id: str, local_path: str = None,
progress_callback: Optional[Callable] = None,
cancel_check: Optional[Callable] = None) -> str:
"""
从ModelScope下载模型
Args:
model_id: 模型ID,格式为"组织/模型名"
local_path: 本地保存路径,默认为~/models
progress_callback: 进度回调函数,接收(progress, detail)参数
cancel_check: 取消检查函数,返回True表示已取消
Returns:
str: 下载的模型路径
Raises:
Exception: 下载失败时抛出异常
"""
if not model_id:
raise ValueError("模型ID不能为空")
# 设置本地路径
if not local_path:
local_path = self.default_download_path
# 确保本地路径存在
os.makedirs(local_path, exist_ok=True)
# 模型保存的完整路径
model_path = os.path.join(local_path, model_id)
# 打印下载信息到终端
print(f"\n{'='*50}")
print(f"开始下载模型")
print(f"模型ID: {model_id}")
print(f"本地路径: {model_path}")
print(f"{'='*50}")
# 如果模型已存在,先删除
if os.path.exists(model_path):
print(f"模型已存在,正在删除: {model_path}")
shutil.rmtree(model_path)
# 调用回调函数
if progress_callback:
progress_callback(0, f"开始下载模型 {model_id}")
try:
if has_modelscope:
# 使用modelscope下载
print(f"使用modelscope下载模型: {model_id}")
print(f"下载目标路径: {model_path}")
# 检查是否已取消
if cancel_check and cancel_check():
print(f"下载任务已取消: {model_id}")
if progress_callback:
progress_callback(-1, {"error": "下载已取消"})
raise Exception("下载已取消")
# 注意:modelscope不直接支持进度回调,我们将在下载后计算文件数量
# 使用进程池执行snapshot_download,以便可以强制终止
import multiprocessing
# 定义下载函数
def download_func():
try:
return snapshot_download(
model_id=model_id,
cache_dir=local_path,
revision="master"
)
except Exception as e:
print(f"下载出错: {e}")
raise
# 创建进程
process = multiprocessing.Process(target=download_func)
process.daemon = True
process.start()
# 定期检查是否已取消
while process.is_alive():
if cancel_check and cancel_check():
print(f"下载任务已取消: {model_id}")
# 强制终止进程
process.terminate()
process.join(timeout=1)
if process.is_alive():
process.kill()
if progress_callback:
progress_callback(-1, {"error": "下载已取消"})
raise Exception("下载已取消")
time.sleep(0.1)
# 检查进程是否正常退出
if process.exitcode != 0:
raise Exception("下载进程异常退出")
print(f"modelscope下载完成,正在处理文件...")
# 下载完成后,计算文件数量并更新进度
if os.path.exists(model_path):
# 获取文件列表
all_files = []
for root, dirs, files in os.walk(model_path):
# 检查是否已取消
if cancel_check and cancel_check():
print(f"下载任务已取消: {model_id}")
if progress_callback:
progress_callback(-1, {"error": "下载已取消"})
raise Exception("下载已取消")
for file in files:
all_files.append(os.path.join(root, file))
file_count = len(all_files)
print(f"发现 {file_count} 个文件")
# 按文件数量更新进度
for i, file_path in enumerate(all_files):
# 检查是否已取消
if cancel_check and cancel_check():
print(f"下载任务已取消: {model_id}")
if progress_callback:
progress_callback(-1, {"error": "下载已取消"})
raise Exception("下载已取消")
progress = int((i + 1) / file_count * 100)
rel_path = os.path.relpath(file_path, model_path)
file_size = os.path.getsize(file_path)
print(f"[{progress}%] 已下载: {rel_path} ({self.get_dir_size(file_path)})")
if progress_callback:
progress_callback(progress, {
"file_count": i + 1,
"total_files": file_count,
"current_file": rel_path,
"file_size": file_size
})
time.sleep(0.05) # 减少延迟
else:
# 直接使用modelscope下载(不使用模拟模式)
print(f"modelscope未安装,无法下载模型: {model_id}")
raise Exception("modelscope未安装,无法下载模型")
if progress_callback:
progress_callback(100, {
"file_count": file_count,
"total_files": file_count,
"current_file": "完成",
"message": f"模型 {model_id} 下载完成"
})
# 打印下载完成信息
print(f"\n{'='*50}")
print(f"模型下载完成!")
print(f"模型ID: {model_id}")
print(f"下载路径: {model_path}")
print(f"文件数量: {file_count}")
print(f"{'='*50}")
return model_path
except Exception as e:
error_msg = str(e)
if progress_callback:
progress_callback(-1, {"error": error_msg})
# 打印错误信息
print(f"\n{'='*50}")
print(f"下载失败!")
print(f"模型ID: {model_id}")
print(f"错误信息: {error_msg}")
print(f"{'='*50}")
raise Exception(f"下载模型 {model_id} 失败: {error_msg}")
def upload_model(self, local_path: str, repo_id: str,
create_repo_flag: bool = True,
progress_callback: Optional[Callable] = None) -> Dict[str, Any]:
"""
上传模型到CsgHub
Args:
local_path: 本地模型路径
repo_id: 仓库ID
create_repo_flag: 是否创建仓库
progress_callback: 进度回调函数,接收(progress, detail)参数
Returns:
Dict[str, Any]: 上传结果
Raises:
Exception: 上传失败时抛出异常
"""
if not local_path or not os.path.exists(local_path):
raise ValueError(f"本地路径 {local_path} 不存在")
if not repo_id:
raise ValueError("仓库ID不能为空")
# 调用回调函数
if progress_callback:
progress_callback(0, f"开始上传模型到仓库 {repo_id}")
try:
# 首先获取所有文件列表,用于计算进度
all_files = []
for root, dirs, files in os.walk(local_path):
for file in files:
file_path = os.path.join(root, file)
all_files.append(file_path)
file_count = len(all_files)
if file_count == 0:
raise ValueError(f"本地路径 {local_path} 中没有文件")
if has_pycsghub:
# 使用pycsghub上传
csg_api = CsgHubApi()
use_full_repo_id = f"root/{repo_id}"
# 创建仓库
if create_repo_flag:
if progress_callback:
progress_callback(5, "正在创建仓库...")
create_repo(
api=csg_api,
repo_id=use_full_repo_id,
repo_type=self.csghub_config["repo_type"],
revision=self.csghub_config["revision"],
endpoint=self.csghub_config["base_url"],
token=self.csghub_config["token"]
)
# 上传模型
if progress_callback:
progress_callback(10, f"准备上传 {file_count} 个文件...")
# 创建一个自定义的进度回调函数
def custom_upload_callback(current_file_index, current_file_path, total_files):
"""自定义上传进度回调"""
progress = int((current_file_index + 1) / total_files * 90) + 10 # 10% - 100%
rel_path = os.path.relpath(current_file_path, local_path)
if progress_callback:
progress_callback(progress, f"上传中 {current_file_index + 1}/{total_files}: {rel_path}")
# 执行上传 - 注意:pycsghub可能不直接支持文件级别的进度回调
# 这里我们将在上传完成后模拟文件级别的进度
upload_large_folder_internal(
repo_id=use_full_repo_id,
local_path=local_path,
repo_type=self.csghub_config["repo_type"],
revision=self.csghub_config["revision"],
endpoint=self.csghub_config["base_url"],
token=self.csghub_config["token"],
allow_patterns=None,
ignore_patterns=None,
num_workers=1,
print_report=False,
print_report_every=1,
)
# 上传完成后,模拟文件级别的进度更新
for i, file_path in enumerate(all_files):
progress = int((i + 1) / file_count * 90) + 10 # 10% - 100%
rel_path = os.path.relpath(file_path, local_path)
if progress_callback:
progress_callback(progress, f"已上传 {i + 1}/{file_count}: {rel_path}")
time.sleep(0.05) # 模拟处理延迟
else:
# 直接使用pycsghub上传(不使用模拟模式)
print(f"pycsghub未安装,无法上传模型: {repo_id}")
raise Exception("pycsghub未安装,无法上传模型")
if progress_callback:
progress_callback(100, f"模型上传完成,仓库ID: {repo_id},共上传 {file_count} 个文件")
return {
"success": True,
"repo_id": repo_id,
"file_count": file_count,
"message": f"模型上传成功,共上传 {file_count} 个文件"
}
except Exception as e:
if progress_callback:
progress_callback(-1, f"上传失败: {str(e)}")
raise Exception(f"上传模型失败: {str(e)}")
def list_models(self, local_path: str = None) -> list:
"""
列出本地模型
Args:
local_path: 本地模型路径,默认为~/models
Returns:
list: 模型列表
"""
if not local_path:
local_path = self.default_download_path
if not os.path.exists(local_path):
print(f"Model path does not exist: {local_path}")
return []
models = []
try:
print(f"Listing models from: {local_path}")
items = os.listdir(local_path)
print(f"Found {len(items)} items in directory")
# 遍历一级目录
for item in items:
item_path = os.path.join(local_path, item)
print(f"Checking item: {item} (type: {'dir' if os.path.isdir(item_path) else 'file'})")
if os.path.isdir(item_path):
# 检查一级目录下的二级子目录
try:
sub_items = os.listdir(item_path)
print(f" Found {len(sub_items)} sub-items in {item}")
for sub_item in sub_items:
sub_item_path = os.path.join(item_path, sub_item)
if os.path.isdir(sub_item_path):
print(f" Checking sub-directory: {sub_item}")
# 检查是否有 README.md
has_readme = os.path.exists(os.path.join(sub_item_path, "README.md"))
# 检查是否有 .safetensors 或 .bin 文件
has_safetensors_or_bin = False
try:
for file in os.listdir(sub_item_path):
if file.endswith('.safetensors') or file.endswith('.bin'):
has_safetensors_or_bin = True
break
except Exception as e:
print(f" Error checking files in {sub_item_path}: {e}")
continue
print(f" - README.md: {has_readme}")
print(f" - has .safetensors or .bin: {has_safetensors_or_bin}")
# 判断是否为模型目录(必须有README.md和.safetensors/.bin文件)
if has_readme and has_safetensors_or_bin:
# 获取模型信息,使用前端期望的字段名
model_info = {
"id": sub_item, # 使用二级目录名作为id
"path": sub_item_path,
"size": self.get_dir_size(sub_item_path),
"status": "downloaded", # 默认状态
"downloadTime": self.get_dir_creation_time(sub_item_path),
"uploadTime": None,
"upload_repo_id": None,
"file_count": 0 # 计算文件数量
}
# 计算文件数量
file_count = 0
for root, dirs, files in os.walk(sub_item_path):
file_count += len(files)
model_info["file_count"] = file_count
models.append(model_info)
print(f" + Added as model: {sub_item} (in {item}/{sub_item})")
else:
print(f" - Skipped (missing required files)")
else:
print(f" - Skipped (not a directory): {sub_item}")
except Exception as e:
print(f" Error processing sub-directories in {item_path}: {e}")
continue
else:
print(f" - Skipped (not a directory): {item}")
print(f"Total models found: {len(models)}")
except Exception as e:
print(f"Error listing models: {e}")
return models
def get_dir_size(self, path: str) -> str:
"""
获取目录大小
Args:
path: 目录路径
Returns:
str: 格式化的大小字符串
"""
total_size = 0
for root, dirs, files in os.walk(path):
for file in files:
file_path = os.path.join(root, file)
total_size += os.path.getsize(file_path)
# 格式化大小
if total_size < 1024:
return f"{total_size}B"
elif total_size < 1024 * 1024:
return f"{total_size / 1024:.1f}KB"
elif total_size < 1024 * 1024 * 1024:
return f"{total_size / (1024 * 1024):.1f}MB"
else:
return f"{total_size / (1024 * 1024 * 1024):.1f}GB"
def get_dir_creation_time(self, path: str) -> str:
"""
获取目录创建时间
Args:
path: 目录路径
Returns:
str: 格式化的时间字符串
"""
try:
# 获取目录创建时间
stat = os.stat(path)
# 尝试获取创建时间,不同系统可能有不同的属性
if hasattr(stat, 'st_birthtime'): # macOS
creation_time = stat.st_birthtime
else: # Linux
creation_time = stat.st_mtime # 使用修改时间作为创建时间
return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(creation_time))
except Exception:
return "未知"
def delete_model(self, model_path: str) -> bool:
"""
删除本地模型
Args:
model_path: 模型完整路径
Returns:
bool: 是否删除成功
"""
if not model_path or not os.path.exists(model_path):
print(f"[DEBUG] 模型路径不存在: {model_path}")
return False
try:
print(f"[DEBUG] 开始删除模型目录: {model_path}")
shutil.rmtree(model_path)
print(f"[DEBUG] 模型目录删除成功: {model_path}")
return True
except Exception as e:
print(f"[DEBUG] 删除模型失败: {str(e)}")
return False
# 测试代码
if __name__ == "__main__":
# 直接在这里创建ModelManager实例,避免循环导入
class TestModelManager:
"""测试用的模型管理器"""
def list_models(self):
"""列出模型"""
return [
{"model_id": "test-model-1", "size": "1.2GB", "created_at": "2024-01-01 10:00:00"},
{"model_id": "test-model-2", "size": "800MB", "created_at": "2024-01-02 14:30:00"}
]
# 使用测试类
manager = TestModelManager()
# 测试列出模型
print("测试列出模型...")
models = manager.list_models()
for model in models:
print(f"模型: {model['model_id']}, 大小: {model['size']}, 创建时间: {model['created_at']}")
print("\n注意: 这是一个简化的测试模式,完整功能需要通过app.py运行")
\ No newline at end of file
{
"DeepSeek-OCR-2_/data/DataStore/models/exp/models/deepseek-ai": {
"model_id": "DeepSeek-OCR-2",
"local_path": "/data/DataStore/models/exp/models/deepseek-ai/DeepSeek-OCR-2",
"status": "downloaded",
"download_time": "2026-02-24T20:14:04.793600",
"upload_time": null,
"upload_repo_id": null
},
"moonshotai/Kimi-K2.5_/data/DataStore/models/exp/models": {
"model_id": "moonshotai/Kimi-K2.5",
"local_path": "/data/DataStore/models/exp/models/moonshotai/Kimi-K2.5",
"status": "failed",
"download_time": null,
"upload_time": null,
"upload_repo_id": null
},
"Qwen/Qwen3.5-397B-A17B-FP8_/data/DataStore/models/exp/models": {
"model_id": "Qwen/Qwen3.5-397B-A17B-FP8",
"local_path": "/data/DataStore/models/exp/models/Qwen/Qwen3.5-397B-A17B-FP8",
"status": "downloaded",
"download_time": "2026-02-25T05:44:05.750740",
"upload_time": null,
"upload_repo_id": null
},
"Kimi-K2___5_/data/DataStore/models/exp/models/moonshotai": {
"model_id": "Kimi-K2___5",
"local_path": "/data/DataStore/models/exp/models/moonshotai/Kimi-K2___5",
"status": "downloaded",
"download_time": "2026-02-25T09:08:36.229119",
"upload_time": null,
"upload_repo_id": null
},
"Qwen3.5-397B-A17B-FP8_/data/DataStore/models/exp/models/Qwen": {
"model_id": "Qwen3.5-397B-A17B-FP8",
"local_path": "/data/DataStore/models/exp/models/Qwen/Qwen3.5-397B-A17B-FP8",
"status": "uploaded",
"download_time": "2026-02-25T09:08:36.229180",
"upload_time": "2026-02-25T09:52:28.573598",
"upload_repo_id": "Qwen3.5-397B-A17B-FP8"
},
"moonshotai/Kimi-K2.5_/home/user/models": {
"model_id": "moonshotai/Kimi-K2.5",
"local_path": "/home/user/models/moonshotai/Kimi-K2.5",
"status": "failed",
"download_time": null,
"upload_time": null,
"upload_repo_id": null
},
"test/model_/tmp": {
"model_id": "test/model",
"local_path": "/tmp/test/model",
"status": "failed",
"download_time": null,
"upload_time": null,
"upload_repo_id": null
},
"Qwen/Qwen3.5-35B-A3B_/data/DataStore/models/exp/models": {
"model_id": "Qwen/Qwen3.5-35B-A3B",
"local_path": "/data/DataStore/models/exp/models/Qwen/Qwen3.5-35B-A3B",
"status": "failed",
"download_time": null,
"upload_time": null,
"upload_repo_id": null
},
"Qwen/Qwen3.5-27B_/data/DataStore/models/exp/models": {
"model_id": "Qwen/Qwen3.5-27B",
"local_path": "/data/DataStore/models/exp/models/Qwen/Qwen3.5-27B",
"status": "failed",
"download_time": null,
"upload_time": null,
"upload_repo_id": null
},
"Qwen/Qwen3.5-122B-A10B_/data/DataStore/models/exp/models": {
"model_id": "Qwen/Qwen3.5-122B-A10B",
"local_path": "/data/DataStore/models/exp/models/Qwen/Qwen3.5-122B-A10B",
"status": "failed",
"download_time": null,
"upload_time": null,
"upload_repo_id": null
},
"Kimi-K2.5_/data/DataStore/models/exp/models/moonshotai": {
"model_id": "Kimi-K2.5",
"local_path": "/data/DataStore/models/exp/models/moonshotai/Kimi-K2.5",
"status": "failed",
"download_time": "2026-02-26T00:46:23.724967",
"upload_time": null,
"upload_repo_id": null
},
"Kimi-K2___5_/data/DataStore/models/exp/models": {
"model_id": "Kimi-K2___5",
"local_path": "/data/DataStore/models/exp/models/Kimi-K2___5",
"status": "failed",
"download_time": null,
"upload_time": null,
"upload_repo_id": null
},
"Qwen3.5-27B_/data/DataStore/models/exp/models/Qwen": {
"model_id": "Qwen3.5-27B",
"local_path": "/data/DataStore/models/exp/models/Qwen/Qwen3.5-27B",
"status": "uploaded",
"download_time": "2026-02-26T15:12:41.148135",
"upload_time": "2026-02-26T15:18:30.857631",
"upload_repo_id": "Qwen3.5-27B"
},
"Qwen3.5-35B-A3B_/data/DataStore/models/exp/models/Qwen": {
"model_id": "Qwen3.5-35B-A3B",
"local_path": "/data/DataStore/models/exp/models/Qwen/Qwen3.5-35B-A3B",
"status": "uploaded",
"download_time": "2026-02-27T09:13:18.834637",
"upload_time": "2026-02-27T09:38:03.664882",
"upload_repo_id": "Qwen3.5-35B-A3B"
}
}
\ No newline at end of file
# Linux模型管理工具
一个基于Web的模型管理工具,用于在Linux环境下下载、上传和管理ModelScope模型,并支持上传到CsgHub平台。
## 功能特点
1. **模型下载**
- 支持通过模型ID下载ModelScope模型
- 支持多个模型ID批量下载(用英文逗号分隔)
- 可自定义本地存放路径,支持设置默认路径
- 实时显示下载进度
- 下载失败自动重试(最多10次)
2. **模型上传**
- 自动列出所有已下载但未上传的模型
- 支持批量上传和单个模型上传
- 实时显示上传进度
3. **模型管理**
- 列出所有已下载的模型
- 显示模型状态、大小、下载时间等信息
- 支持删除选中的模型
4. **配置管理**
- 设置默认模型存放路径
- 配置最大重试次数
- 配置CsgHub连接信息
## 技术栈
- **前端**:HTML5, CSS3, JavaScript, Tailwind CSS, Font Awesome
- **后端**:Python, Flask, Flask-SocketIO
- **数据存储**:SQLite
## 安装与运行
### 前提条件
- Python 3.6+
- pip
### 安装步骤
1. 克隆或下载本项目代码
2. 进入项目目录
```bash
cd model_manager_webapp
```
3. 运行启动脚本
```bash
chmod +x start.sh
./start.sh
```
启动脚本会自动:
- 创建虚拟环境
- 安装所需依赖
- 启动Web应用
4. 打开浏览器访问
```
http://localhost:5000
```
## 使用说明
### 下载模型
1. 在"模型下载"标签页中,输入模型ID(多个模型ID用英文逗号分隔)
2. 选择本地存放路径(可选,默认使用配置中的路径)
3. 点击"开始下载"按钮
4. 查看下载进度和状态
### 上传模型
1. 切换到"模型上传"标签页
2. 选择要上传的模型(可多选)
3. 点击"上传选中"按钮
4. 查看上传进度和状态
### 管理模型
1. 切换到"模型管理"标签页
2. 查看所有已下载的模型列表
3. 可通过点击删除图标删除模型
4. 对于已下载但未上传的模型,可点击上传图标单独上传
### 配置应用
1. 点击顶部导航栏的"配置"按钮
2. 修改所需配置项
3. 点击"保存配置"按钮
## 注意事项
1. 本应用目前是一个演示版本,实际的模型下载和上传功能需要集成ModelScope SDK和pycsghub库
2. 当前版本使用模拟数据展示功能,实际部署时需要替换为真实的下载和上传逻辑
3. 确保应用有足够的权限访问指定的模型存放路径
## 后续开发计划
1. 集成ModelScope SDK实现真实的模型下载
2. 集成pycsghub实现真实的模型上传
3. 添加用户认证功能
4. 支持更多模型源
5. 优化批量操作性能
\ No newline at end of file
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import sys
import json
import time
import uuid
import shutil
import threading
import subprocess
from datetime import datetime
from flask import Flask, request, jsonify, send_from_directory
from flask_cors import CORS
from flask_socketio import SocketIO, emit
# 确保可以导入自定义模块
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
# 导入模型管理模块
from model_manager import ModelManager
app = Flask(__name__, static_folder='static', template_folder='templates')
app.config['SECRET_KEY'] = 'secret!'
CORS(app)
socketio = SocketIO(app, cors_allowed_origins="*")
# 初始化模型管理器
model_manager = ModelManager()
# 任务状态
tasks = {}
# 线程存储
task_threads = {}
# 模型状态数据库
models_db = {}
# 数据库锁,用于避免多线程访问冲突
db_lock = threading.Lock()
# 数据库文件路径
db_file = 'models_db.json'
def save_models_db():
"""保存模型数据库到文件"""
global models_db
try:
print(f"[DEBUG] 开始保存数据库,当前线程: {threading.current_thread().name}")
# 不使用锁,避免可能的死锁
# with db_lock:
# with open(db_file, 'w') as f:
# json.dump(models_db, f, indent=2)
# 简化版本,直接写入
with open(db_file, 'w') as f:
json.dump(models_db, f, indent=2)
print(f"[DEBUG] 模型数据库已保存到 {db_file}")
except Exception as e:
print(f"[ERROR] 保存模型数据库失败: {str(e)}")
@app.route('/')
def index():
"""提供前端页面"""
return send_from_directory('../frontend', 'index.html')
@app.route('/api/download', methods=['POST'])
def download_model():
"""下载模型API"""
data = request.json
model_id = data.get('model_id')
local_path = data.get('local_path', '/home/user/models')
task_id = data.get('task_id')
if not model_id:
return jsonify({'error': '请提供有效的模型ID'}), 400
if not task_id:
task_id = f"task_{uuid.uuid4().hex}"
# 创建任务
tasks[task_id] = {
'task_id': task_id,
'model_id': model_id,
'local_path': local_path,
'type': 'download',
'status': 'pending',
'progress': 0,
'retry_count': 0,
'start_time': datetime.now().isoformat(),
'message': '准备下载...'
}
# 将模型添加到数据库或更新状态
model_key = f"{model_id}_{local_path}"
# 不使用锁,避免可能的死锁
# with db_lock:
if model_key not in models_db:
models_db[model_key] = {
'model_id': model_id,
'local_path': os.path.join(local_path, model_id),
'status': 'downloading',
'download_time': None,
'upload_time': None,
'upload_repo_id': None
}
else:
# 更新现有模型的状态为下载中
models_db[model_key]['status'] = 'downloading'
models_db[model_key]['download_time'] = None
# 清除之前的进度信息
models_db[model_key].pop('progress', None)
models_db[model_key].pop('message', None)
# 保存数据库
save_models_db()
print(f"[DEBUG] 模型状态已更新为下载中: {model_id}")
# 启动下载线程
thread = threading.Thread(target=download_models, args=([task_id],))
task_threads[task_id] = thread
thread.start()
return jsonify({'status': 'success', 'task_id': task_id}), 200
@app.route('/api/upload', methods=['POST'])
def upload_model():
"""上传模型API"""
try:
data = request.json
model_ids = data.get('model_ids', [])
create_repo_flag = data.get('create_repo_flag', True)
print(f"[DEBUG] 上传API被调用,模型IDs: {model_ids}, 创建仓库: {create_repo_flag}")
if not model_ids:
return jsonify({'error': '请选择要上传的模型'}), 400
# 为每个模型创建任务
task_ids = []
for model_id in model_ids:
# 查找模型信息
model_info = None
for key, model in models_db.items():
if model['model_id'] == model_id and model['status'] in ['downloaded', 'uploading']:
model_info = model
break
if not model_info:
print(f"[DEBUG] 模型 {model_id} 未找到或状态不允许上传")
continue
task_id = f"task_{uuid.uuid4().hex}"
tasks[task_id] = {
'task_id': task_id,
'model_id': model_id,
'local_path': model_info['local_path'],
'type': 'upload',
'status': 'pending',
'progress': 0,
'start_time': datetime.now().isoformat(),
'message': '准备上传...'
}
task_ids.append(task_id)
# 更新模型状态
model_info['status'] = 'uploading'
# 启动上传线程
thread = threading.Thread(target=upload_models, args=(task_ids, create_repo_flag))
# 为每个任务ID存储同一个线程
for task_id in task_ids:
task_threads[task_id] = thread
thread.start()
return jsonify({'task_ids': task_ids}), 200
except Exception as e:
print(f"[DEBUG] 上传API错误: {str(e)}")
return jsonify({'error': str(e)}), 500
@app.route('/api/delete', methods=['POST'])
def delete_model():
"""删除模型API"""
print("[DEBUG] 删除API被调用")
try:
data = request.json
print(f"[DEBUG] 删除API请求数据: {data}")
model_ids = data.get('model_ids', [])
if not model_ids:
print("[DEBUG] 未提供模型ID")
return jsonify({'error': '请选择要删除的模型'}), 400
deleted_models = []
errors = []
for model_id in model_ids:
# 查找模型信息
model_key = None
model_info = None
for key, model in models_db.items():
if model['model_id'] == model_id:
model_key = key
model_info = model
break
if not model_info:
errors.append(f"模型 {model_id} 不存在")
continue
# 检查模型状态
if model_info['status'] in ['downloading', 'uploading']:
errors.append(f"模型 {model_id} 正在进行下载/上传操作,无法删除")
continue
try:
# 使用模型管理器删除模型
print(f"[DEBUG] 调用模型管理器删除模型: {model_id}, 路径: {model_info['local_path']}")
success = model_manager.delete_model(model_info['local_path'])
if success:
# 从数据库中删除
with db_lock:
if model_key:
del models_db[model_key]
deleted_models.append(model_id)
print(f"[DEBUG] 模型 {model_id} 删除成功")
else:
errors.append(f"删除模型 {model_id} 失败: 模型管理器返回失败")
except Exception as e:
errors.append(f"删除模型 {model_id} 失败: {str(e)}")
print(f"[ERROR] 删除模型 {model_id} 异常: {str(e)}")
result = {
'deleted': deleted_models,
'errors': errors
}
# 如果有模型被删除,保存数据库
if deleted_models:
save_models_db()
return jsonify(result), 200
except Exception as e:
print(f"[ERROR] 删除API异常: {str(e)}")
return jsonify({'error': str(e)}), 500
@app.route('/api/models', methods=['GET'])
def get_models():
"""获取模型列表"""
try:
# 获取查询参数
status = request.args.get('status')
all_models = request.args.get('all', 'false').lower() == 'true'
model_path = request.args.get('path') # 获取用户指定的模型路径
print(f"API get_models called with: status={status}, all={all_models}, path={model_path}")
# 从本地文件系统获取模型列表,使用用户指定的路径或默认路径
local_models = model_manager.list_models(local_path=model_path)
# 合并本地模型和数据库中的模型信息
for local_model in local_models:
# 查找数据库中是否有该模型的信息
model_key = f"{local_model['id']}_{os.path.dirname(local_model['path'])}"
if model_key in models_db:
# 更新本地模型的状态信息
db_model = models_db[model_key]
# 保留数据库中的状态,不覆盖进行中的任务状态
local_model['status'] = db_model.get('status', 'downloaded')
local_model['download_time'] = db_model.get('download_time')
local_model['upload_time'] = db_model.get('upload_time')
local_model['upload_repo_id'] = db_model.get('upload_repo_id')
# 添加进度信息用于前端显示
if db_model.get('status') in ['downloading', 'uploading']:
local_model['progress'] = db_model.get('progress', 0)
local_model['message'] = db_model.get('message', '进行中...')
print(f"[DEBUG] 从数据库加载模型: {local_model['id']}, 状态: {local_model['status']}")
else:
# 如果模型不在数据库中,添加到数据库
models_db[model_key] = {
'model_id': local_model['id'],
'local_path': local_model['path'],
'status': 'downloaded',
'download_time': datetime.now().isoformat(),
'upload_time': None,
'upload_repo_id': None
}
print(f"[DEBUG] 新增模型到数据库: {local_model['id']}")
# 同时更新本地模型的状态信息
local_model['status'] = 'downloaded'
local_model['download_time'] = models_db[model_key]['download_time']
local_model['upload_time'] = None
local_model['upload_repo_id'] = None
# 及时保存数据库到文件
save_models_db()
# 根据状态筛选
if status == 'downloaded':
# 返回已下载但未上传的模型
models = [model for model in local_models if model.get('status') == 'downloaded']
else:
# 返回所有模型
models = local_models
# 格式化返回数据
result = {
'models': models
}
return jsonify(result), 200
except Exception as e:
print(f"Error getting models: {e}")
return jsonify({'error': str(e)}), 500
@app.route('/api/task/<task_id>', methods=['GET'])
def get_task_status(task_id):
"""获取任务状态"""
if task_id not in tasks:
return jsonify({'error': '任务不存在'}), 404
return jsonify(tasks[task_id]), 200
@app.route('/api/system/info', methods=['GET'])
def get_system_info():
"""获取系统信息"""
try:
# 获取操作系统信息
os_info = subprocess.check_output(['uname', '-a']).decode('utf-8').strip()
# 获取磁盘使用情况
disk_usage = subprocess.check_output(['df', '-h']).decode('utf-8')
# 获取内存使用情况
memory_usage = subprocess.check_output(['free', '-h']).decode('utf-8')
return jsonify({
'os': os_info,
'disk_usage': disk_usage,
'memory_usage': memory_usage
}), 200
except Exception as e:
return jsonify({'error': str(e)}), 500
@socketio.on('connect')
def handle_connect():
"""处理WebSocket连接"""
print('客户端已连接')
@socketio.on('disconnect')
def handle_disconnect():
"""处理WebSocket断开连接"""
pass # 静默处理断开连接,减少日志输出
@app.route('/api/download/cancel/<task_id>', methods=['POST'])
def cancel_download(task_id):
"""取消下载任务"""
try:
print(f"[DEBUG] 收到取消下载请求: {task_id}")
# 检查任务是否存在
if task_id in tasks:
# 从任务字典中移除任务
task = tasks.pop(task_id)
print(f"[DEBUG] 已取消下载任务: {task_id}, 模型ID: {task.get('model_id')}")
# 更新模型状态
model_id = task.get('model_id')
local_path = task.get('local_path')
if model_id and local_path:
model_key = f"{model_id}_{local_path}"
if model_key in models_db:
# 将状态从downloading改为failed
if models_db[model_key].get('status') == 'downloading':
models_db[model_key]['status'] = 'failed'
models_db[model_key].pop('progress', None)
models_db[model_key].pop('message', None)
save_models_db()
print(f"[DEBUG] 已更新模型状态为失败: {model_id}")
# 从线程存储中移除
if task_id in task_threads:
del task_threads[task_id]
return jsonify({'success': True, 'message': '下载任务已取消'})
else:
print(f"[DEBUG] 任务不存在: {task_id}")
return jsonify({'success': False, 'message': '任务不存在'}), 404
except Exception as e:
print(f"[DEBUG] 取消下载失败: {str(e)}")
return jsonify({'success': False, 'message': f'取消失败: {str(e)}'}), 500
@app.route('/api/upload/cancel/<task_id>', methods=['POST'])
def cancel_upload(task_id):
"""取消上传任务"""
try:
print(f"[DEBUG] 收到取消上传请求: {task_id}")
# 检查任务是否存在
if task_id in tasks:
# 从任务字典中移除任务
task = tasks.pop(task_id)
print(f"[DEBUG] 已取消上传任务: {task_id}, 模型ID: {task.get('model_id')}")
# 更新模型状态
model_id = task.get('model_id')
local_path = task.get('local_path')
if model_id and local_path:
model_key = f"{model_id}_{local_path}"
if model_key in models_db:
# 将状态从uploading改为downloaded
if models_db[model_key].get('status') == 'uploading':
models_db[model_key]['status'] = 'downloaded'
models_db[model_key].pop('progress', None)
models_db[model_key].pop('message', None)
save_models_db()
print(f"[DEBUG] 已更新模型状态为已下载: {model_id}")
return jsonify({'success': True, 'message': '上传任务已取消'})
else:
print(f"[DEBUG] 任务不存在: {task_id}")
return jsonify({'success': False, 'message': '任务不存在'}), 404
except Exception as e:
print(f"[DEBUG] 取消上传失败: {str(e)}")
return jsonify({'success': False, 'message': f'取消失败: {str(e)}'}), 500
@socketio.on('subscribe_task')
def handle_subscribe_task(data):
"""订阅任务进度更新"""
task_id = data.get('task_id')
if task_id in tasks:
# 立即发送当前状态
emit('task_update', tasks[task_id])
def download_models(task_ids):
"""下载模型线程"""
import concurrent.futures
for task_id in task_ids:
task = tasks.get(task_id)
if not task:
continue
model_id = task['model_id']
local_path = task['local_path']
# 更新任务状态
task['status'] = 'downloading'
task['message'] = f'开始下载模型 {model_id}'
socketio.emit('task_update', task)
# 尝试下载模型
max_retries = 10
retry_count = 0
# 使用默认参数捕获task_id,避免lambda闭包问题
def make_progress_callback(tid):
def progress_callback(progress, detail):
update_download_progress(tid, progress, detail)
return progress_callback
while retry_count < max_retries:
# 检查任务是否已被取消
if task_id not in tasks:
print(f"[INFO] 下载任务已取消: {task_id}")
return
try:
# 创建取消检查函数
def cancel_check():
return task_id not in tasks
# 使用线程池执行下载,以便可以取消
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
# 提交下载任务
future = executor.submit(
model_manager.download_model,
model_id=model_id,
local_path=local_path,
progress_callback=make_progress_callback(task_id),
cancel_check=cancel_check
)
# 等待下载完成,同时检查任务是否被取消
while not future.done():
# 检查任务是否已被取消
if task_id not in tasks:
print(f"[INFO] 下载任务已取消: {task_id}")
# 取消future
future.cancel()
return
# 短暂睡眠,避免CPU占用过高
time.sleep(0.1)
# 获取下载结果
model_path = future.result()
# 下载成功
task['status'] = 'completed'
task['progress'] = 100
task['message'] = f'模型 {model_id} 下载完成'
# 发送下载完成事件
socketio.emit('download_complete', {
'taskId': task_id,
'modelId': model_id
})
socketio.emit('task_update', task)
# 更新模型状态
model_key = f"{model_id}_{local_path}"
if model_key in models_db:
# 只有当状态不是已上传时才更新为已下载
if models_db[model_key].get('status') != 'uploaded':
models_db[model_key]['status'] = 'downloaded'
models_db[model_key]['download_time'] = datetime.now().isoformat()
# 清除进度信息
models_db[model_key].pop('progress', None)
models_db[model_key].pop('message', None)
break
except Exception as e:
# 检查任务是否已被取消
if task_id not in tasks:
print(f"[INFO] 下载任务已取消: {task_id}")
return
retry_count += 1
task['retry_count'] = retry_count
task['message'] = f'下载失败 (尝试 {retry_count}/{max_retries}): {str(e)}'
socketio.emit('task_update', task)
if retry_count < max_retries:
# 等待一段时间后重试
time.sleep(5)
else:
# 达到最大重试次数
task['status'] = 'failed'
task['message'] = f'达到最大重试次数,下载失败: {str(e)}'
# 发送下载失败事件
socketio.emit('download_failed', {
'taskId': task_id,
'modelId': model_id,
'error': str(e)
})
socketio.emit('task_update', task)
# 更新模型状态
model_key = f"{model_id}_{local_path}"
if model_key in models_db:
# 只有当状态不是已上传时才更新为失败
if models_db[model_key].get('status') != 'uploaded':
models_db[model_key]['status'] = 'failed'
models_db[model_key]['download_time'] = None
# 清除进度信息
models_db[model_key].pop('progress', None)
models_db[model_key].pop('message', None)
def update_download_progress(task_id, progress, detail=None):
"""更新下载进度"""
if task_id not in tasks:
return
task = tasks[task_id]
task['progress'] = progress
if detail:
if isinstance(detail, dict):
# 新的进度回调格式
task['message'] = f"正在下载: {detail.get('current_file', 'unknown')} ({detail.get('file_count', 0)}/{detail.get('total_files', 0)})"
# 通过WebSocket发送进度更新
socketio.emit('download_progress', {
'taskId': task_id,
'progress': progress,
'fileCount': detail.get('file_count', 0),
'totalFiles': detail.get('total_files', 0),
'currentFile': detail.get('current_file', 'unknown'),
'fileSize': detail.get('file_size', 0)
})
else:
# 旧的进度回调格式
task['message'] = detail
socketio.emit('download_progress', {
'taskId': task_id,
'progress': progress,
'message': detail
})
# 发送通用任务更新
socketio.emit('task_update', task)
def upload_models(task_ids, create_repo_flag=True):
"""上传模型线程"""
for task_id in task_ids:
task = tasks.get(task_id)
if not task:
continue
model_id = task['model_id']
local_path = task['local_path']
# 更新任务状态
task['status'] = 'uploading'
task['message'] = f'开始上传模型 {model_id}'
socketio.emit('task_update', task)
# 使用默认参数捕获task_id,避免lambda闭包问题
def make_upload_progress_callback(tid):
def progress_callback(progress, detail):
update_upload_progress(tid, progress, detail)
return progress_callback
try:
# 检查任务是否已被取消
if task_id not in tasks:
print(f"[INFO] 上传任务已取消: {task_id}")
return
# 调用模型管理器上传模型
repo_id = os.path.basename(model_id)
model_manager.upload_model(
local_path=local_path,
repo_id=repo_id,
create_repo_flag=create_repo_flag,
progress_callback=make_upload_progress_callback(task_id)
)
# 上传成功
task['status'] = 'completed'
task['progress'] = 100
task['message'] = f'模型 {model_id} 上传完成'
socketio.emit('task_update', task)
# 更新模型状态
for key, model in models_db.items():
if model['model_id'] == model_id:
model['status'] = 'uploaded'
model['upload_time'] = datetime.now().isoformat()
model['upload_repo_id'] = repo_id
break
except Exception as e:
# 检查任务是否已被取消
if task_id not in tasks:
print(f"[INFO] 上传任务已取消: {task_id}")
return
task['status'] = 'failed'
task['message'] = f'上传失败: {str(e)}'
socketio.emit('task_update', task)
# 更新模型状态
for key, model in models_db.items():
if model['model_id'] == model_id:
model['status'] = 'downloaded' # 恢复为已下载状态
break
def update_upload_progress(task_id, progress, detail=None):
"""更新上传进度"""
if task_id not in tasks:
return
task = tasks[task_id]
task['progress'] = progress
if detail:
task['message'] = detail
socketio.emit('task_update', task)
if __name__ == '__main__':
# 加载模型数据库
db_file = 'models_db.json'
if os.path.exists(db_file):
try:
with open(db_file, 'r') as f:
models_db = json.load(f)
except Exception as e:
print(f"加载模型数据库失败: {str(e)}")
# 清理数据库中状态为 downloading 或 uploading 的任务(可能是之前未正常关闭的任务)
for model_key, model_info in models_db.items():
if model_info.get('status') in ['downloading', 'uploading']:
print(f"[INFO] 清理异常状态模型: {model_info.get('model_id')}, 状态: {model_info.get('status')}")
model_info['status'] = 'failed'
model_info.pop('progress', None)
model_info.pop('message', None)
save_models_db()
# 启动服务器
socketio.run(app, host='0.0.0.0', port=2026, debug=False)
# 保存模型数据库
try:
with open(db_file, 'w') as f:
json.dump(models_db, f, indent=2)
except Exception as e:
print(f"保存模型数据库失败: {str(e)}")
\ No newline at end of file
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import sys
import time
import json
import shutil
import glob
import requests
import subprocess
from pathlib import Path
from typing import Optional, Callable, Dict, Any
# 尝试导入modelscope和pycsghub
try:
from modelscope.hub.snapshot_download import snapshot_download
has_modelscope = True
except ImportError:
print("Warning: modelscope not installed, download functionality will be limited")
has_modelscope = False
try:
from pycsghub.upload_large_folder.main import upload_large_folder_internal, create_repo
from pycsghub.csghub_api import CsgHubApi
has_pycsghub = True
except ImportError:
print("Warning: pycsghub not installed, upload functionality will be limited")
has_pycsghub = False
class ModelManager:
"""模型管理器,用于下载和上传模型"""
def __init__(self):
"""初始化模型管理器"""
self.default_download_path = os.path.expanduser("~/models")
self.csghub_config = {
"base_url": "http://10.17.27.227:4997",
"token": "f5dad38a9426410aa861155cd184f84a",
"repo_type": "model",
"revision": "main"
}
# 确保默认下载路径存在
os.makedirs(self.default_download_path, exist_ok=True)
def download_model(self, model_id: str, local_path: str = None,
progress_callback: Optional[Callable] = None,
cancel_check: Optional[Callable] = None) -> str:
"""
从ModelScope下载模型
Args:
model_id: 模型ID,格式为"组织/模型名"
local_path: 本地保存路径,默认为~/models
progress_callback: 进度回调函数,接收(progress, detail)参数
cancel_check: 取消检查函数,返回True表示已取消
Returns:
str: 下载的模型路径
Raises:
Exception: 下载失败时抛出异常
"""
if not model_id:
raise ValueError("模型ID不能为空")
# 设置本地路径
if not local_path:
local_path = self.default_download_path
# 确保本地路径存在
os.makedirs(local_path, exist_ok=True)
# 模型保存的完整路径
model_path = os.path.join(local_path, model_id)
# 打印下载信息到终端
print(f"\n{'='*50}")
print(f"开始下载模型")
print(f"模型ID: {model_id}")
print(f"本地路径: {model_path}")
print(f"{'='*50}")
# 如果模型已存在,先删除
if os.path.exists(model_path):
print(f"模型已存在,正在删除: {model_path}")
shutil.rmtree(model_path)
# 调用回调函数
if progress_callback:
progress_callback(0, f"开始下载模型 {model_id}")
try:
if has_modelscope:
# 使用modelscope下载
print(f"使用modelscope下载模型: {model_id}")
print(f"下载目标路径: {model_path}")
# 检查是否已取消
if cancel_check and cancel_check():
print(f"下载任务已取消: {model_id}")
if progress_callback:
progress_callback(-1, {"error": "下载已取消"})
raise Exception("下载已取消")
# 注意:modelscope不直接支持进度回调,我们将在下载后计算文件数量
# 使用进程池执行snapshot_download,以便可以强制终止
import multiprocessing
# 定义下载函数
def download_func():
try:
return snapshot_download(
model_id=model_id,
cache_dir=local_path,
revision="master"
)
except Exception as e:
print(f"下载出错: {e}")
raise
# 创建进程
process = multiprocessing.Process(target=download_func)
process.daemon = True
process.start()
# 定期检查是否已取消
while process.is_alive():
if cancel_check and cancel_check():
print(f"下载任务已取消: {model_id}")
# 强制终止进程
process.terminate()
process.join(timeout=1)
if process.is_alive():
process.kill()
if progress_callback:
progress_callback(-1, {"error": "下载已取消"})
raise Exception("下载已取消")
time.sleep(0.1)
# 检查进程是否正常退出
if process.exitcode != 0:
raise Exception("下载进程异常退出")
print(f"modelscope下载完成,正在处理文件...")
# 下载完成后,计算文件数量并更新进度
if os.path.exists(model_path):
# 获取文件列表
all_files = []
for root, dirs, files in os.walk(model_path):
# 检查是否已取消
if cancel_check and cancel_check():
print(f"下载任务已取消: {model_id}")
if progress_callback:
progress_callback(-1, {"error": "下载已取消"})
raise Exception("下载已取消")
for file in files:
all_files.append(os.path.join(root, file))
file_count = len(all_files)
print(f"发现 {file_count} 个文件")
# 按文件数量更新进度
for i, file_path in enumerate(all_files):
# 检查是否已取消
if cancel_check and cancel_check():
print(f"下载任务已取消: {model_id}")
if progress_callback:
progress_callback(-1, {"error": "下载已取消"})
raise Exception("下载已取消")
progress = int((i + 1) / file_count * 100)
rel_path = os.path.relpath(file_path, model_path)
file_size = os.path.getsize(file_path)
print(f"[{progress}%] 已下载: {rel_path} ({self.get_dir_size(file_path)})")
if progress_callback:
progress_callback(progress, {
"file_count": i + 1,
"total_files": file_count,
"current_file": rel_path,
"file_size": file_size
})
time.sleep(0.05) # 减少延迟
else:
# 直接使用modelscope下载(不使用模拟模式)
print(f"modelscope未安装,无法下载模型: {model_id}")
raise Exception("modelscope未安装,无法下载模型")
if progress_callback:
progress_callback(100, {
"file_count": file_count,
"total_files": file_count,
"current_file": "完成",
"message": f"模型 {model_id} 下载完成"
})
# 打印下载完成信息
print(f"\n{'='*50}")
print(f"模型下载完成!")
print(f"模型ID: {model_id}")
print(f"下载路径: {model_path}")
print(f"文件数量: {file_count}")
print(f"{'='*50}")
return model_path
except Exception as e:
error_msg = str(e)
if progress_callback:
progress_callback(-1, {"error": error_msg})
# 打印错误信息
print(f"\n{'='*50}")
print(f"下载失败!")
print(f"模型ID: {model_id}")
print(f"错误信息: {error_msg}")
print(f"{'='*50}")
raise Exception(f"下载模型 {model_id} 失败: {error_msg}")
def upload_model(self, local_path: str, repo_id: str,
create_repo_flag: bool = True,
progress_callback: Optional[Callable] = None) -> Dict[str, Any]:
"""
上传模型到CsgHub
Args:
local_path: 本地模型路径
repo_id: 仓库ID
create_repo_flag: 是否创建仓库
progress_callback: 进度回调函数,接收(progress, detail)参数
Returns:
Dict[str, Any]: 上传结果
Raises:
Exception: 上传失败时抛出异常
"""
if not local_path or not os.path.exists(local_path):
raise ValueError(f"本地路径 {local_path} 不存在")
if not repo_id:
raise ValueError("仓库ID不能为空")
# 调用回调函数
if progress_callback:
progress_callback(0, f"开始上传模型到仓库 {repo_id}")
try:
# 首先获取所有文件列表,用于计算进度
all_files = []
for root, dirs, files in os.walk(local_path):
for file in files:
file_path = os.path.join(root, file)
all_files.append(file_path)
file_count = len(all_files)
if file_count == 0:
raise ValueError(f"本地路径 {local_path} 中没有文件")
if has_pycsghub:
# 使用pycsghub上传
csg_api = CsgHubApi()
use_full_repo_id = f"root/{repo_id}"
# 创建仓库
if create_repo_flag:
if progress_callback:
progress_callback(5, "正在创建仓库...")
create_repo(
api=csg_api,
repo_id=use_full_repo_id,
repo_type=self.csghub_config["repo_type"],
revision=self.csghub_config["revision"],
endpoint=self.csghub_config["base_url"],
token=self.csghub_config["token"]
)
# 上传模型
if progress_callback:
progress_callback(10, f"准备上传 {file_count} 个文件...")
# 创建一个自定义的进度回调函数
def custom_upload_callback(current_file_index, current_file_path, total_files):
"""自定义上传进度回调"""
progress = int((current_file_index + 1) / total_files * 90) + 10 # 10% - 100%
rel_path = os.path.relpath(current_file_path, local_path)
if progress_callback:
progress_callback(progress, f"上传中 {current_file_index + 1}/{total_files}: {rel_path}")
# 执行上传 - 注意:pycsghub可能不直接支持文件级别的进度回调
# 这里我们将在上传完成后模拟文件级别的进度
upload_large_folder_internal(
repo_id=use_full_repo_id,
local_path=local_path,
repo_type=self.csghub_config["repo_type"],
revision=self.csghub_config["revision"],
endpoint=self.csghub_config["base_url"],
token=self.csghub_config["token"],
allow_patterns=None,
ignore_patterns=None,
num_workers=1,
print_report=False,
print_report_every=1,
)
# 上传完成后,模拟文件级别的进度更新
for i, file_path in enumerate(all_files):
progress = int((i + 1) / file_count * 90) + 10 # 10% - 100%
rel_path = os.path.relpath(file_path, local_path)
if progress_callback:
progress_callback(progress, f"已上传 {i + 1}/{file_count}: {rel_path}")
time.sleep(0.05) # 模拟处理延迟
else:
# 直接使用pycsghub上传(不使用模拟模式)
print(f"pycsghub未安装,无法上传模型: {repo_id}")
raise Exception("pycsghub未安装,无法上传模型")
if progress_callback:
progress_callback(100, f"模型上传完成,仓库ID: {repo_id},共上传 {file_count} 个文件")
return {
"success": True,
"repo_id": repo_id,
"file_count": file_count,
"message": f"模型上传成功,共上传 {file_count} 个文件"
}
except Exception as e:
if progress_callback:
progress_callback(-1, f"上传失败: {str(e)}")
raise Exception(f"上传模型失败: {str(e)}")
def list_models(self, local_path: str = None) -> list:
"""
列出本地模型
Args:
local_path: 本地模型路径,默认为~/models
Returns:
list: 模型列表
"""
if not local_path:
local_path = self.default_download_path
if not os.path.exists(local_path):
print(f"Model path does not exist: {local_path}")
return []
models = []
try:
print(f"Listing models from: {local_path}")
items = os.listdir(local_path)
print(f"Found {len(items)} items in directory")
# 遍历一级目录
for item in items:
item_path = os.path.join(local_path, item)
print(f"Checking item: {item} (type: {'dir' if os.path.isdir(item_path) else 'file'})")
if os.path.isdir(item_path):
# 检查一级目录下的二级子目录
try:
sub_items = os.listdir(item_path)
print(f" Found {len(sub_items)} sub-items in {item}")
for sub_item in sub_items:
sub_item_path = os.path.join(item_path, sub_item)
if os.path.isdir(sub_item_path):
print(f" Checking sub-directory: {sub_item}")
# 检查是否有 README.md
has_readme = os.path.exists(os.path.join(sub_item_path, "README.md"))
# 检查是否有 .safetensors 或 .bin 文件
has_safetensors_or_bin = False
try:
for file in os.listdir(sub_item_path):
if file.endswith('.safetensors') or file.endswith('.bin'):
has_safetensors_or_bin = True
break
except Exception as e:
print(f" Error checking files in {sub_item_path}: {e}")
continue
print(f" - README.md: {has_readme}")
print(f" - has .safetensors or .bin: {has_safetensors_or_bin}")
# 判断是否为模型目录(必须有README.md和.safetensors/.bin文件)
if has_readme and has_safetensors_or_bin:
# 获取模型信息,使用前端期望的字段名
model_info = {
"id": sub_item, # 使用二级目录名作为id
"path": sub_item_path,
"size": self.get_dir_size(sub_item_path),
"status": "downloaded", # 默认状态
"downloadTime": self.get_dir_creation_time(sub_item_path),
"uploadTime": None,
"upload_repo_id": None,
"file_count": 0 # 计算文件数量
}
# 计算文件数量
file_count = 0
for root, dirs, files in os.walk(sub_item_path):
file_count += len(files)
model_info["file_count"] = file_count
models.append(model_info)
print(f" + Added as model: {sub_item} (in {item}/{sub_item})")
else:
print(f" - Skipped (missing required files)")
else:
print(f" - Skipped (not a directory): {sub_item}")
except Exception as e:
print(f" Error processing sub-directories in {item_path}: {e}")
continue
else:
print(f" - Skipped (not a directory): {item}")
print(f"Total models found: {len(models)}")
except Exception as e:
print(f"Error listing models: {e}")
return models
def get_dir_size(self, path: str) -> str:
"""
获取目录大小
Args:
path: 目录路径
Returns:
str: 格式化的大小字符串
"""
total_size = 0
for root, dirs, files in os.walk(path):
for file in files:
file_path = os.path.join(root, file)
total_size += os.path.getsize(file_path)
# 格式化大小
if total_size < 1024:
return f"{total_size}B"
elif total_size < 1024 * 1024:
return f"{total_size / 1024:.1f}KB"
elif total_size < 1024 * 1024 * 1024:
return f"{total_size / (1024 * 1024):.1f}MB"
else:
return f"{total_size / (1024 * 1024 * 1024):.1f}GB"
def get_dir_creation_time(self, path: str) -> str:
"""
获取目录创建时间
Args:
path: 目录路径
Returns:
str: 格式化的时间字符串
"""
try:
# 获取目录创建时间
stat = os.stat(path)
# 尝试获取创建时间,不同系统可能有不同的属性
if hasattr(stat, 'st_birthtime'): # macOS
creation_time = stat.st_birthtime
else: # Linux
creation_time = stat.st_mtime # 使用修改时间作为创建时间
return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(creation_time))
except Exception:
return "未知"
def delete_model(self, model_path: str) -> bool:
"""
删除本地模型
Args:
model_path: 模型完整路径
Returns:
bool: 是否删除成功
"""
if not model_path or not os.path.exists(model_path):
print(f"[DEBUG] 模型路径不存在: {model_path}")
return False
try:
print(f"[DEBUG] 开始删除模型目录: {model_path}")
shutil.rmtree(model_path)
print(f"[DEBUG] 模型目录删除成功: {model_path}")
return True
except Exception as e:
print(f"[DEBUG] 删除模型失败: {str(e)}")
return False
# 测试代码
if __name__ == "__main__":
# 直接在这里创建ModelManager实例,避免循环导入
class TestModelManager:
"""测试用的模型管理器"""
def list_models(self):
"""列出模型"""
return [
{"model_id": "test-model-1", "size": "1.2GB", "created_at": "2024-01-01 10:00:00"},
{"model_id": "test-model-2", "size": "800MB", "created_at": "2024-01-02 14:30:00"}
]
# 使用测试类
manager = TestModelManager()
# 测试列出模型
print("测试列出模型...")
models = manager.list_models()
for model in models:
print(f"模型: {model['model_id']}, 大小: {model['size']}, 创建时间: {model['created_at']}")
print("\n注意: 这是一个简化的测试模式,完整功能需要通过app.py运行")
\ No newline at end of file
{
"DeepSeek-OCR-2_/data/DataStore/models/exp/models/deepseek-ai": {
"model_id": "DeepSeek-OCR-2",
"local_path": "/data/DataStore/models/exp/models/deepseek-ai/DeepSeek-OCR-2",
"status": "downloaded",
"download_time": "2026-02-24T20:14:04.793600",
"upload_time": null,
"upload_repo_id": null
},
"moonshotai/Kimi-K2.5_/data/DataStore/models/exp/models": {
"model_id": "moonshotai/Kimi-K2.5",
"local_path": "/data/DataStore/models/exp/models/moonshotai/Kimi-K2.5",
"status": "failed",
"download_time": null,
"upload_time": null,
"upload_repo_id": null
},
"Qwen/Qwen3.5-397B-A17B-FP8_/data/DataStore/models/exp/models": {
"model_id": "Qwen/Qwen3.5-397B-A17B-FP8",
"local_path": "/data/DataStore/models/exp/models/Qwen/Qwen3.5-397B-A17B-FP8",
"status": "downloaded",
"download_time": "2026-02-25T05:44:05.750740",
"upload_time": null,
"upload_repo_id": null
},
"Kimi-K2___5_/data/DataStore/models/exp/models/moonshotai": {
"model_id": "Kimi-K2___5",
"local_path": "/data/DataStore/models/exp/models/moonshotai/Kimi-K2___5",
"status": "downloaded",
"download_time": "2026-02-25T09:08:36.229119",
"upload_time": null,
"upload_repo_id": null
},
"Qwen3.5-397B-A17B-FP8_/data/DataStore/models/exp/models/Qwen": {
"model_id": "Qwen3.5-397B-A17B-FP8",
"local_path": "/data/DataStore/models/exp/models/Qwen/Qwen3.5-397B-A17B-FP8",
"status": "uploaded",
"download_time": "2026-02-25T09:08:36.229180",
"upload_time": "2026-02-25T09:52:28.573598",
"upload_repo_id": "Qwen3.5-397B-A17B-FP8"
},
"moonshotai/Kimi-K2.5_/home/user/models": {
"model_id": "moonshotai/Kimi-K2.5",
"local_path": "/home/user/models/moonshotai/Kimi-K2.5",
"status": "failed",
"download_time": null,
"upload_time": null,
"upload_repo_id": null
},
"test/model_/tmp": {
"model_id": "test/model",
"local_path": "/tmp/test/model",
"status": "failed",
"download_time": null,
"upload_time": null,
"upload_repo_id": null
},
"Qwen/Qwen3.5-35B-A3B_/data/DataStore/models/exp/models": {
"model_id": "Qwen/Qwen3.5-35B-A3B",
"local_path": "/data/DataStore/models/exp/models/Qwen/Qwen3.5-35B-A3B",
"status": "failed",
"download_time": null,
"upload_time": null,
"upload_repo_id": null
},
"Qwen/Qwen3.5-27B_/data/DataStore/models/exp/models": {
"model_id": "Qwen/Qwen3.5-27B",
"local_path": "/data/DataStore/models/exp/models/Qwen/Qwen3.5-27B",
"status": "failed",
"download_time": null,
"upload_time": null,
"upload_repo_id": null
},
"Qwen/Qwen3.5-122B-A10B_/data/DataStore/models/exp/models": {
"model_id": "Qwen/Qwen3.5-122B-A10B",
"local_path": "/data/DataStore/models/exp/models/Qwen/Qwen3.5-122B-A10B",
"status": "failed",
"download_time": null,
"upload_time": null,
"upload_repo_id": null
},
"Kimi-K2.5_/data/DataStore/models/exp/models/moonshotai": {
"model_id": "Kimi-K2.5",
"local_path": "/data/DataStore/models/exp/models/moonshotai/Kimi-K2.5",
"status": "failed",
"download_time": "2026-02-26T00:46:23.724967",
"upload_time": null,
"upload_repo_id": null
},
"Kimi-K2___5_/data/DataStore/models/exp/models": {
"model_id": "Kimi-K2___5",
"local_path": "/data/DataStore/models/exp/models/Kimi-K2___5",
"status": "failed",
"download_time": null,
"upload_time": null,
"upload_repo_id": null
},
"Qwen3.5-27B_/data/DataStore/models/exp/models/Qwen": {
"model_id": "Qwen3.5-27B",
"local_path": "/data/DataStore/models/exp/models/Qwen/Qwen3.5-27B",
"status": "uploaded",
"download_time": "2026-02-26T15:12:41.148135",
"upload_time": "2026-02-26T15:18:30.857631",
"upload_repo_id": "Qwen3.5-27B"
},
"Qwen3.5-35B-A3B_/data/DataStore/models/exp/models/Qwen": {
"model_id": "Qwen3.5-35B-A3B",
"local_path": "/data/DataStore/models/exp/models/Qwen/Qwen3.5-35B-A3B",
"status": "uploaded",
"download_time": "2026-02-27T09:13:18.834637",
"upload_time": "2026-02-27T09:38:03.664882",
"upload_repo_id": "Qwen3.5-35B-A3B"
}
}
\ No newline at end of file
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Linux模型管理工具</title>
<!-- Tailwind CSS v3 -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Font Awesome -->
<link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
<!-- Chart.js -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.8/dist/chart.umd.min.js"></script>
<!-- Socket.IO Client -->
<script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script>
<!-- 统一的 Tailwind 配置 -->
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#0ea5e9',
secondary: '#6366f1',
success: '#10b981',
warning: '#f59e0b',
danger: '#ef4444',
dark: '#1e293b',
'dark-light': '#334155',
'dark-lighter': '#475569'
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
},
animation: {
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
}
}
}
}
</script>
<style type="text/tailwindcss">
@layer utilities {
.content-auto {
content-visibility: auto;
}
.glass-effect {
background: rgba(30, 41, 59, 0.7);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.progress-bar-glow {
box-shadow: 0 0 10px theme('colors.primary'), 0 0 20px theme('colors.primary');
}
.sidebar-icon {
@apply relative flex items-center justify-center h-12 w-12 mt-2 mb-2 mx-auto shadow-lg
bg-dark-light text-primary hover:bg-primary hover:text-white
rounded-3xl hover:rounded-xl transition-all duration-300 ease-linear
cursor-pointer;
}
.sidebar-tooltip {
@apply absolute w-auto p-2 m-2 min-w-max left-14 rounded-md shadow-md
text-white bg-dark-lighter
text-xs font-bold transition-all duration-100 scale-0 origin-left z-50;
}
.sidebar-hr {
@apply bg-dark-lighter border border-dark-lighter rounded-full mx-2;
}
.main-container {
@apply flex flex-col md:flex-row h-screen bg-dark text-white overflow-hidden;
}
.sidebar {
@apply bg-dark-light w-full md:w-16 flex flex-col items-center
md:items-start md:py-8 md:px-2;
}
.main-content {
@apply flex-1 flex flex-col overflow-hidden;
}
.top-bar {
@apply glass-effect flex justify-between items-center p-4;
}
.content-area {
@apply flex-1 overflow-y-auto p-6 bg-gradient-to-br from-dark to-dark-light;
}
.card {
@apply glass-effect rounded-xl p-6 shadow-lg mb-6;
}
.btn-primary {
@apply bg-primary hover:bg-primary/80 text-white font-bold py-2 px-4 rounded-lg
transition-all duration-300 transform hover:scale-105 focus:outline-none focus:ring-2
focus:ring-primary focus:ring-opacity-50;
}
.btn-secondary {
@apply bg-secondary hover:bg-secondary/80 text-white font-bold py-2 px-4 rounded-lg
transition-all duration-300 transform hover:scale-105 focus:outline-none focus:ring-2
focus:ring-secondary focus:ring-opacity-50;
}
.btn-danger {
@apply bg-danger hover:bg-danger/80 text-white font-bold py-2 px-4 rounded-lg
transition-all duration-300 transform hover:scale-105 focus:outline-none focus:ring-2
focus:ring-danger focus:ring-opacity-50;
}
.input-field {
@apply bg-dark-lighter text-white rounded-lg border border-dark-lighter
focus:border-primary focus:ring-2 focus:ring-primary focus:outline-none
px-4 py-2 transition-all duration-300;
}
.progress-bar {
@apply h-2 rounded-full bg-dark-lighter overflow-hidden;
}
.progress-value {
@apply h-full bg-primary rounded-full transition-all duration-300 ease-out;
}
.tab-active {
@apply border-b-2 border-primary text-primary;
}
.tab-inactive {
@apply text-gray-400 hover:text-white;
}
.model-item {
@apply glass-effect rounded-lg p-4 mb-4 transition-all duration-300
hover:shadow-lg hover:shadow-primary/20;
}
.status-badge {
@apply px-2 py-1 rounded-full text-xs font-bold;
}
.status-downloading {
@apply bg-blue-900/50 text-blue-300 border border-blue-700;
}
.status-downloaded {
@apply bg-green-900/50 text-green-300 border border-green-700;
}
.status-uploading {
@apply bg-purple-900/50 text-purple-300 border border-purple-700;
}
.status-uploaded {
@apply bg-yellow-900/50 text-yellow-300 border border-yellow-700;
}
}
</style>
</head>
<body class="bg-dark text-white">
<div class="main-container">
<!-- 侧边栏 -->
<div class="sidebar">
<div class="sidebar-icon group">
<i class="fa fa-download text-xl"></i>
<span class="sidebar-tooltip group-hover:scale-100">下载模型</span>
</div>
<hr class="sidebar-hr" />
<div class="sidebar-icon group">
<i class="fa fa-upload text-xl"></i>
<span class="sidebar-tooltip group-hover:scale-100">上传模型</span>
</div>
<hr class="sidebar-hr" />
<div class="sidebar-icon group">
<i class="fa fa-trash text-xl"></i>
<span class="sidebar-tooltip group-hover:scale-100">删除模型</span>
</div>
<hr class="sidebar-hr" />
<div class="sidebar-icon group">
<i class="fa fa-list text-xl"></i>
<span class="sidebar-tooltip group-hover:scale-100">模型列表</span>
</div>
</div>
<!-- 主内容区 -->
<div class="main-content">
<!-- 顶部状态栏 -->
<div class="top-bar">
<div class="flex items-center">
<h1 class="text-2xl font-bold text-primary">Linux模型管理工具</h1>
<span class="ml-4 text-sm bg-dark-lighter px-2 py-1 rounded-full">
<i class="fa fa-circle text-green-500 animate-pulse mr-1"></i>
服务运行中
</span>
</div>
<div class="flex items-center space-x-4">
<div class="hidden md:block text-sm">
<span class="text-gray-400">系统:</span>
<span id="system-info">Linux</span>
</div>
<div class="hidden md:block text-sm">
<span class="text-gray-400">模型目录:</span>
<span id="model-dir">/home/user/models</span>
</div>
<button id="settings-btn" class="p-2 rounded-full hover:bg-dark-lighter transition-colors">
<i class="fa fa-cog text-gray-400 hover:text-white"></i>
</button>
</div>
</div>
<!-- 内容区域 -->
<div class="content-area">
<!-- 下载模型选项卡 -->
<div id="download-tab" class="tab-content">
<div class="card">
<h2 class="text-xl font-bold mb-4 flex items-center">
<i class="fa fa-download mr-2 text-primary"></i>
下载模型
</h2>
<div class="mb-4">
<label for="model-ids" class="block text-sm font-medium text-gray-300 mb-1">
模型ID (多个ID用英文逗号分隔)
</label>
<textarea id="model-ids" rows="3" class="input-field w-full"
placeholder="例如: ZhipuAI/GLM-5, Qwen/Qwen3-Coder-Next"></textarea>
</div>
<div class="mb-4">
<label for="local-path" class="block text-sm font-medium text-gray-300 mb-1">
本地存放路径
</label>
<input type="text" id="local-path" class="input-field w-full"
value="/home/user/models" placeholder="输入模型存放路径">
</div>
<div class="flex justify-end">
<button id="download-btn" class="btn-primary flex items-center">
<i class="fa fa-download mr-2"></i>
开始下载
</button>
</div>
</div>
<!-- 下载进度区域 -->
<div id="download-progress" class="card hidden">
<h2 class="text-xl font-bold mb-4 flex items-center">
<i class="fa fa-spinner fa-spin mr-2 text-primary"></i>
下载进度
</h2>
<div id="progress-container" class="space-y-6">
<!-- 进度条将在这里动态生成 -->
</div>
</div>
</div>
<!-- 上传模型选项卡 -->
<div id="upload-tab" class="tab-content hidden">
<div class="card">
<h2 class="text-xl font-bold mb-4 flex items-center">
<i class="fa fa-upload mr-2 text-secondary"></i>
上传模型
</h2>
<div class="mb-4">
<div class="flex justify-between items-center mb-2">
<label class="block text-sm font-medium text-gray-300">
未上传模型列表
</label>
<button id="refresh-models-btn" class="text-sm text-primary hover:text-primary/80 flex items-center">
<i class="fa fa-refresh mr-1"></i>
刷新列表
</button>
</div>
<div id="models-list" class="space-y-2 max-h-96 overflow-y-auto p-2 bg-dark-lighter/30 rounded-lg">
<!-- 模型列表将在这里动态生成 -->
<div class="text-center text-gray-400 py-4">
点击"刷新列表"加载未上传模型
</div>
</div>
</div>
<div class="flex justify-between">
<button id="select-all-btn" class="btn-secondary flex items-center">
<i class="fa fa-check-square-o mr-2"></i>
全选
</button>
<button id="upload-btn" class="btn-primary flex items-center">
<i class="fa fa-upload mr-2"></i>
上传选中模型
</button>
</div>
</div>
<!-- 上传进度区域 -->
<div id="upload-progress" class="card hidden">
<h2 class="text-xl font-bold mb-4 flex items-center">
<i class="fa fa-spinner fa-spin mr-2 text-primary"></i>
上传进度
</h2>
<div id="upload-progress-container" class="space-y-6">
<!-- 上传进度条将在这里动态生成 -->
</div>
</div>
</div>
<!-- 删除模型选项卡 -->
<div id="delete-tab" class="tab-content hidden">
<div class="card">
<h2 class="text-xl font-bold mb-4 flex items-center">
<i class="fa fa-trash mr-2 text-danger"></i>
删除模型
</h2>
<div class="mb-4">
<div class="flex justify-between items-center mb-2">
<label class="block text-sm font-medium text-gray-300">
已下载模型列表
</label>
<button id="refresh-delete-models-btn" class="text-sm text-primary hover:text-primary/80 flex items-center">
<i class="fa fa-refresh mr-1"></i>
刷新列表
</button>
</div>
<div id="delete-models-list" class="space-y-2 max-h-96 overflow-y-auto p-2 bg-dark-lighter/30 rounded-lg">
<!-- 删除模型列表将在这里动态生成 -->
<div class="text-center text-gray-400 py-4">
点击"刷新列表"加载已下载模型
</div>
</div>
</div>
<div class="flex justify-between">
<button id="select-all-delete-btn" class="btn-secondary flex items-center">
<i class="fa fa-check-square-o mr-2"></i>
全选
</button>
<button id="delete-btn" class="btn-danger flex items-center">
<i class="fa fa-trash mr-2"></i>
删除选中模型
</button>
</div>
</div>
</div>
<!-- 模型列表选项卡 -->
<div id="list-tab" class="tab-content hidden">
<div class="card">
<h2 class="text-xl font-bold mb-4 flex items-center">
<i class="fa fa-list mr-2 text-primary"></i>
所有模型
</h2>
<div class="mb-4">
<div class="flex justify-between items-center mb-2">
<label class="block text-sm font-medium text-gray-300">
模型列表
</label>
<button id="refresh-all-models-btn" class="text-sm text-primary hover:text-primary/80 flex items-center">
<i class="fa fa-refresh mr-1"></i>
刷新列表
</button>
</div>
<div id="all-models-list" class="space-y-4 max-h-96 overflow-y-auto p-2 bg-dark-lighter/30 rounded-lg">
<!-- 所有模型列表将在这里动态生成 -->
<div class="text-center text-gray-400 py-4">
点击"刷新列表"加载所有模型
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 设置弹窗 -->
<div id="settings-modal" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 hidden">
<div class="glass-effect rounded-xl p-6 max-w-md w-full">
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-bold">设置</h3>
<button id="close-settings-btn" class="text-gray-400 hover:text-white">
<i class="fa fa-times"></i>
</button>
</div>
<div class="space-y-4">
<div>
<label for="default-model-path" class="block text-sm font-medium text-gray-300 mb-1">
默认模型路径
</label>
<input type="text" id="default-model-path" class="input-field w-full"
value="/home/user/models" placeholder="输入默认模型存放路径">
</div>
<div>
<label for="max-retry" class="block text-sm font-medium text-gray-300 mb-1">
最大重试次数
</label>
<input type="number" id="max-retry" class="input-field w-full"
value="10" min="1" max="20" placeholder="输入最大重试次数">
</div>
<div>
<label for="csghub-url" class="block text-sm font-medium text-gray-300 mb-1">
CsgHub API URL
</label>
<input type="text" id="csghub-url" class="input-field w-full"
value="http://10.17.27.227:4997" placeholder="输入CsgHub API URL">
</div>
<div>
<label for="csghub-token" class="block text-sm font-medium text-gray-300 mb-1">
CsgHub Token
</label>
<input type="password" id="csghub-token" class="input-field w-full"
value="f5dad38a9426410aa861155cd184f84a" placeholder="输入CsgHub Token">
</div>
</div>
<div class="mt-6 flex justify-end">
<button id="save-settings-btn" class="btn-primary">
保存设置
</button>
</div>
</div>
</div>
<!-- 删除确认弹窗 -->
<div id="delete-confirm-modal" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 hidden">
<div class="glass-effect rounded-xl p-6 max-w-md w-full">
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-bold text-danger">确认删除</h3>
<button id="close-delete-confirm-btn" class="text-gray-400 hover:text-white">
<i class="fa fa-times"></i>
</button>
</div>
<p class="mb-4">
您确定要删除选中的 <span id="delete-count" class="font-bold">0</span> 个模型吗?
此操作无法撤销。
</p>
<div class="flex justify-end space-x-3">
<button id="cancel-delete-btn" class="px-4 py-2 border border-gray-500 rounded-lg hover:bg-dark-lighter transition-colors">
取消
</button>
<button id="confirm-delete-btn" class="btn-danger">
确认删除
</button>
</div>
</div>
</div>
<!-- 通知弹窗 -->
<div id="notification" class="fixed top-4 right-4 glass-effect rounded-lg p-4 shadow-lg z-50 transform translate-x-full transition-transform duration-300 max-w-sm">
<div class="flex items-start">
<div id="notification-icon" class="flex-shrink-0 mt-0.5">
<i class="fa fa-check-circle text-green-500 text-xl"></i>
</div>
<div class="ml-3 w-0 flex-1">
<p id="notification-title" class="text-sm font-medium text-white">
操作成功
</p>
<p id="notification-message" class="mt-1 text-sm text-gray-300">
操作已成功完成。
</p>
</div>
<div class="ml-4 flex-shrink-0 flex">
<button id="close-notification-btn" class="inline-flex text-gray-400 hover:text-white">
<i class="fa fa-times"></i>
</button>
</div>
</div>
</div>
<script>
// 全局变量
let currentTab = 'download-tab';
let socket = null;
let downloadTasks = {};
let uploadTasks = {};
let selectedModels = [];
let selectedDeleteModels = [];
let downloadQueue = []; // 下载队列
let currentDownloadTask = null; // 当前正在下载的任务
let uploadQueue = []; // 上传队列
let currentUploadTask = null; // 当前正在上传的任务
let settings = {
defaultModelPath: '/data/DataStore/models/exp/models',
maxRetry: 10,
csghubUrl: 'http://10.17.27.227:4997',
csghubToken: 'f5dad38a9426410aa861155cd184f84a'
};
// DOM 元素
const sidebarIcons = document.querySelectorAll('.sidebar-icon');
const tabContents = document.querySelectorAll('.tab-content');
const downloadBtn = document.getElementById('download-btn');
const uploadBtn = document.getElementById('upload-btn');
const deleteBtn = document.getElementById('delete-btn');
const refreshModelsBtn = document.getElementById('refresh-models-btn');
const refreshDeleteModelsBtn = document.getElementById('refresh-delete-models-btn');
const refreshAllModelsBtn = document.getElementById('refresh-all-models-btn');
const selectAllBtn = document.getElementById('select-all-btn');
const selectAllDeleteBtn = document.getElementById('select-all-delete-btn');
const settingsBtn = document.getElementById('settings-btn');
const closeSettingsBtn = document.getElementById('close-settings-btn');
const saveSettingsBtn = document.getElementById('save-settings-btn');
const deleteConfirmBtn = document.getElementById('confirm-delete-btn');
const cancelDeleteBtn = document.getElementById('cancel-delete-btn');
const closeDeleteConfirmBtn = document.getElementById('close-delete-confirm-btn');
const closeNotificationBtn = document.getElementById('close-notification-btn');
const settingsModal = document.getElementById('settings-modal');
const deleteConfirmModal = document.getElementById('delete-confirm-modal');
const notification = document.getElementById('notification');
// 初始化
document.addEventListener('DOMContentLoaded', () => {
console.log('页面加载完成,初始化应用...');
// 加载设置
loadSettings();
// 加载下载任务和上传任务
loadDownloadTasks();
loadUploadTasks();
// 设置侧边栏图标点击事件
sidebarIcons.forEach((icon, index) => {
icon.addEventListener('click', () => {
console.log('点击侧边栏图标:', index);
const tabs = ['download-tab', 'upload-tab', 'delete-tab', 'list-tab'];
if (index < tabs.length) {
switchTab(tabs[index]);
}
});
});
// 设置按钮点击事件
downloadBtn.addEventListener('click', startDownload);
uploadBtn.addEventListener('click', startUpload);
deleteBtn.addEventListener('click', showDeleteConfirm);
refreshModelsBtn.addEventListener('click', loadModelsList);
refreshDeleteModelsBtn.addEventListener('click', loadDeleteModelsList);
refreshAllModelsBtn.addEventListener('click', loadAllModelsList);
selectAllBtn.addEventListener('click', toggleSelectAll);
selectAllDeleteBtn.addEventListener('click', toggleSelectAllDelete);
settingsBtn.addEventListener('click', showSettings);
closeSettingsBtn.addEventListener('click', hideSettings);
saveSettingsBtn.addEventListener('click', saveSettings);
deleteConfirmBtn.addEventListener('click', confirmDelete);
cancelDeleteBtn.addEventListener('click', hideDeleteConfirm);
closeDeleteConfirmBtn.addEventListener('click', hideDeleteConfirm);
closeNotificationBtn.addEventListener('click', hideNotification);
// 连接Socket.IO(暂时禁用,避免版本不兼容问题)
// connectSocketIO();
// 加载系统信息
loadSystemInfo();
console.log('事件监听器设置完成');
});
// 切换选项卡
function switchTab(tabId) {
// 隐藏所有选项卡
tabContents.forEach(tab => tab.classList.add('hidden'));
// 显示选中的选项卡
document.getElementById(tabId).classList.remove('hidden');
currentTab = tabId;
// 根据选项卡加载数据
if (tabId === 'upload-tab') {
loadModelsList();
} else if (tabId === 'delete-tab') {
loadDeleteModelsList();
} else if (tabId === 'list-tab') {
loadAllModelsList();
}
}
// 连接Socket.IO
function connectSocketIO() {
// Socket.IO已禁用,避免版本不兼容问题
console.log('Socket.IO已禁用,避免版本不兼容问题');
window.socket = null;
}
// 处理 WebSocket 消息
function handleWebSocketMessage(message) {
const { type, data } = message;
switch (type) {
case 'task_update':
handleTaskUpdate(data);
break;
case 'download_progress':
updateDownloadProgress(data);
break;
case 'download_complete':
handleDownloadComplete(data);
break;
case 'download_failed':
handleDownloadFailed(data);
break;
case 'upload_progress':
updateUploadProgress(data);
break;
case 'upload_complete':
handleUploadComplete(data);
break;
case 'upload_failed':
handleUploadFailed(data);
break;
case 'models_list':
updateModelsList(data);
break;
case 'delete_models_list':
updateDeleteModelsList(data);
break;
case 'all_models_list':
updateAllModelsList(data);
break;
case 'system_info':
updateSystemInfo(data);
break;
}
}
// 处理任务更新
function handleTaskUpdate(task) {
const taskId = task.task_id;
if (task.type === 'download') {
// 更新下载进度
const progressElement = document.getElementById(`progress_${taskId}`);
if (progressElement) {
const progressText = document.getElementById(`progress_text_${taskId}`);
const progressValue = document.getElementById(`progress_value_${taskId}`);
const progressDetail = document.getElementById(`progress_detail_${taskId}`);
const statusBadge = progressElement.querySelector('.status-badge');
if (progressText) progressText.textContent = `${Math.round(task.progress)}%`;
if (progressValue) progressValue.style.width = `${task.progress}%`;
if (progressDetail) progressDetail.textContent = task.message;
if (statusBadge) {
switch (task.status) {
case 'downloading':
statusBadge.className = 'status-badge status-downloading';
statusBadge.innerHTML = '<i class="fa fa-spinner fa-spin mr-1"></i>下载中...';
break;
case 'completed':
statusBadge.className = 'status-badge status-success';
statusBadge.innerHTML = '<i class="fa fa-check mr-1"></i>下载完成';
break;
case 'failed':
statusBadge.className = 'status-badge status-error';
statusBadge.innerHTML = '<i class="fa fa-times mr-1"></i>下载失败';
break;
case 'pending':
statusBadge.className = 'status-badge status-pending';
statusBadge.innerHTML = '<i class="fa fa-clock-o mr-1"></i>等待中...';
break;
}
}
}
// 保存任务状态到localStorage
saveDownloadTask(task);
} else if (task.type === 'upload') {
// 更新上传进度
const progressElement = document.getElementById(`upload_progress_${taskId}`);
if (progressElement) {
const progressText = document.getElementById(`upload_progress_text_${taskId}`);
const progressValue = document.getElementById(`upload_progress_value_${taskId}`);
const progressDetail = document.getElementById(`upload_progress_detail_${taskId}`);
const statusBadge = progressElement.querySelector('.status-badge');
if (progressText) progressText.textContent = `${Math.round(task.progress)}%`;
if (progressValue) progressValue.style.width = `${task.progress}%`;
if (progressDetail) progressDetail.textContent = task.message;
if (statusBadge) {
switch (task.status) {
case 'uploading':
statusBadge.className = 'status-badge status-uploading';
statusBadge.innerHTML = '<i class="fa fa-spinner fa-spin mr-1"></i>上传中...';
break;
case 'completed':
statusBadge.className = 'status-badge status-success';
statusBadge.innerHTML = '<i class="fa fa-check mr-1"></i>上传完成';
break;
case 'failed':
statusBadge.className = 'status-badge status-error';
statusBadge.innerHTML = '<i class="fa fa-times mr-1"></i>上传失败';
break;
case 'pending':
statusBadge.className = 'status-badge status-pending';
statusBadge.innerHTML = '<i class="fa fa-clock-o mr-1"></i>等待中...';
break;
}
}
}
}
}
// 保存下载任务到localStorage
function saveDownloadTask(task) {
try {
const savedTasks = JSON.parse(localStorage.getItem('downloadTasks') || '{}');
savedTasks[task.task_id] = {
task_id: task.task_id,
model_id: task.model_id,
local_path: task.local_path,
status: task.status,
progress: task.progress,
message: task.message,
retry_count: task.retry_count || 0,
start_time: task.start_time
};
localStorage.setItem('downloadTasks', JSON.stringify(savedTasks));
} catch (error) {
console.error('保存下载任务失败:', error);
}
}
// 从localStorage加载下载任务
function loadDownloadTasks() {
try {
const savedTasks = JSON.parse(localStorage.getItem('downloadTasks') || '{}');
const activeTasks = [];
// 清空进度容器
const progressContainer = document.getElementById('progress-container');
progressContainer.innerHTML = '';
// 用于存储需要验证的任务
const tasksToVerify = [];
// 恢复所有任务,不仅仅是活跃任务
Object.values(savedTasks).forEach(task => {
// 无论状态如何,都显示任务
tasksToVerify.push(task);
});
// 如果有任务需要验证,先向后端确认任务是否还存在
if (tasksToVerify.length > 0) {
// 先显示进度区域
document.getElementById('download-progress').classList.remove('hidden');
let verifiedCount = 0;
tasksToVerify.forEach(task => {
fetch(`/api/task/${task.task_id}`, {
method: 'GET'
})
.then(response => {
if (response.ok) {
return response.json();
}
return null;
})
.then(taskData => {
verifiedCount++;
// 无论任务是否在后端存在,都显示前端存储的任务
activeTasks.push(task);
// 确定任务状态
let taskStatus = task.status;
let taskProgress = task.progress || 0;
let taskMessage = task.message || '恢复任务...';
// 如果后端有任务数据,使用后端数据
if (taskData) {
taskStatus = taskData.status;
taskProgress = taskData.progress || 0;
taskMessage = taskData.message || taskMessage;
}
// 创建进度条元素
const progressElement = document.createElement('div');
progressElement.id = `progress_${task.task_id}`;
progressElement.className = 'model-item';
// 根据任务状态生成不同的HTML
let statusBadge = '';
let buttonsHTML = '';
switch (taskStatus) {
case 'downloading':
statusBadge = '<span class="status-badge status-downloading"><i class="fa fa-spinner fa-spin mr-1"></i>下载中...</span>';
buttonsHTML = `
<button class="priority-btn bg-blue-900/30 hover:bg-blue-900/50 text-blue-300 text-xs px-2 py-1 rounded border border-blue-800/50"
onclick="setPriority('${task.task_id}', '${task.model_id}')">
<i class="fa fa-arrow-up mr-1"></i>优先
</button>
<button class="pause-btn bg-yellow-900/30 hover:bg-yellow-900/50 text-yellow-300 text-xs px-2 py-1 rounded border border-yellow-800/50"
onclick="pauseDownload('${task.task_id}', '${task.model_id}')">
<i class="fa fa-pause mr-1"></i>暂停
</button>
<button class="delete-btn bg-red-900/30 hover:bg-red-900/50 text-red-300 text-xs px-2 py-1 rounded border border-red-800/50"
onclick="deleteDownloadTask('${task.task_id}', '${task.model_id}')">
<i class="fa fa-trash mr-1"></i>删除
</button>
`;
break;
case 'pending':
statusBadge = '<span class="status-badge status-pending"><i class="fa fa-clock-o mr-1"></i>等待中...</span>';
buttonsHTML = `
<button class="priority-btn bg-blue-900/30 hover:bg-blue-900/50 text-blue-300 text-xs px-2 py-1 rounded border border-blue-800/50"
onclick="setPriority('${task.task_id}', '${task.model_id}')">
<i class="fa fa-arrow-up mr-1"></i>优先
</button>
<button class="pause-btn bg-yellow-900/30 hover:bg-yellow-900/50 text-yellow-300 text-xs px-2 py-1 rounded border border-yellow-800/50"
onclick="pauseDownload('${task.task_id}', '${task.model_id}')">
<i class="fa fa-pause mr-1"></i>暂停
</button>
<button class="delete-btn bg-red-900/30 hover:bg-red-900/50 text-red-300 text-xs px-2 py-1 rounded border border-red-800/50"
onclick="deleteDownloadTask('${task.task_id}', '${task.model_id}')">
<i class="fa fa-trash mr-1"></i>删除
</button>
`;
break;
case 'paused':
statusBadge = '<span class="status-badge bg-gray-700/50 text-gray-300 border border-gray-600"><i class="fa fa-pause mr-1"></i>已暂停</span>';
buttonsHTML = `
<button class="priority-btn bg-blue-900/30 hover:bg-blue-900/50 text-blue-300 text-xs px-2 py-1 rounded border border-blue-800/50"
onclick="setPriority('${task.task_id}', '${task.model_id}')">
<i class="fa fa-arrow-up mr-1"></i>优先
</button>
<button class="pause-btn bg-green-900/30 hover:bg-green-900/50 text-green-300 text-xs px-2 py-1 rounded border border-green-800/50"
onclick="resumeDownload('${task.task_id}', '${task.model_id}')">
<i class="fa fa-play mr-1"></i>继续
</button>
<button class="delete-btn bg-red-900/30 hover:bg-red-900/50 text-red-300 text-xs px-2 py-1 rounded border border-red-800/50"
onclick="deleteDownloadTask('${task.task_id}', '${task.model_id}')">
<i class="fa fa-trash mr-1"></i>删除
</button>
`;
break;
case 'downloaded':
statusBadge = '<span class="status-badge status-downloaded"><i class="fa fa-check mr-1"></i>下载完成</span>';
buttonsHTML = `
<button class="delete-btn bg-red-900/30 hover:bg-red-900/50 text-red-300 text-xs px-2 py-1 rounded border border-red-800/50"
onclick="deleteDownloadTask('${task.task_id}', '${task.model_id}')">
<i class="fa fa-trash mr-1"></i>删除
</button>
`;
break;
case 'failed':
statusBadge = '<span class="status-badge bg-red-900/50 text-red-300 border border-red-700"><i class="fa fa-times mr-1"></i>下载失败</span>';
buttonsHTML = `
<button class="delete-btn bg-red-900/30 hover:bg-red-900/50 text-red-300 text-xs px-2 py-1 rounded border border-red-800/50"
onclick="deleteDownloadTask('${task.task_id}', '${task.model_id}')">
<i class="fa fa-trash mr-1"></i>删除
</button>
`;
break;
case 'cancelled':
statusBadge = '<span class="status-badge bg-gray-700/50 text-gray-300 border border-gray-600"><i class="fa fa-ban mr-1"></i>已取消</span>';
buttonsHTML = `
<button class="delete-btn bg-red-900/30 hover:bg-red-900/50 text-red-300 text-xs px-2 py-1 rounded border border-red-800/50"
onclick="deleteDownloadTask('${task.task_id}', '${task.model_id}')">
<i class="fa fa-trash mr-1"></i>删除
</button>
`;
break;
default:
statusBadge = '<span class="status-badge status-pending"><i class="fa fa-clock-o mr-1"></i>等待中...</span>';
buttonsHTML = `
<button class="priority-btn bg-blue-900/30 hover:bg-blue-900/50 text-blue-300 text-xs px-2 py-1 rounded border border-blue-800/50"
onclick="setPriority('${task.task_id}', '${task.model_id}')">
<i class="fa fa-arrow-up mr-1"></i>优先
</button>
<button class="pause-btn bg-yellow-900/30 hover:bg-yellow-900/50 text-yellow-300 text-xs px-2 py-1 rounded border border-yellow-800/50"
onclick="pauseDownload('${task.task_id}', '${task.model_id}')">
<i class="fa fa-pause mr-1"></i>暂停
</button>
<button class="delete-btn bg-red-900/30 hover:bg-red-900/50 text-red-300 text-xs px-2 py-1 rounded border border-red-800/50"
onclick="deleteDownloadTask('${task.task_id}', '${task.model_id}')">
<i class="fa fa-trash mr-1"></i>删除
</button>
`;
}
// 构建完整的HTML
progressElement.innerHTML = `
<div class="flex justify-between items-center mb-2">
<div>
<h3 class="font-medium">${task.model_id}</h3>
${statusBadge}
</div>
<div class="flex items-center space-x-2">
<span class="text-sm text-gray-400" id="progress_text_${task.task_id}">${Math.round(taskProgress)}%</span>
${buttonsHTML}
</div>
</div>
<div class="progress-bar">
<div class="progress-value" id="progress_value_${task.task_id}" style="width: ${taskProgress}%"></div>
</div>
<div class="mt-2 flex justify-between items-center">
<div class="text-xs text-gray-400" id="progress_detail_${task.task_id}">
${taskMessage}
</div>
<div class="flex items-center">
<input type="checkbox" id="auto_upload_${task.task_id}" class="mr-1 h-3 w-3 rounded border-gray-500 text-primary focus:ring-primary"
${task.autoUpload ? 'checked' : ''} onchange="toggleAutoUpload('${task.task_id}', this.checked)">
<label for="auto_upload_${task.task_id}" class="text-xs text-gray-400 cursor-pointer">自动上传</label>
</div>
</div>
`;
progressContainer.appendChild(progressElement);
// 订阅任务更新
if (window.socket && window.socket.connected) {
window.socket.emit('subscribe_task', { task_id: task.task_id });
}
// 恢复任务到全局变量
downloadTasks[task.task_id] = task;
// 如果任务状态是 pending,添加到下载队列
if (task.status === 'pending') {
downloadQueue.push(task.task_id);
}
// 所有验证完成后更新 localStorage 并处理队列
if (verifiedCount === tasksToVerify.length) {
localStorage.setItem('downloadTasks', JSON.stringify(savedTasks));
console.log('已恢复', activeTasks.length, '个下载任务');
// 如果没有活跃任务,隐藏进度区域
if (activeTasks.length === 0) {
document.getElementById('download-progress').classList.add('hidden');
} else {
// 开始处理下载队列
processDownloadQueue();
}
}
})
.catch(error => {
verifiedCount++;
console.error('验证任务失败:', error);
// 即使验证失败,也显示任务
activeTasks.push(task);
// 创建进度条元素
const progressElement = document.createElement('div');
progressElement.id = `progress_${task.task_id}`;
progressElement.className = 'model-item';
progressElement.innerHTML = `
<div class="flex justify-between items-center mb-2">
<div>
<h3 class="font-medium">${task.model_id}</h3>
<span class="status-badge bg-gray-700/50 text-gray-300 border border-gray-600">
<i class="fa fa-exclamation mr-1"></i>状态未知
</span>
</div>
<div class="flex items-center space-x-2">
<span class="text-sm text-gray-400" id="progress_text_${task.task_id}">${Math.round(task.progress || 0)}%</span>
<button class="delete-btn bg-red-900/30 hover:bg-red-900/50 text-red-300 text-xs px-2 py-1 rounded border border-red-800/50"
onclick="deleteDownloadTask('${task.task_id}', '${task.model_id}')">
<i class="fa fa-trash mr-1"></i>删除
</button>
</div>
</div>
<div class="progress-bar">
<div class="progress-value" id="progress_value_${task.task_id}" style="width: ${task.progress || 0}%"></div>
</div>
<div class="mt-2 flex justify-between items-center">
<div class="text-xs text-gray-400" id="progress_detail_${task.task_id}">
任务状态未知
</div>
<div class="flex items-center">
<input type="checkbox" id="auto_upload_${task.task_id}" class="mr-1 h-3 w-3 rounded border-gray-500 text-primary focus:ring-primary"
${task.autoUpload ? 'checked' : ''} onchange="toggleAutoUpload('${task.task_id}', this.checked)">
<label for="auto_upload_${task.task_id}" class="text-xs text-gray-400 cursor-pointer">自动上传</label>
</div>
</div>
`;
progressContainer.appendChild(progressElement);
// 恢复任务到全局变量
downloadTasks[task.task_id] = task;
if (verifiedCount === tasksToVerify.length) {
localStorage.setItem('downloadTasks', JSON.stringify(savedTasks));
console.log('已恢复', activeTasks.length, '个下载任务');
if (activeTasks.length === 0) {
document.getElementById('download-progress').classList.add('hidden');
}
}
});
});
} else {
// 没有任务,隐藏进度区域
document.getElementById('download-progress').classList.add('hidden');
}
} catch (error) {
console.error('加载下载任务失败:', error);
document.getElementById('download-progress').classList.add('hidden');
}
}
// 开始下载
function startDownload() {
const modelIdsInput = document.getElementById('model-ids').value.trim();
const localPath = document.getElementById('local-path').value.trim();
if (!modelIdsInput) {
showNotification('错误', '请输入模型ID', 'error');
return;
}
if (!localPath) {
showNotification('错误', '请输入本地存放路径', 'error');
return;
}
// 分割模型ID
const modelIds = modelIdsInput.split(',').map(id => id.trim()).filter(id => id);
if (modelIds.length === 0) {
showNotification('错误', '请输入有效的模型ID', 'error');
return;
}
// 显示下载进度区域
document.getElementById('download-progress').classList.remove('hidden');
// 为每个模型创建进度条并添加到队列
modelIds.forEach(modelId => {
const taskId = `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
// 保存任务信息 - 确保包含 task_id 字段
const task = {
task_id: taskId,
model_id: modelId,
local_path: localPath,
retryCount: 0,
status: 'pending',
progress: 0,
autoUpload: false
};
downloadTasks[taskId] = task;
downloadQueue.push(taskId);
// 立即保存到 localStorage
const savedTasks = JSON.parse(localStorage.getItem('downloadTasks') || '{}');
savedTasks[taskId] = task;
localStorage.setItem('downloadTasks', JSON.stringify(savedTasks));
// 创建进度条元素
const progressElement = document.createElement('div');
progressElement.id = `progress_${taskId}`;
progressElement.className = 'model-item';
progressElement.innerHTML = `
<div class="flex justify-between items-center mb-2">
<div>
<h3 class="font-medium">${modelId}</h3>
<span class="status-badge status-pending">
<i class="fa fa-clock-o mr-1"></i>
等待中...
</span>
</div>
<div class="flex items-center space-x-2">
<span class="text-sm text-gray-400" id="progress_text_${taskId}">0%</span>
<button class="priority-btn bg-blue-900/30 hover:bg-blue-900/50 text-blue-300 text-xs px-2 py-1 rounded border border-blue-800/50"
onclick="setPriority('${taskId}', '${modelId}')">
<i class="fa fa-arrow-up mr-1"></i>优先
</button>
<button class="pause-btn bg-yellow-900/30 hover:bg-yellow-900/50 text-yellow-300 text-xs px-2 py-1 rounded border border-yellow-800/50"
onclick="pauseDownload('${taskId}', '${modelId}')">
<i class="fa fa-pause mr-1"></i>暂停
</button>
<button class="delete-btn bg-red-900/30 hover:bg-red-900/50 text-red-300 text-xs px-2 py-1 rounded border border-red-800/50"
onclick="deleteDownloadTask('${taskId}', '${modelId}')">
<i class="fa fa-trash mr-1"></i>删除
</button>
</div>
</div>
<div class="progress-bar">
<div class="progress-value" id="progress_value_${taskId}" style="width: 0%"></div>
</div>
<div class="mt-2 flex justify-between items-center">
<div class="text-xs text-gray-400" id="progress_detail_${taskId}">
准备下载...
</div>
<div class="flex items-center">
<input type="checkbox" id="auto_upload_${taskId}" class="mr-1 h-3 w-3 rounded border-gray-500 text-primary focus:ring-primary"
onchange="toggleAutoUpload('${taskId}', this.checked)">
<label for="auto_upload_${taskId}" class="text-xs text-gray-400 cursor-pointer">自动上传</label>
</div>
</div>
`;
document.getElementById('progress-container').appendChild(progressElement);
});
// 开始处理下载队列
processDownloadQueue();
console.log('开始下载模型:', modelIds, '到路径:', localPath);
}
// 处理下载队列
function processDownloadQueue() {
if (currentDownloadTask || downloadQueue.length === 0) {
return;
}
// 获取队列中的第一个任务
const taskId = downloadQueue.shift();
const task = downloadTasks[taskId];
if (!task || task.status === 'cancelled' || task.status === 'failed') {
// 跳过已取消或失败的任务
processDownloadQueue();
return;
}
// 更新任务状态为下载中
task.status = 'downloading';
// 更新UI
const progressElement = document.getElementById(`progress_${taskId}`);
if (progressElement) {
const statusBadge = progressElement.querySelector('.status-badge');
if (statusBadge) {
statusBadge.className = 'status-badge status-downloading';
statusBadge.innerHTML = '<i class="fa fa-spinner fa-spin mr-1"></i>下载中...';
}
}
// 设置当前下载任务
currentDownloadTask = taskId;
// 使用 AbortController 支持取消请求
const controller = new AbortController();
const signal = controller.signal;
task.abortController = controller;
// 设置10秒超时
const timeoutId = setTimeout(() => {
controller.abort();
showNotification('错误', '连接服务器超时,请检查后端服务是否运行', 'error');
task.status = 'failed';
currentDownloadTask = null;
const progressElement = document.getElementById(`progress_${taskId}`);
if (progressElement) {
const statusBadge = progressElement.querySelector('.status-badge');
if (statusBadge) {
statusBadge.className = 'status-badge bg-red-900/50 text-red-300 border border-red-700';
statusBadge.innerHTML = '<i class="fa fa-times mr-1"></i>连接超时';
}
}
// 继续处理队列
processDownloadQueue();
}, 10000);
// 发送请求到后端开始下载
fetch('/api/download', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
model_id: task.model_id,
local_path: task.local_path,
task_id: task.task_id
}),
signal: signal
})
.then(response => {
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error('Network response was not ok: ' + response.status);
}
return response.json();
})
.then(data => {
console.log('下载请求已发送:', data);
if (data.status === 'error') {
throw new Error(data.message || '下载请求失败');
}
})
.catch(error => {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
console.log('下载请求已被取消');
return;
}
console.error('Error starting download:', error);
showNotification('错误', `启动下载失败: ${error.message}`, 'error');
// 更新任务状态为失败
task.status = 'failed';
currentDownloadTask = null;
const progressElement = document.getElementById(`progress_${taskId}`);
if (progressElement) {
const statusBadge = progressElement.querySelector('.status-badge');
const progressDetail = document.getElementById(`progress_detail_${taskId}`);
if (statusBadge) {
statusBadge.className = 'status-badge bg-red-900/50 text-red-300 border border-red-700';
statusBadge.innerHTML = '<i class="fa fa-times mr-1"></i>下载失败';
}
if (progressDetail) progressDetail.textContent = `下载失败: ${error.message}`;
}
// 继续处理队列
processDownloadQueue();
});
}
// 设置下载优先级
function setPriority(taskId, modelId) {
// 从队列中移除任务
downloadQueue = downloadQueue.filter(id => id !== taskId);
// 将任务添加到队列开头
downloadQueue.unshift(taskId);
// 如果当前没有下载任务,立即开始处理
if (!currentDownloadTask) {
processDownloadQueue();
}
// 更新UI,将任务移到列表顶部
const progressElement = document.getElementById(`progress_${taskId}`);
if (progressElement) {
const progressContainer = document.getElementById('progress-container');
progressContainer.insertBefore(progressElement, progressContainer.firstChild);
}
showNotification('提示', `模型 ${modelId} 已设置为最高优先级`, 'info');
}
// 暂停下载
function pauseDownload(taskId, modelId) {
const task = downloadTasks[taskId];
if (!task) return;
if (task.status === 'downloading' && currentDownloadTask === taskId) {
// 取消当前正在下载的任务
if (task.abortController) {
task.abortController.abort();
}
// 发送取消请求到后端
fetch(`/api/download/cancel/${taskId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.catch(error => console.error('取消下载失败:', error));
// 更新任务状态
task.status = 'paused';
currentDownloadTask = null;
// 更新UI
const progressElement = document.getElementById(`progress_${taskId}`);
if (progressElement) {
const statusBadge = progressElement.querySelector('.status-badge');
const progressDetail = document.getElementById(`progress_detail_${taskId}`);
const pauseBtn = progressElement.querySelector('.pause-btn');
if (statusBadge) {
statusBadge.className = 'status-badge bg-gray-700/50 text-gray-300 border border-gray-600';
statusBadge.innerHTML = '<i class="fa fa-pause mr-1"></i>已暂停';
}
if (progressDetail) progressDetail.textContent = '下载已暂停';
if (pauseBtn) {
pauseBtn.innerHTML = '<i class="fa fa-play mr-1"></i>继续';
pauseBtn.onclick = () => resumeDownload(taskId, modelId);
}
}
// 继续处理队列中的下一个任务
processDownloadQueue();
} else if (task.status === 'paused') {
// 任务已暂停,将其添加到队列
downloadQueue.push(taskId);
task.status = 'pending';
// 更新UI
const progressElement = document.getElementById(`progress_${taskId}`);
if (progressElement) {
const statusBadge = progressElement.querySelector('.status-badge');
const progressDetail = document.getElementById(`progress_detail_${taskId}`);
const pauseBtn = progressElement.querySelector('.pause-btn');
if (statusBadge) {
statusBadge.className = 'status-badge status-pending';
statusBadge.innerHTML = '<i class="fa fa-clock-o mr-1"></i>等待中...';
}
if (progressDetail) progressDetail.textContent = '准备下载...';
if (pauseBtn) {
pauseBtn.innerHTML = '<i class="fa fa-pause mr-1"></i>暂停';
pauseBtn.onclick = () => pauseDownload(taskId, modelId);
}
}
// 如果当前没有下载任务,开始处理
if (!currentDownloadTask) {
processDownloadQueue();
}
}
showNotification('提示', `模型 ${modelId} 下载已${task.status === 'paused' ? '暂停' : '继续'}`, 'info');
}
// 恢复下载
function resumeDownload(taskId, modelId) {
const task = downloadTasks[taskId];
if (!task) return;
// 将任务添加到队列
downloadQueue.push(taskId);
task.status = 'pending';
// 更新UI
const progressElement = document.getElementById(`progress_${taskId}`);
if (progressElement) {
const statusBadge = progressElement.querySelector('.status-badge');
const progressDetail = document.getElementById(`progress_detail_${taskId}`);
const pauseBtn = progressElement.querySelector('.pause-btn');
if (statusBadge) {
statusBadge.className = 'status-badge status-pending';
statusBadge.innerHTML = '<i class="fa fa-clock-o mr-1"></i>等待中...';
}
if (progressDetail) progressDetail.textContent = '准备下载...';
if (pauseBtn) {
pauseBtn.innerHTML = '<i class="fa fa-pause mr-1"></i>暂停';
pauseBtn.onclick = () => pauseDownload(taskId, modelId);
}
}
// 如果当前没有下载任务,开始处理
if (!currentDownloadTask) {
processDownloadQueue();
}
showNotification('提示', `模型 ${modelId} 下载已继续`, 'info');
}
// 切换自动上传选项
function toggleAutoUpload(taskId, checked) {
const task = downloadTasks[taskId];
if (task) {
task.autoUpload = checked;
// 保存到localStorage
const savedTasks = JSON.parse(localStorage.getItem('downloadTasks') || '{}');
if (savedTasks[taskId]) {
savedTasks[taskId].autoUpload = checked;
localStorage.setItem('downloadTasks', JSON.stringify(savedTasks));
}
}
}
// 删除下载任务
function deleteDownloadTask(taskId, modelId) {
if (confirm(`确定要删除下载任务 ${modelId} 吗?`)) {
// 从队列中移除任务
downloadQueue = downloadQueue.filter(id => id !== taskId);
// 如果是当前正在下载的任务,取消下载
if (currentDownloadTask === taskId) {
const task = downloadTasks[taskId];
if (task && task.abortController) {
task.abortController.abort();
}
// 发送取消请求到后端
fetch(`/api/download/cancel/${taskId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.catch(error => console.error('取消下载失败:', error));
currentDownloadTask = null;
// 继续处理队列中的下一个任务
processDownloadQueue();
}
// 从任务列表中移除
delete downloadTasks[taskId];
// 从localStorage中移除
const savedTasks = JSON.parse(localStorage.getItem('downloadTasks') || '{}');
delete savedTasks[taskId];
localStorage.setItem('downloadTasks', JSON.stringify(savedTasks));
// 从UI中移除
const progressElement = document.getElementById(`progress_${taskId}`);
if (progressElement) {
progressElement.remove();
}
// 如果没有任务了,隐藏进度区域
if (Object.keys(downloadTasks).length === 0) {
document.getElementById('download-progress').classList.add('hidden');
}
showNotification('提示', `下载任务 ${modelId} 已删除`, 'info');
}
}
// 模拟下载进度
function simulateDownloadProgress(taskId) {
const task = downloadTasks[taskId];
if (!task) return;
// 模拟进度增加
let progress = task.progress;
const interval = setInterval(() => {
// 随机增加进度
const increment = Math.random() * 10;
progress = Math.min(progress + increment, 100);
// 更新任务进度
task.progress = progress;
// 更新UI
const progressText = document.getElementById(`progress_text_${taskId}`);
const progressValue = document.getElementById(`progress_value_${taskId}`);
const progressDetail = document.getElementById(`progress_detail_${taskId}`);
if (progressText) progressText.textContent = `${Math.round(progress)}%`;
if (progressValue) progressValue.style.width = `${progress}%`;
// 随机更新详细信息
const details = [
`下载中: ${Math.round(progress)}%`,
`正在获取模型文件...`,
`已下载 ${Math.round(progress * 100 / 100)}MB / 100MB`,
`正在验证文件完整性...`
];
if (progressDetail) {
progressDetail.textContent = details[Math.floor(Math.random() * details.length)];
}
// 检查是否完成
if (progress >= 100) {
clearInterval(interval);
// 模拟下载完成
setTimeout(() => {
handleDownloadComplete({
taskId,
modelId: task.modelId,
localPath: task.localPath
});
}, 500);
}
}, 1000);
// 保存定时器ID
task.interval = interval;
}
// 更新下载进度
function updateDownloadProgress(data) {
const { taskId, progress, detail } = data;
const task = downloadTasks[taskId];
if (task) {
task.progress = progress;
// 更新UI
const progressText = document.getElementById(`progress_text_${taskId}`);
const progressValue = document.getElementById(`progress_value_${taskId}`);
const progressDetail = document.getElementById(`progress_detail_${taskId}`);
if (progressText) progressText.textContent = `${Math.round(progress)}%`;
if (progressValue) progressValue.style.width = `${progress}%`;
if (progressDetail && detail) progressDetail.textContent = detail;
}
}
// 处理下载完成
function handleDownloadComplete(data) {
const { taskId, modelId, localPath } = data;
const task = downloadTasks[taskId];
if (task) {
// 清除定时器
if (task.interval) {
clearInterval(task.interval);
}
// 更新任务状态
task.status = 'downloaded';
task.progress = 100;
// 更新UI
const progressElement = document.getElementById(`progress_${taskId}`);
if (progressElement) {
const statusBadge = progressElement.querySelector('.status-badge');
const progressText = document.getElementById(`progress_text_${taskId}`);
const progressValue = document.getElementById(`progress_value_${taskId}`);
const progressDetail = document.getElementById(`progress_detail_${taskId}`);
if (statusBadge) {
statusBadge.className = 'status-badge status-downloaded';
statusBadge.innerHTML = '<i class="fa fa-check mr-1"></i>下载完成';
}
if (progressText) progressText.textContent = '100%';
if (progressValue) {
progressValue.style.width = '100%';
progressValue.classList.add('bg-success');
}
if (progressDetail) progressDetail.textContent = `模型已保存到: ${localPath}/${modelId}`;
}
// 显示通知
showNotification('成功', `模型 ${modelId} 下载完成`, 'success');
// 检查是否需要自动上传
if (task.autoUpload) {
showNotification('提示', `开始自动上传模型 ${modelId}`, 'info');
// 调用上传单个模型的函数
uploadSingleModel(modelId);
}
// 清空当前下载任务
currentDownloadTask = null;
// 继续处理队列中的下一个任务
processDownloadQueue();
// 如果是最后一个任务,重新加载模型列表
if (Object.values(downloadTasks).every(t => t.status === 'downloaded' || t.status === 'failed' || t.status === 'cancelled')) {
setTimeout(() => {
if (currentTab === 'upload-tab') {
loadModelsList();
} else if (currentTab === 'delete-tab') {
loadDeleteModelsList();
} else if (currentTab === 'list-tab') {
loadAllModelsList();
}
}, 1000);
}
}
}
// 取消下载
function cancelDownload(taskId, modelId) {
if (confirm(`确定要取消下载模型 ${modelId} 吗?`)) {
console.log(`取消下载任务: ${taskId}, 模型ID: ${modelId}`);
// 先尝试取消前端的 fetch 请求
const task = downloadTasks[taskId];
if (task && task.abortController) {
task.abortController.abort();
}
// 使用 AbortController 支持超时取消
const controller = new AbortController();
const signal = controller.signal;
// 设置5秒超时
const timeoutId = setTimeout(() => {
controller.abort();
}, 5000);
// 发送取消请求到后端
fetch(`/api/download/cancel/${taskId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
signal: signal
})
.then(response => {
clearTimeout(timeoutId);
// 即使任务不存在,也清理前端界面
return response.json().catch(() => ({ success: false, message: '任务不存在' }));
})
.then(data => {
console.log('取消下载响应:', data);
// 无论后端返回什么,都清理前端界面
const task = downloadTasks[taskId];
if (task) {
// 清除定时器
if (task.interval) {
clearInterval(task.interval);
}
task.status = 'cancelled';
// 更新UI
const progressElement = document.getElementById(`progress_${taskId}`);
if (progressElement) {
const statusBadge = progressElement.querySelector('.status-badge');
const progressDetail = document.getElementById(`progress_detail_${taskId}`);
const cancelBtn = progressElement.querySelector('.cancel-btn');
if (statusBadge) {
statusBadge.className = 'status-badge bg-gray-700/50 text-gray-300 border border-gray-600';
statusBadge.innerHTML = '<i class="fa fa-ban mr-1"></i>已取消';
}
if (progressDetail) progressDetail.textContent = '下载已取消';
// 隐藏取消按钮
if (cancelBtn) {
cancelBtn.disabled = true;
cancelBtn.textContent = '已取消';
cancelBtn.classList.remove('hover:bg-red-900/50');
cancelBtn.classList.add('bg-gray-800/50', 'text-gray-400', 'cursor-not-allowed');
}
}
// 显示通知
showNotification('提示', `模型 ${modelId} 下载已取消`, 'info');
// 从任务列表中移除
setTimeout(() => {
delete downloadTasks[taskId];
const progressElement = document.getElementById(`progress_${taskId}`);
if (progressElement) {
progressElement.remove();
}
// 如果没有活跃任务,隐藏进度区域
if (Object.keys(downloadTasks).length === 0) {
document.getElementById('download-progress').classList.add('hidden');
}
}, 2000);
}
})
.catch(error => {
clearTimeout(timeoutId);
// 即使请求失败,也清理前端界面
const task = downloadTasks[taskId];
if (task) {
if (task.interval) {
clearInterval(task.interval);
}
task.status = 'cancelled';
const progressElement = document.getElementById(`progress_${taskId}`);
if (progressElement) {
const statusBadge = progressElement.querySelector('.status-badge');
if (statusBadge) {
statusBadge.className = 'status-badge bg-gray-700/50 text-gray-300 border border-gray-600';
statusBadge.innerHTML = '<i class="fa fa-ban mr-1"></i>已取消';
}
}
setTimeout(() => {
delete downloadTasks[taskId];
const progressElement = document.getElementById(`progress_${taskId}`);
if (progressElement) {
progressElement.remove();
}
if (Object.keys(downloadTasks).length === 0) {
document.getElementById('download-progress').classList.add('hidden');
}
}, 2000);
}
if (error.name === 'AbortError') {
console.error('取消请求超时');
showNotification('错误', '取消请求超时,请重试', 'error');
} else {
console.error('取消下载失败:', error);
showNotification('提示', `模型 ${modelId} 下载已取消`, 'info');
}
});
}
}
// 处理下载失败
function handleDownloadFailed(data) {
const { taskId, modelId, error } = data;
const task = downloadTasks[taskId];
if (task) {
// 清除定时器
if (task.interval) {
clearInterval(task.interval);
}
// 增加重试次数
task.retryCount++;
// 检查是否超过最大重试次数
if (task.retryCount < settings.maxRetry) {
// 更新任务状态
task.status = 'retrying';
// 更新UI
const progressElement = document.getElementById(`progress_${taskId}`);
if (progressElement) {
const statusBadge = progressElement.querySelector('.status-badge');
const progressDetail = document.getElementById(`progress_detail_${taskId}`);
if (statusBadge) {
statusBadge.className = 'status-badge status-downloading';
statusBadge.innerHTML = `<i class="fa fa-refresh fa-spin mr-1"></i>重试中 (${task.retryCount}/${settings.maxRetry})`;
}
if (progressDetail) progressDetail.textContent = `下载失败: ${error}. 正在重试...`;
}
// 重新开始下载
setTimeout(() => {
fetch('/api/download', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
model_id: task.model_id,
local_path: task.localPath,
task_id: taskId
})
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
console.log('重试下载请求已发送:', data);
})
.catch(error => {
console.error('Error retrying download:', error);
});
}, 2000);
} else {
// 更新任务状态
task.status = 'failed';
// 更新UI
const progressElement = document.getElementById(`progress_${taskId}`);
if (progressElement) {
const statusBadge = progressElement.querySelector('.status-badge');
const progressValue = document.getElementById(`progress_value_${taskId}`);
const progressDetail = document.getElementById(`progress_detail_${taskId}`);
if (statusBadge) {
statusBadge.className = 'status-badge bg-red-900/50 text-red-300 border border-red-700';
statusBadge.innerHTML = '<i class="fa fa-times mr-1"></i>下载失败';
}
if (progressValue) {
progressValue.classList.add('bg-danger');
}
if (progressDetail) progressDetail.textContent = `下载失败: ${error}. 已重试 ${settings.maxRetry} 次`;
}
// 显示通知
showNotification('错误', `模型 ${modelId} 下载失败,已重试 ${settings.maxRetry} 次`, 'error');
}
}
}
// 加载未上传模型列表
function loadModelsList() {
const modelsList = document.getElementById('models-list');
// 显示加载状态
modelsList.innerHTML = `
<div class="text-center text-gray-400 py-4">
<i class="fa fa-spinner fa-spin mr-2"></i>
加载中...
</div>
`;
// 从后端获取未上传模型列表,传递当前配置的模型路径
const modelPath = settings.defaultModelPath;
// 使用AbortController设置超时
const controller = new AbortController();
const signal = controller.signal;
// 设置30秒超时
const timeoutId = setTimeout(() => {
controller.abort();
modelsList.innerHTML = `
<div class="text-center text-red-400 py-4">
<i class="fa fa-exclamation-triangle mr-2"></i>
加载超时: 请检查后端服务是否运行
</div>
`;
}, 30000);
fetch(`/api/models?status=downloaded&path=${encodeURIComponent(modelPath)}`, {
signal: signal
})
.then(response => {
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`Network response was not ok: ${response.status}`);
}
return response.json();
})
.then(data => {
clearTimeout(timeoutId);
const models = data.models || [];
// 更新模型列表
updateModelsList(models);
})
.catch(error => {
clearTimeout(timeoutId);
console.error('Error loading models list:', error);
let errorMessage = error.message;
if (error.name === 'AbortError') {
errorMessage = '请求超时';
}
modelsList.innerHTML = `
<div class="text-center text-red-400 py-4">
<i class="fa fa-exclamation-triangle mr-2"></i>
加载失败: ${errorMessage}
</div>
`;
});
}
// 更新未上传模型列表
function updateModelsList(models) {
const modelsList = document.getElementById('models-list');
if (models.length === 0) {
modelsList.innerHTML = `
<div class="text-center text-gray-400 py-4">
没有未上传的模型
</div>
`;
return;
}
// 清空列表
modelsList.innerHTML = '';
// 添加模型项
models.forEach(model => {
const modelElement = document.createElement('div');
modelElement.className = 'model-item flex items-center justify-between';
modelElement.innerHTML = `
<div class="flex items-center">
<input type="checkbox" id="model_${model.id}" class="model-checkbox mr-3 h-4 w-4 rounded border-gray-500 text-primary focus:ring-primary" value="${model.id}">
<div>
<label for="model_${model.id}" class="font-medium cursor-pointer">${model.id}</label>
<div class="text-xs text-gray-400">${model.path} (${model.size})</div>
</div>
</div>
<button class="upload-single-btn text-primary hover:text-primary/80" data-model-id="${model.id}">
<i class="fa fa-upload"></i>
</button>
`;
modelsList.appendChild(modelElement);
// 添加复选框事件
const checkbox = modelElement.querySelector('.model-checkbox');
checkbox.addEventListener('change', () => {
if (checkbox.checked) {
selectedModels.push(model.id);
} else {
selectedModels = selectedModels.filter(id => id !== model.id);
}
});
// 添加单个上传按钮事件
const uploadSingleBtn = modelElement.querySelector('.upload-single-btn');
uploadSingleBtn.addEventListener('click', () => {
uploadSingleModel(model.id);
});
});
}
// 加载删除模型列表
function loadDeleteModelsList() {
const deleteModelsList = document.getElementById('delete-models-list');
// 显示加载状态
deleteModelsList.innerHTML = `
<div class="text-center text-gray-400 py-4">
<i class="fa fa-spinner fa-spin mr-2"></i>
加载中...
</div>
`;
// 从后端获取所有模型列表,传递当前配置的模型路径
const modelPath = settings.defaultModelPath;
// 使用AbortController设置超时
const controller = new AbortController();
const signal = controller.signal;
// 设置30秒超时
const timeoutId = setTimeout(() => {
controller.abort();
deleteModelsList.innerHTML = `
<div class="text-center text-red-400 py-4">
<i class="fa fa-exclamation-triangle mr-2"></i>
加载超时: 请检查后端服务是否运行
</div>
`;
}, 30000);
fetch(`/api/models?all=true&path=${encodeURIComponent(modelPath)}`, {
signal: signal
})
.then(response => {
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`Network response was not ok: ${response.status}`);
}
return response.json();
})
.then(data => {
clearTimeout(timeoutId);
const models = data.models || [];
// 更新删除模型列表
updateDeleteModelsList(models);
})
.catch(error => {
clearTimeout(timeoutId);
console.error('Error loading delete models list:', error);
let errorMessage = error.message;
if (error.name === 'AbortError') {
errorMessage = '请求超时';
}
deleteModelsList.innerHTML = `
<div class="text-center text-red-400 py-4">
<i class="fa fa-exclamation-triangle mr-2"></i>
加载失败: ${errorMessage}
</div>
`;
});
}
// 更新删除模型列表
function updateDeleteModelsList(models) {
const deleteModelsList = document.getElementById('delete-models-list');
if (models.length === 0) {
deleteModelsList.innerHTML = `
<div class="text-center text-gray-400 py-4">
没有已下载的模型
</div>
`;
return;
}
// 清空列表
deleteModelsList.innerHTML = '';
// 添加模型项
models.forEach(model => {
const modelElement = document.createElement('div');
modelElement.className = 'model-item flex items-center justify-between';
// 确定状态标签
let statusBadge = '';
if (model.status === 'uploaded') {
statusBadge = '<span class="status-badge status-uploaded"><i class="fa fa-cloud-upload mr-1"></i>已上传</span>';
} else if (model.status === 'downloading') {
statusBadge = '<span class="status-badge status-downloading"><i class="fa fa-spinner fa-spin mr-1"></i>下载中</span>';
} else if (model.status === 'uploading') {
statusBadge = '<span class="status-badge status-uploading"><i class="fa fa-spinner fa-spin mr-1"></i>上传中</span>';
} else {
statusBadge = '<span class="status-badge status-downloaded"><i class="fa fa-check mr-1"></i>已下载</span>';
}
modelElement.innerHTML = `
<div class="flex items-center">
<input type="checkbox" id="delete_model_${model.id}" class="delete-model-checkbox mr-3 h-4 w-4 rounded border-gray-500 text-danger focus:ring-danger" value="${model.id}">
<div>
<label for="delete_model_${model.id}" class="font-medium cursor-pointer">${model.id}</label>
<div class="text-xs text-gray-400">${model.path} (${model.size})</div>
</div>
</div>
<div>
${statusBadge}
</div>
`;
deleteModelsList.appendChild(modelElement);
// 添加复选框事件
const checkbox = modelElement.querySelector('.delete-model-checkbox');
checkbox.addEventListener('change', () => {
if (checkbox.checked) {
selectedDeleteModels.push(model.id);
} else {
selectedDeleteModels = selectedDeleteModels.filter(id => id !== model.id);
}
});
});
}
// 检查进行中的任务
function checkInProgressTasks() {
console.log('检查进行中的任务...');
const modelPath = settings.defaultModelPath;
fetch(`/api/models?all=true&path=${encodeURIComponent(modelPath)}`)
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
const models = data.models || [];
console.log('获取到模型列表:', models);
// 检查是否有正在下载或上传的模型
const downloadingModels = models.filter(model => model.status === 'downloading' && model.progress !== undefined);
const uploadingModels = models.filter(model => model.status === 'uploading' && model.progress !== undefined);
console.log('正在下载的模型:', downloadingModels);
console.log('正在上传的模型:', uploadingModels);
// 如果有正在下载的模型,显示下载进度区域
if (downloadingModels.length > 0) {
const downloadProgressArea = document.getElementById('download-progress');
const progressContainer = document.getElementById('progress-container');
// 显示下载进度区域
downloadProgressArea.classList.remove('hidden');
// 为每个下载中的模型创建进度条
downloadingModels.forEach(model => {
// 使用固定的任务ID格式,基于模型ID
const taskId = `task_download_${model.id}`;
// 存储任务信息
downloadTasks[taskId] = {
task_id: taskId,
model_id: model.id,
local_path: model.path,
type: 'download',
status: 'downloading',
progress: model.progress || 0,
message: model.message || '正在下载...',
start_time: model.downloadTime || new Date().toISOString()
};
// 创建进度条元素
createDownloadProgressElement(taskId, model.id);
// 立即更新进度显示
updateDownloadProgress(taskId, model.progress || 0, model.message || '正在下载...');
// 如果Socket.IO连接已建立,订阅任务更新
if (window.socket) {
window.socket.emit('subscribe_task', { task_id: taskId });
}
// 主动查询任务状态
fetchTaskStatus(taskId, 'download');
});
}
// 如果有正在上传的模型,显示上传进度区域
if (uploadingModels.length > 0) {
const uploadProgressArea = document.getElementById('upload-progress');
const uploadProgressContainer = document.getElementById('upload-progress-container');
// 显示上传进度区域
uploadProgressArea.classList.remove('hidden');
// 为每个上传中的模型创建进度条
uploadingModels.forEach(model => {
// 使用固定的任务ID格式,基于模型ID
const taskId = `task_upload_${model.id}`;
// 存储任务信息
uploadTasks[taskId] = {
task_id: taskId,
model_id: model.id,
local_path: model.path,
type: 'upload',
status: 'uploading',
progress: model.progress || 0,
message: model.message || '正在上传...',
start_time: model.uploadTime || new Date().toISOString()
};
// 创建进度条元素
createUploadProgressElement(taskId, model.id);
// 立即更新进度显示
updateUploadProgress(taskId, model.progress || 0, model.message || '正在上传...');
// 如果Socket.IO连接已建立,订阅任务更新
if (window.socket) {
window.socket.emit('subscribe_task', { task_id: taskId });
}
// 主动查询任务状态
fetchTaskStatus(taskId, 'upload');
});
}
})
.catch(error => {
console.error('检查进行中任务失败:', error);
});
}
// 取消上传
function cancelUpload(taskId, modelId) {
if (confirm(`确定要取消上传模型 ${modelId} 吗?`)) {
console.log(`取消上传任务: ${taskId}, 模型ID: ${modelId}`);
// 从队列中移除任务
uploadQueue = uploadQueue.filter(id => id !== taskId);
// 如果是当前正在上传的任务,取消上传
if (currentUploadTask === taskId) {
// 发送取消请求到后端
fetch(`/api/upload/cancel/${taskId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.catch(error => {
console.error('取消上传失败:', error);
showNotification('错误', '取消上传失败', 'error');
});
currentUploadTask = null;
// 继续处理队列中的下一个任务
processUploadQueue();
}
// 更新任务状态
const task = uploadTasks[taskId];
if (task) {
task.status = 'cancelled';
// 更新UI
const progressElement = document.getElementById(`upload_progress_${taskId}`);
if (progressElement) {
const statusBadge = progressElement.querySelector('.status-badge');
const progressDetail = document.getElementById(`upload_progress_detail_${taskId}`);
const cancelBtn = progressElement.querySelector('.cancel-btn');
if (statusBadge) {
statusBadge.className = 'status-badge bg-gray-700/50 text-gray-300 border border-gray-600';
statusBadge.innerHTML = '<i class="fa fa-ban mr-1"></i>已取消';
}
if (progressDetail) progressDetail.textContent = '上传已取消';
// 隐藏取消按钮
if (cancelBtn) {
cancelBtn.disabled = true;
cancelBtn.textContent = '已取消';
cancelBtn.classList.remove('hover:bg-red-900/50');
cancelBtn.classList.add('bg-gray-800/50', 'text-gray-400', 'cursor-not-allowed');
}
}
// 显示通知
showNotification('提示', `模型 ${modelId} 上传已取消`, 'info');
// 从任务列表中移除
setTimeout(() => {
delete uploadTasks[taskId];
const progressElement = document.getElementById(`upload_progress_${taskId}`);
if (progressElement) {
progressElement.remove();
}
// 如果没有活跃任务,隐藏进度区域
if (Object.keys(uploadTasks).length === 0) {
document.getElementById('upload-progress').classList.add('hidden');
}
// 重新加载模型列表
loadModelsList();
}, 2000);
}
}
}
// 主动查询任务状态
function fetchTaskStatus(taskId, taskType) {
console.log(`[DEBUG] 主动查询任务状态: ${taskId}, 类型: ${taskType}`);
fetch(`/api/task/${taskId}`)
.then(response => {
if (!response.ok) {
throw new Error(`Network response was not ok: ${response.status}`);
}
return response.json();
})
.then(taskData => {
console.log(`[DEBUG] 收到任务状态:`, taskData);
// 更新任务信息
if (taskType === 'download') {
downloadTasks[taskId] = taskData;
updateDownloadProgress(taskId, taskData.progress, taskData.message);
// 如果任务已完成或失败,停止查询
if (taskData.status === 'completed' || taskData.status === 'failed') {
return;
}
} else if (taskType === 'upload') {
uploadTasks[taskId] = taskData;
updateUploadProgress(taskId, taskData.progress, taskData.message);
// 如果任务已完成或失败,停止查询
if (taskData.status === 'uploaded' || taskData.status === 'failed') {
return;
}
}
// 继续定期查询
setTimeout(() => {
fetchTaskStatus(taskId, taskType);
}, 3000); // 每3秒查询一次
})
.catch(error => {
console.error(`[DEBUG] 查询任务状态失败: ${error.message}`);
// 错误后重试
setTimeout(() => {
fetchTaskStatus(taskId, taskType);
}, 5000); // 错误后5秒重试
});
}
// 加载所有模型列表
function loadAllModelsList() {
const allModelsList = document.getElementById('all-models-list');
// 显示加载状态
allModelsList.innerHTML = `
<div class="text-center text-gray-400 py-4">
<i class="fa fa-spinner fa-spin mr-2"></i>
加载中...
</div>
`;
// 从后端获取所有模型列表,传递当前配置的模型路径
const modelPath = settings.defaultModelPath;
// 使用AbortController设置超时
const controller = new AbortController();
const signal = controller.signal;
// 设置30秒超时
const timeoutId = setTimeout(() => {
controller.abort();
allModelsList.innerHTML = `
<div class="text-center text-red-400 py-4">
<i class="fa fa-exclamation-triangle mr-2"></i>
加载超时: 请检查后端服务是否运行
</div>
`;
}, 30000);
fetch(`/api/models?all=true&path=${encodeURIComponent(modelPath)}`, {
signal: signal
})
.then(response => {
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`Network response was not ok: ${response.status}`);
}
return response.json();
})
.then(data => {
clearTimeout(timeoutId);
const models = data.models || [];
// 更新所有模型列表
updateAllModelsList(models);
})
.catch(error => {
clearTimeout(timeoutId);
console.error('Error loading all models list:', error);
let errorMessage = error.message;
if (error.name === 'AbortError') {
errorMessage = '请求超时';
}
allModelsList.innerHTML = `
<div class="text-center text-red-400 py-4">
<i class="fa fa-exclamation-triangle mr-2"></i>
加载失败: ${errorMessage}
</div>
`;
});
}
// 更新所有模型列表
function updateAllModelsList(models) {
const allModelsList = document.getElementById('all-models-list');
if (models.length === 0) {
allModelsList.innerHTML = `
<div class="text-center text-gray-400 py-4">
没有模型
</div>
`;
return;
}
// 清空列表
allModelsList.innerHTML = '';
// 添加模型项
models.forEach(model => {
const modelElement = document.createElement('div');
modelElement.className = 'model-item';
// 确定状态标签
let statusBadge = '';
if (model.status === 'uploaded') {
statusBadge = '<span class="status-badge status-uploaded"><i class="fa fa-cloud-upload mr-1"></i>已上传</span>';
} else if (model.status === 'downloading') {
statusBadge = '<span class="status-badge status-downloading"><i class="fa fa-spinner fa-spin mr-1"></i>下载中</span>';
} else if (model.status === 'uploading') {
statusBadge = '<span class="status-badge status-uploading"><i class="fa fa-spinner fa-spin mr-1"></i>上传中</span>';
} else {
statusBadge = '<span class="status-badge status-downloaded"><i class="fa fa-check mr-1"></i>已下载</span>';
}
modelElement.innerHTML = `
<div class="flex justify-between items-center mb-2">
<h3 class="font-medium">${model.id}</h3>
${statusBadge}
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-2 text-sm">
<div class="text-gray-400">
<span class="text-gray-500">路径:</span> ${model.path}
</div>
<div class="text-gray-400">
<span class="text-gray-500">大小:</span> ${model.size}
</div>
<div class="text-gray-400">
<span class="text-gray-500">下载时间:</span> ${model.downloadTime}
</div>
<div class="text-gray-400">
<span class="text-gray-500">上传时间:</span> ${model.uploadTime || '未上传'}
</div>
</div>
`;
allModelsList.appendChild(modelElement);
});
}
// 开始上传
function startUpload() {
if (selectedModels.length === 0) {
showNotification('错误', '请选择要上传的模型', 'error');
return;
}
// 显示上传进度区域
document.getElementById('upload-progress').classList.remove('hidden');
// 清空上传队列
uploadQueue = [];
// 为每个模型创建上传进度条并添加到队列
selectedModels.forEach(modelId => {
const taskId = `upload_task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
// 保存任务信息
const task = {
task_id: taskId,
modelId,
status: 'pending',
progress: 0,
message: '准备上传...'
};
uploadTasks[taskId] = task;
uploadQueue.push(taskId);
// 保存到localStorage
saveUploadTask(task);
// 创建进度条元素
const progressElement = document.createElement('div');
progressElement.id = `upload_progress_${taskId}`;
progressElement.className = 'model-item';
progressElement.innerHTML = `
<div class="flex justify-between items-center mb-2">
<div>
<h3 class="font-medium">${modelId}</h3>
<span class="status-badge status-pending">
<i class="fa fa-clock-o mr-1"></i>
等待中...
</span>
</div>
<div class="flex items-center space-x-2">
<span class="text-sm text-gray-400" id="upload_progress_text_${taskId}">0%</span>
<button class="delete-btn bg-red-900/30 hover:bg-red-900/50 text-red-300 text-xs px-2 py-1 rounded border border-red-800/50"
onclick="deleteUploadTask('${taskId}', '${modelId}')">
<i class="fa fa-trash mr-1"></i>删除
</button>
</div>
</div>
<div class="progress-bar">
<div class="progress-value" id="upload_progress_value_${taskId}" style="width: 0%"></div>
</div>
<div class="mt-2 text-xs text-gray-400" id="upload_progress_detail_${taskId}">
准备上传...
</div>
`;
document.getElementById('upload-progress-container').appendChild(progressElement);
});
// 开始处理上传队列
processUploadQueue();
// 清空选中的模型
selectedModels = [];
console.log('开始上传模型:', selectedModels);
}
// 保存上传任务到localStorage
function saveUploadTask(task) {
try {
const savedTasks = JSON.parse(localStorage.getItem('uploadTasks') || '{}');
savedTasks[task.task_id] = {
task_id: task.task_id,
modelId: task.modelId,
status: task.status,
progress: task.progress,
message: task.message
};
localStorage.setItem('uploadTasks', JSON.stringify(savedTasks));
} catch (error) {
console.error('保存上传任务失败:', error);
}
}
// 从localStorage加载上传任务
function loadUploadTasks() {
try {
const savedTasks = JSON.parse(localStorage.getItem('uploadTasks') || '{}');
const activeTasks = [];
// 清空进度容器
const uploadProgressContainer = document.getElementById('upload-progress-container');
uploadProgressContainer.innerHTML = '';
// 用于存储需要验证的任务
const tasksToVerify = [];
// 恢复所有任务
Object.values(savedTasks).forEach(task => {
tasksToVerify.push(task);
});
// 如果有任务需要验证,先向后端确认任务是否还存在
if (tasksToVerify.length > 0) {
// 先显示进度区域
document.getElementById('upload-progress').classList.remove('hidden');
let verifiedCount = 0;
tasksToVerify.forEach(task => {
fetch(`/api/task/${task.task_id}`, {
method: 'GET'
})
.then(response => {
if (response.ok) {
return response.json();
}
return null;
})
.then(taskData => {
verifiedCount++;
// 无论任务是否在后端存在,都显示前端存储的任务
activeTasks.push(task);
// 确定任务状态
let taskStatus = task.status;
let taskProgress = task.progress || 0;
let taskMessage = task.message || '恢复任务...';
// 如果后端有任务数据,使用后端数据
if (taskData) {
taskStatus = taskData.status;
taskProgress = taskData.progress || 0;
taskMessage = taskData.message || taskMessage;
}
// 创建进度条元素
const progressElement = document.createElement('div');
progressElement.id = `upload_progress_${task.task_id}`;
progressElement.className = 'model-item';
// 根据任务状态生成不同的HTML
let statusBadge = '';
let buttonsHTML = '';
switch (taskStatus) {
case 'uploading':
statusBadge = '<span class="status-badge status-uploading"><i class="fa fa-spinner fa-spin mr-1"></i>上传中...</span>';
buttonsHTML = `
<button class="delete-btn bg-red-900/30 hover:bg-red-900/50 text-red-300 text-xs px-2 py-1 rounded border border-red-800/50"
onclick="deleteUploadTask('${task.task_id}', '${task.modelId}')">
<i class="fa fa-trash mr-1"></i>删除
</button>
`;
break;
case 'pending':
statusBadge = '<span class="status-badge status-pending"><i class="fa fa-clock-o mr-1"></i>等待中...</span>';
buttonsHTML = `
<button class="delete-btn bg-red-900/30 hover:bg-red-900/50 text-red-300 text-xs px-2 py-1 rounded border border-red-800/50"
onclick="deleteUploadTask('${task.task_id}', '${task.modelId}')">
<i class="fa fa-trash mr-1"></i>删除
</button>
`;
break;
case 'completed':
statusBadge = '<span class="status-badge bg-green-900/50 text-green-300 border border-green-700"><i class="fa fa-check mr-1"></i>上传完成</span>';
buttonsHTML = `
<button class="delete-btn bg-red-900/30 hover:bg-red-900/50 text-red-300 text-xs px-2 py-1 rounded border border-red-800/50"
onclick="deleteUploadTask('${task.task_id}', '${task.modelId}')">
<i class="fa fa-trash mr-1"></i>删除
</button>
`;
break;
case 'failed':
statusBadge = '<span class="status-badge bg-red-900/50 text-red-300 border border-red-700"><i class="fa fa-times mr-1"></i>上传失败</span>';
buttonsHTML = `
<button class="delete-btn bg-red-900/30 hover:bg-red-900/50 text-red-300 text-xs px-2 py-1 rounded border border-red-800/50"
onclick="deleteUploadTask('${task.task_id}', '${task.modelId}')">
<i class="fa fa-trash mr-1"></i>删除
</button>
`;
break;
case 'cancelled':
statusBadge = '<span class="status-badge bg-gray-700/50 text-gray-300 border border-gray-600"><i class="fa fa-ban mr-1"></i>已取消</span>';
buttonsHTML = `
<button class="delete-btn bg-red-900/30 hover:bg-red-900/50 text-red-300 text-xs px-2 py-1 rounded border border-red-800/50"
onclick="deleteUploadTask('${task.task_id}', '${task.modelId}')">
<i class="fa fa-trash mr-1"></i>删除
</button>
`;
break;
default:
statusBadge = '<span class="status-badge status-pending"><i class="fa fa-clock-o mr-1"></i>等待中...</span>';
buttonsHTML = `
<button class="delete-btn bg-red-900/30 hover:bg-red-900/50 text-red-300 text-xs px-2 py-1 rounded border border-red-800/50"
onclick="deleteUploadTask('${task.task_id}', '${task.modelId}')">
<i class="fa fa-trash mr-1"></i>删除
</button>
`;
}
// 构建完整的HTML
progressElement.innerHTML = `
<div class="flex justify-between items-center mb-2">
<div>
<h3 class="font-medium">${task.modelId}</h3>
${statusBadge}
</div>
<div class="flex items-center space-x-2">
<span class="text-sm text-gray-400" id="upload_progress_text_${task.task_id}">${Math.round(taskProgress)}%</span>
${buttonsHTML}
</div>
</div>
<div class="progress-bar">
<div class="progress-value" id="upload_progress_value_${task.task_id}" style="width: ${taskProgress}%"></div>
</div>
<div class="mt-2 text-xs text-gray-400" id="upload_progress_detail_${task.task_id}">
${taskMessage}
</div>
`;
uploadProgressContainer.appendChild(progressElement);
// 恢复任务到全局变量
uploadTasks[task.task_id] = task;
// 如果任务状态是 pending,添加到上传队列
if (task.status === 'pending') {
uploadQueue.push(task.task_id);
}
// 所有验证完成后更新 localStorage 并处理队列
if (verifiedCount === tasksToVerify.length) {
localStorage.setItem('uploadTasks', JSON.stringify(savedTasks));
console.log('已恢复', activeTasks.length, '个上传任务');
// 如果没有活跃任务,隐藏进度区域
if (activeTasks.length === 0) {
document.getElementById('upload-progress').classList.add('hidden');
} else {
// 开始处理上传队列
processUploadQueue();
}
}
})
.catch(error => {
verifiedCount++;
console.error('验证上传任务失败:', error);
// 即使验证失败,也显示任务
activeTasks.push(task);
// 创建进度条元素
const progressElement = document.createElement('div');
progressElement.id = `upload_progress_${task.task_id}`;
progressElement.className = 'model-item';
progressElement.innerHTML = `
<div class="flex justify-between items-center mb-2">
<div>
<h3 class="font-medium">${task.modelId}</h3>
<span class="status-badge bg-gray-700/50 text-gray-300 border border-gray-600">
<i class="fa fa-exclamation mr-1"></i>状态未知
</span>
</div>
<div class="flex items-center space-x-2">
<span class="text-sm text-gray-400" id="upload_progress_text_${task.task_id}">${Math.round(task.progress || 0)}%</span>
<button class="delete-btn bg-red-900/30 hover:bg-red-900/50 text-red-300 text-xs px-2 py-1 rounded border border-red-800/50"
onclick="deleteUploadTask('${task.task_id}', '${task.modelId}')">
<i class="fa fa-trash mr-1"></i>删除
</button>
</div>
</div>
<div class="progress-bar">
<div class="progress-value" id="upload_progress_value_${task.task_id}" style="width: ${task.progress || 0}%"></div>
</div>
<div class="mt-2 text-xs text-gray-400" id="upload_progress_detail_${task.task_id}">
任务状态未知
</div>
`;
uploadProgressContainer.appendChild(progressElement);
// 恢复任务到全局变量
uploadTasks[task.task_id] = task;
if (verifiedCount === tasksToVerify.length) {
localStorage.setItem('uploadTasks', JSON.stringify(savedTasks));
console.log('已恢复', activeTasks.length, '个上传任务');
if (activeTasks.length === 0) {
document.getElementById('upload-progress').classList.add('hidden');
}
}
});
});
} else {
// 没有任务,隐藏进度区域
document.getElementById('upload-progress').classList.add('hidden');
}
} catch (error) {
console.error('加载上传任务失败:', error);
document.getElementById('upload-progress').classList.add('hidden');
}
}
// 处理上传队列
function processUploadQueue() {
if (currentUploadTask || uploadQueue.length === 0) {
return;
}
// 获取队列中的第一个任务
const taskId = uploadQueue.shift();
const task = uploadTasks[taskId];
if (!task || task.status === 'cancelled' || task.status === 'failed') {
// 跳过已取消或失败的任务
processUploadQueue();
return;
}
// 更新任务状态为上传中
task.status = 'uploading';
// 更新UI
const progressElement = document.getElementById(`upload_progress_${taskId}`);
if (progressElement) {
const statusBadge = progressElement.querySelector('.status-badge');
if (statusBadge) {
statusBadge.className = 'status-badge status-uploading';
statusBadge.innerHTML = '<i class="fa fa-spinner fa-spin mr-1"></i>上传中...';
}
}
// 设置当前上传任务
currentUploadTask = taskId;
// 发送请求到后端开始上传
fetch('/api/upload', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
model_ids: [task.modelId],
create_repo_flag: true
})
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
console.log('上传请求已发送:', data);
if (data.error) {
throw new Error(data.error);
}
})
.catch(error => {
console.error('Error starting upload:', error);
showNotification('错误', `启动上传失败: ${error.message}`, 'error');
// 更新任务状态为失败
task.status = 'failed';
currentUploadTask = null;
const progressElement = document.getElementById(`upload_progress_${taskId}`);
if (progressElement) {
const statusBadge = progressElement.querySelector('.status-badge');
const progressDetail = document.getElementById(`upload_progress_detail_${taskId}`);
if (statusBadge) {
statusBadge.className = 'status-badge bg-red-900/50 text-red-300 border border-red-700';
statusBadge.innerHTML = '<i class="fa fa-times mr-1"></i>上传失败';
}
if (progressDetail) progressDetail.textContent = `上传失败: ${error.message}`;
}
// 继续处理队列
processUploadQueue();
});
}
// 删除上传任务
function deleteUploadTask(taskId, modelId) {
if (confirm(`确定要删除上传任务 ${modelId} 吗?`)) {
// 从队列中移除任务
uploadQueue = uploadQueue.filter(id => id !== taskId);
// 如果是当前正在上传的任务,取消它
if (currentUploadTask === taskId) {
// 发送取消请求到后端
fetch(`/api/upload/cancel/${taskId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.catch(error => console.error('取消上传失败:', error));
currentUploadTask = null;
}
// 从uploadTasks对象中删除任务
delete uploadTasks[taskId];
// 更新localStorage
const savedTasks = JSON.parse(localStorage.getItem('uploadTasks') || '{}');
delete savedTasks[taskId];
localStorage.setItem('uploadTasks', JSON.stringify(savedTasks));
// 从UI中移除任务
const progressElement = document.getElementById(`upload_progress_${taskId}`);
if (progressElement) {
progressElement.remove();
}
// 显示通知
showNotification('提示', `上传任务 ${modelId} 已删除`, 'info');
// 继续处理队列
processUploadQueue();
// 如果没有活跃任务,隐藏进度区域
if (Object.keys(uploadTasks).length === 0) {
document.getElementById('upload-progress').classList.add('hidden');
}
}
}
// 上传单个模型
function uploadSingleModel(modelId) {
// 显示上传进度区域
document.getElementById('upload-progress').classList.remove('hidden');
// 创建任务ID
const taskId = `upload_task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
// 保存任务信息
uploadTasks[taskId] = {
modelId,
status: 'uploading',
progress: 0
};
// 清空上传进度容器
const uploadProgressContainer = document.getElementById('upload-progress-container');
// 创建进度条元素
const progressElement = document.createElement('div');
progressElement.id = `upload_progress_${taskId}`;
progressElement.className = 'model-item';
progressElement.innerHTML = `
<div class="flex justify-between items-center mb-2">
<div>
<h3 class="font-medium">${modelId}</h3>
<span class="status-badge status-uploading">
<i class="fa fa-spinner fa-spin mr-1"></i>
上传中...
</span>
</div>
<span class="text-sm text-gray-400" id="upload_progress_text_${taskId}">0%</span>
</div>
<div class="progress-bar">
<div class="progress-value" id="upload_progress_value_${taskId}" style="width: 0%"></div>
</div>
<div class="mt-2 text-xs text-gray-400" id="upload_progress_detail_${taskId}">
准备上传...
</div>
`;
uploadProgressContainer.appendChild(progressElement);
// 发送请求到后端开始上传
fetch('/api/upload', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
model_ids: [modelId],
create_repo_flag: true
})
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
console.log('上传请求已发送:', data);
if (data.error) {
throw new Error(data.error);
}
})
.catch(error => {
console.error('Error starting upload:', error);
showNotification('错误', `启动上传失败: ${error.message}`, 'error');
// 更新任务状态为失败
task.status = 'failed';
const progressElement = document.getElementById(`upload_progress_${taskId}`);
if (progressElement) {
const statusBadge = progressElement.querySelector('.status-badge');
const progressDetail = document.getElementById(`upload_progress_detail_${taskId}`);
if (statusBadge) {
statusBadge.className = 'status-badge bg-red-900/50 text-red-300 border border-red-700';
statusBadge.innerHTML = '<i class="fa fa-times mr-1"></i>上传失败';
}
if (progressDetail) progressDetail.textContent = `上传失败: ${error.message}`;
}
});
console.log('开始上传模型:', modelId);
}
// 模拟上传进度
function simulateUploadProgress(taskId) {
const task = uploadTasks[taskId];
if (!task) return;
// 模拟进度增加
let progress = task.progress;
const interval = setInterval(() => {
// 随机增加进度
const increment = Math.random() * 8;
progress = Math.min(progress + increment, 100);
// 更新任务进度
task.progress = progress;
// 更新UI
const progressText = document.getElementById(`upload_progress_text_${taskId}`);
const progressValue = document.getElementById(`upload_progress_value_${taskId}`);
const progressDetail = document.getElementById(`upload_progress_detail_${taskId}`);
if (progressText) progressText.textContent = `${Math.round(progress)}%`;
if (progressValue) progressValue.style.width = `${progress}%`;
// 随机更新详细信息
const details = [
`上传中: ${Math.round(progress)}%`,
`正在上传模型文件...`,
`已上传 ${Math.round(progress * 100 / 100)}MB / 100MB`,
`正在验证上传文件...`
];
if (progressDetail) {
progressDetail.textContent = details[Math.floor(Math.random() * details.length)];
}
// 检查是否完成
if (progress >= 100) {
clearInterval(interval);
// 模拟上传完成
setTimeout(() => {
handleUploadComplete({
taskId,
modelId: task.modelId
});
}, 500);
}
}, 1000);
// 保存定时器ID
task.interval = interval;
}
// 更新上传进度
function updateUploadProgress(data) {
const { taskId, progress, detail } = data;
const task = uploadTasks[taskId];
if (task) {
task.progress = progress;
// 更新UI
const progressText = document.getElementById(`upload_progress_text_${taskId}`);
const progressValue = document.getElementById(`upload_progress_value_${taskId}`);
const progressDetail = document.getElementById(`upload_progress_detail_${taskId}`);
if (progressText) progressText.textContent = `${Math.round(progress)}%`;
if (progressValue) progressValue.style.width = `${progress}%`;
if (progressDetail && detail) progressDetail.textContent = detail;
}
}
// 处理上传完成
function handleUploadComplete(data) {
const { taskId, modelId } = data;
const task = uploadTasks[taskId];
if (task) {
// 清除定时器
if (task.interval) {
clearInterval(task.interval);
}
// 更新任务状态
task.status = 'uploaded';
task.progress = 100;
// 更新UI
const progressElement = document.getElementById(`upload_progress_${taskId}`);
if (progressElement) {
const statusBadge = progressElement.querySelector('.status-badge');
const progressText = document.getElementById(`upload_progress_text_${taskId}`);
const progressValue = document.getElementById(`upload_progress_value_${taskId}`);
const progressDetail = document.getElementById(`upload_progress_detail_${taskId}`);
if (statusBadge) {
statusBadge.className = 'status-badge status-uploaded';
statusBadge.innerHTML = '<i class="fa fa-check mr-1"></i>上传完成';
}
if (progressText) progressText.textContent = '100%';
if (progressValue) {
progressValue.style.width = '100%';
progressValue.classList.add('bg-secondary');
}
if (progressDetail) progressDetail.textContent = `模型已成功上传到 CsgHub`;
}
// 显示通知
showNotification('成功', `模型 ${modelId} 上传完成`, 'success');
// 清空当前上传任务
currentUploadTask = null;
// 继续处理队列中的下一个任务
processUploadQueue();
// 如果是最后一个任务,重新加载模型列表
if (Object.values(uploadTasks).every(t => t.status === 'uploaded' || t.status === 'failed' || t.status === 'cancelled')) {
setTimeout(() => {
loadModelsList();
loadAllModelsList();
}, 1000);
}
}
}
// 处理上传失败
function handleUploadFailed(data) {
const { taskId, modelId, error } = data;
const task = uploadTasks[taskId];
if (task) {
// 清除定时器
if (task.interval) {
clearInterval(task.interval);
}
// 更新任务状态
task.status = 'failed';
// 更新UI
const progressElement = document.getElementById(`upload_progress_${taskId}`);
if (progressElement) {
const statusBadge = progressElement.querySelector('.status-badge');
const progressValue = document.getElementById(`upload_progress_value_${taskId}`);
const progressDetail = document.getElementById(`upload_progress_detail_${taskId}`);
if (statusBadge) {
statusBadge.className = 'status-badge bg-red-900/50 text-red-300 border border-red-700';
statusBadge.innerHTML = '<i class="fa fa-times mr-1"></i>上传失败';
}
if (progressValue) {
progressValue.classList.add('bg-danger');
}
if (progressDetail) progressDetail.textContent = `上传失败: ${error}`;
}
// 显示通知
showNotification('错误', `模型 ${modelId} 上传失败`, 'error');
// 清空当前上传任务
currentUploadTask = null;
// 继续处理队列中的下一个任务
processUploadQueue();
}
}
// 显示删除确认对话框
function showDeleteConfirm() {
if (selectedDeleteModels.length === 0) {
showNotification('错误', '请选择要删除的模型', 'error');
return;
}
// 更新删除数量
document.getElementById('delete-count').textContent = selectedDeleteModels.length;
// 显示确认对话框
deleteConfirmModal.classList.remove('hidden');
}
// 确认删除
function confirmDelete() {
console.log('[DEBUG] confirmDelete函数被调用');
console.log('[DEBUG] selectedDeleteModels:', selectedDeleteModels);
if (selectedDeleteModels.length === 0) {
console.log('[DEBUG] 没有选中的模型,直接返回');
return;
}
// 隐藏确认对话框
hideDeleteConfirm();
// 显示加载状态
const deleteBtn = document.querySelector('#close-delete-confirm-btn');
const originalText = deleteBtn.textContent;
deleteBtn.disabled = true;
deleteBtn.innerHTML = '<i class="fa fa-spinner fa-spin mr-2"></i>删除中...';
// 发送请求到后端删除模型
console.log('[DEBUG] 准备发送删除请求到后端');
console.log('[DEBUG] 请求URL: /api/delete');
console.log('[DEBUG] 请求数据:', { model_ids: selectedDeleteModels });
fetch('/api/delete', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
model_ids: selectedDeleteModels
})
})
.then(response => {
console.log('[DEBUG] 收到后端响应:', response);
console.log('[DEBUG] 响应状态码:', response.status);
console.log('[DEBUG] 响应状态文本:', response.statusText);
if (!response.ok) {
throw new Error(`Network response was not ok: ${response.status} ${response.statusText}`);
}
return response.json();
})
.then(data => {
console.log('[DEBUG] 删除结果:', data);
if (data.deleted && data.deleted.length > 0) {
showNotification('成功', `已成功删除 ${data.deleted.length} 个模型`, 'success');
// 重新加载模型列表
setTimeout(() => {
loadDeleteModelsList();
loadAllModelsList();
}, 500);
}
if (data.errors && data.errors.length > 0) {
showNotification('警告', `部分模型删除失败: ${data.errors.join(', ')}`, 'warning');
}
// 清空选中的模型
selectedDeleteModels = [];
})
.catch(error => {
console.error('[DEBUG] 删除模型失败:', error);
console.error('[DEBUG] 错误详情:', error.stack);
showNotification('错误', `删除模型失败: ${error.message}`, 'error');
})
.finally(() => {
// 恢复按钮状态
deleteBtn.disabled = false;
deleteBtn.textContent = originalText;
});
}
// 隐藏删除确认对话框
function hideDeleteConfirm() {
deleteConfirmModal.classList.add('hidden');
}
// 显示设置对话框
function showSettings() {
// 加载当前设置
document.getElementById('default-model-path').value = settings.defaultModelPath;
document.getElementById('max-retry').value = settings.maxRetry;
document.getElementById('csghub-url').value = settings.csghubUrl;
document.getElementById('csghub-token').value = settings.csghubToken;
// 显示设置对话框
settingsModal.classList.remove('hidden');
}
// 隐藏设置对话框
function hideSettings() {
settingsModal.classList.add('hidden');
}
// 保存设置
function saveSettings() {
const defaultModelPath = document.getElementById('default-model-path').value.trim();
const maxRetry = parseInt(document.getElementById('max-retry').value);
const csghubUrl = document.getElementById('csghub-url').value.trim();
const csghubToken = document.getElementById('csghub-token').value.trim();
if (!defaultModelPath) {
showNotification('错误', '请输入默认模型路径', 'error');
return;
}
if (isNaN(maxRetry) || maxRetry < 1 || maxRetry > 20) {
showNotification('错误', '最大重试次数必须在 1-20 之间', 'error');
return;
}
if (!csghubUrl) {
showNotification('错误', '请输入 CsgHub API URL', 'error');
return;
}
if (!csghubToken) {
showNotification('错误', '请输入 CsgHub Token', 'error');
return;
}
// 更新设置
settings = {
defaultModelPath,
maxRetry,
csghubUrl,
csghubToken
};
// 保存到本地存储
localStorage.setItem('modelManagerSettings', JSON.stringify(settings));
// 更新本地路径输入框
document.getElementById('local-path').value = settings.defaultModelPath;
// 更新顶部状态栏的模型目录显示
const modelDirElement = document.getElementById('model-dir');
if (modelDirElement) {
modelDirElement.textContent = settings.defaultModelPath;
}
// 隐藏设置对话框
hideSettings();
// 显示通知
showNotification('成功', '设置已保存', 'success');
console.log('保存设置:', settings);
}
// 加载设置
function loadSettings() {
const savedSettings = localStorage.getItem('modelManagerSettings');
if (savedSettings) {
try {
settings = JSON.parse(savedSettings);
// 更新本地路径输入框
document.getElementById('local-path').value = settings.defaultModelPath;
} catch (error) {
console.error('加载设置失败:', error);
}
}
}
// 切换全选
function toggleSelectAll() {
const checkboxes = document.querySelectorAll('.model-checkbox');
const isAllSelected = selectedModels.length === checkboxes.length;
checkboxes.forEach(checkbox => {
checkbox.checked = !isAllSelected;
});
// 更新选中模型数组
selectedModels = isAllSelected ? [] : Array.from(checkboxes).map(cb => cb.value);
}
// 切换删除全选
function toggleSelectAllDelete() {
const checkboxes = document.querySelectorAll('.delete-model-checkbox');
const isAllSelected = selectedDeleteModels.length === checkboxes.length;
checkboxes.forEach(checkbox => {
checkbox.checked = !isAllSelected;
});
// 更新选中模型数组
selectedDeleteModels = isAllSelected ? [] : Array.from(checkboxes).map(cb => cb.value);
}
// 显示通知
function showNotification(title, message, type = 'info') {
const notificationTitle = document.getElementById('notification-title');
const notificationMessage = document.getElementById('notification-message');
const notificationIcon = document.getElementById('notification-icon');
// 设置通知内容
notificationTitle.textContent = title;
notificationMessage.textContent = message;
// 设置图标
let iconClass = 'fa-info-circle text-blue-500';
if (type === 'success') {
iconClass = 'fa-check-circle text-green-500';
} else if (type === 'error') {
iconClass = 'fa-times-circle text-red-500';
} else if (type === 'warning') {
iconClass = 'fa-exclamation-triangle text-yellow-500';
}
notificationIcon.innerHTML = `<i class="fa ${iconClass} text-xl"></i>`;
// 显示通知
notification.classList.remove('translate-x-full');
// 3秒后自动隐藏
setTimeout(() => {
hideNotification();
}, 3000);
}
// 隐藏通知
function hideNotification() {
notification.classList.add('translate-x-full');
}
// 加载系统信息
function loadSystemInfo() {
// 模拟加载系统信息
setTimeout(() => {
const systemInfo = {
os: 'Linux Ubuntu 22.04',
modelDir: '/home/user/models',
diskUsage: '65%',
memoryUsage: '42%'
};
// 更新系统信息
document.getElementById('system-info').textContent = systemInfo.os;
document.getElementById('model-dir').textContent = systemInfo.modelDir;
}, 500);
}
// 更新系统信息
function updateSystemInfo(data) {
if (data.os) {
document.getElementById('system-info').textContent = data.os;
}
if (data.modelDir) {
document.getElementById('model-dir').textContent = data.modelDir;
}
}
// 页面加载完成后初始化 - 已移至主初始化函数
</script>
</body>
</html>
\ No newline at end of file
Flask==2.0.1
Flask-SocketIO==5.1.1
eventlet==0.33.1
SQLAlchemy==1.4.23
Werkzeug==2.0.1
Jinja2==3.0.1
MarkupSafe==2.0.1
itsdangerous==2.0.1
click==8.0.1
python-engineio==4.2.1
python-socketio==5.4.0
greenlet==1.1.2
six==1.16.0
dnspython==2.2.1
\ No newline at end of file
#!/bin/bash
# 模型管理工具启动脚本
echo "========================================="
echo "Linux模型管理工具启动脚本"
echo "========================================="
# 检查Python版本
echo "检查Python版本..."
if ! command -v python3 &> /dev/null; then
echo "错误: 未找到Python3,请先安装Python3"
exit 1
fi
PYTHON_VERSION=$(python3 --version 2>&1 | awk '{print $2}')
echo "已安装Python版本: $PYTHON_VERSION"
# 创建虚拟环境(如果不存在)
if [ ! -d "venv" ]; then
echo "创建Python虚拟环境..."
python3 -m venv venv
if [ $? -ne 0 ]; then
echo "错误: 创建虚拟环境失败"
exit 1
fi
fi
# 激活虚拟环境
echo "激活虚拟环境..."
if [ $? -ne 0 ]; then
echo "错误: 激活虚拟环境失败"
exit 1
fi
# 升级pip
echo "升级pip..."
pip3 install --upgrade pip -i https://pypi.tuna.tsinghua.edu.cn/simple
if [ $? -ne 0 ]; then
echo "警告: 升级pip失败"
fi
# 安装依赖
echo "安装依赖包..."
pip install flask flask-cors flask-socketio eventlet requests modelscope -i https://pypi.tuna.tsinghua.edu.cn/simple
if [ $? -ne 0 ]; then
echo "警告: 安装依赖包失败,某些功能可能受限"
fi
# 创建必要的目录
echo "创建必要的目录..."
# mkdir -p ~/models
mkdir -p backend/static
mkdir -p backend/templates
# 检查端口是否被占用
PORT=2026
if lsof -Pi :$PORT -sTCP:LISTEN -t >/dev/null 2>&1; then
echo "警告: 端口 $PORT 已被占用,请先关闭占用该端口的进程"
echo "您可以使用以下命令关闭占用端口的进程:"
echo "sudo lsof -t -i:$PORT | xargs kill -9"
read -p "是否继续启动?(y/N): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 1
fi
fi
# 启动应用
echo "========================================="
echo "启动模型管理工具..."
echo "访问地址: http://localhost:$PORT"
echo "========================================="
# 切换到backend目录
cd backend
# 启动Flask应用
python3 app.py
\ No newline at end of file
home = /usr/bin
include-system-site-packages = false
version = 3.10.12
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Linux模型管理工具</title>
<!-- Tailwind CSS v3 -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Font Awesome -->
<link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
<!-- Chart.js -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.8/dist/chart.umd.min.js"></script>
<!-- Socket.IO Client -->
<script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script>
<!-- 统一的 Tailwind 配置 -->
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#0ea5e9',
secondary: '#6366f1',
success: '#10b981',
warning: '#f59e0b',
danger: '#ef4444',
dark: '#1e293b',
'dark-light': '#334155',
'dark-lighter': '#475569'
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
},
animation: {
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
}
}
}
}
</script>
<style type="text/tailwindcss">
@layer utilities {
.content-auto {
content-visibility: auto;
}
.glass-effect {
background: rgba(30, 41, 59, 0.7);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.progress-bar-glow {
box-shadow: 0 0 10px theme('colors.primary'), 0 0 20px theme('colors.primary');
}
.sidebar-icon {
@apply relative flex items-center justify-center h-12 w-12 mt-2 mb-2 mx-auto shadow-lg
bg-dark-light text-primary hover:bg-primary hover:text-white
rounded-3xl hover:rounded-xl transition-all duration-300 ease-linear
cursor-pointer;
}
.sidebar-tooltip {
@apply absolute w-auto p-2 m-2 min-w-max left-14 rounded-md shadow-md
text-white bg-dark-lighter
text-xs font-bold transition-all duration-100 scale-0 origin-left z-50;
}
.sidebar-hr {
@apply bg-dark-lighter border border-dark-lighter rounded-full mx-2;
}
.main-container {
@apply flex flex-col md:flex-row h-screen bg-dark text-white overflow-hidden;
}
.sidebar {
@apply bg-dark-light w-full md:w-16 flex flex-col items-center
md:items-start md:py-8 md:px-2;
}
.main-content {
@apply flex-1 flex flex-col overflow-hidden;
}
.top-bar {
@apply glass-effect flex justify-between items-center p-4;
}
.content-area {
@apply flex-1 overflow-y-auto p-6 bg-gradient-to-br from-dark to-dark-light;
}
.card {
@apply glass-effect rounded-xl p-6 shadow-lg mb-6;
}
.btn-primary {
@apply bg-primary hover:bg-primary/80 text-white font-bold py-2 px-4 rounded-lg
transition-all duration-300 transform hover:scale-105 focus:outline-none focus:ring-2
focus:ring-primary focus:ring-opacity-50;
}
.btn-secondary {
@apply bg-secondary hover:bg-secondary/80 text-white font-bold py-2 px-4 rounded-lg
transition-all duration-300 transform hover:scale-105 focus:outline-none focus:ring-2
focus:ring-secondary focus:ring-opacity-50;
}
.btn-danger {
@apply bg-danger hover:bg-danger/80 text-white font-bold py-2 px-4 rounded-lg
transition-all duration-300 transform hover:scale-105 focus:outline-none focus:ring-2
focus:ring-danger focus:ring-opacity-50;
}
.input-field {
@apply bg-dark-lighter text-white rounded-lg border border-dark-lighter
focus:border-primary focus:ring-2 focus:ring-primary focus:outline-none
px-4 py-2 transition-all duration-300;
}
.progress-bar {
@apply h-2 rounded-full bg-dark-lighter overflow-hidden;
}
.progress-value {
@apply h-full bg-primary rounded-full transition-all duration-300 ease-out;
}
.tab-active {
@apply border-b-2 border-primary text-primary;
}
.tab-inactive {
@apply text-gray-400 hover:text-white;
}
.model-item {
@apply glass-effect rounded-lg p-4 mb-4 transition-all duration-300
hover:shadow-lg hover:shadow-primary/20;
}
.status-badge {
@apply px-2 py-1 rounded-full text-xs font-bold;
}
.status-downloading {
@apply bg-blue-900/50 text-blue-300 border border-blue-700;
}
.status-downloaded {
@apply bg-green-900/50 text-green-300 border border-green-700;
}
.status-uploading {
@apply bg-purple-900/50 text-purple-300 border border-purple-700;
}
.status-uploaded {
@apply bg-yellow-900/50 text-yellow-300 border border-yellow-700;
}
}
</style>
</head>
<body class="bg-dark text-white">
<div class="main-container">
<!-- 侧边栏 -->
<div class="sidebar">
<div class="sidebar-icon group">
<i class="fa fa-download text-xl"></i>
<span class="sidebar-tooltip group-hover:scale-100">下载模型</span>
</div>
<hr class="sidebar-hr" />
<div class="sidebar-icon group">
<i class="fa fa-upload text-xl"></i>
<span class="sidebar-tooltip group-hover:scale-100">上传模型</span>
</div>
<hr class="sidebar-hr" />
<div class="sidebar-icon group">
<i class="fa fa-trash text-xl"></i>
<span class="sidebar-tooltip group-hover:scale-100">删除模型</span>
</div>
<hr class="sidebar-hr" />
<div class="sidebar-icon group">
<i class="fa fa-list text-xl"></i>
<span class="sidebar-tooltip group-hover:scale-100">模型列表</span>
</div>
</div>
<!-- 主内容区 -->
<div class="main-content">
<!-- 顶部状态栏 -->
<div class="top-bar">
<div class="flex items-center">
<h1 class="text-2xl font-bold text-primary">Linux模型管理工具</h1>
<span class="ml-4 text-sm bg-dark-lighter px-2 py-1 rounded-full">
<i class="fa fa-circle text-green-500 animate-pulse mr-1"></i>
服务运行中
</span>
</div>
<div class="flex items-center space-x-4">
<div class="hidden md:block text-sm">
<span class="text-gray-400">系统:</span>
<span id="system-info">Linux</span>
</div>
<div class="hidden md:block text-sm">
<span class="text-gray-400">模型目录:</span>
<span id="model-dir">/home/user/models</span>
</div>
<button id="settings-btn" class="p-2 rounded-full hover:bg-dark-lighter transition-colors">
<i class="fa fa-cog text-gray-400 hover:text-white"></i>
</button>
</div>
</div>
<!-- 内容区域 -->
<div class="content-area">
<!-- 下载模型选项卡 -->
<div id="download-tab" class="tab-content">
<div class="card">
<h2 class="text-xl font-bold mb-4 flex items-center">
<i class="fa fa-download mr-2 text-primary"></i>
下载模型
</h2>
<div class="mb-4">
<label for="model-ids" class="block text-sm font-medium text-gray-300 mb-1">
模型ID (多个ID用英文逗号分隔)
</label>
<textarea id="model-ids" rows="3" class="input-field w-full"
placeholder="例如: ZhipuAI/GLM-5, Qwen/Qwen3-Coder-Next"></textarea>
</div>
<div class="mb-4">
<label for="local-path" class="block text-sm font-medium text-gray-300 mb-1">
本地存放路径
</label>
<input type="text" id="local-path" class="input-field w-full"
value="/home/user/models" placeholder="输入模型存放路径">
</div>
<div class="flex justify-end">
<button id="download-btn" class="btn-primary flex items-center">
<i class="fa fa-download mr-2"></i>
开始下载
</button>
</div>
</div>
<!-- 下载进度区域 -->
<div id="download-progress" class="card hidden">
<h2 class="text-xl font-bold mb-4 flex items-center">
<i class="fa fa-spinner fa-spin mr-2 text-primary"></i>
下载进度
</h2>
<div id="progress-container" class="space-y-6">
<!-- 进度条将在这里动态生成 -->
</div>
</div>
</div>
<!-- 上传模型选项卡 -->
<div id="upload-tab" class="tab-content hidden">
<div class="card">
<h2 class="text-xl font-bold mb-4 flex items-center">
<i class="fa fa-upload mr-2 text-secondary"></i>
上传模型
</h2>
<div class="mb-4">
<div class="flex justify-between items-center mb-2">
<label class="block text-sm font-medium text-gray-300">
未上传模型列表
</label>
<button id="refresh-models-btn" class="text-sm text-primary hover:text-primary/80 flex items-center">
<i class="fa fa-refresh mr-1"></i>
刷新列表
</button>
</div>
<div id="models-list" class="space-y-2 max-h-96 overflow-y-auto p-2 bg-dark-lighter/30 rounded-lg">
<!-- 模型列表将在这里动态生成 -->
<div class="text-center text-gray-400 py-4">
点击"刷新列表"加载未上传模型
</div>
</div>
</div>
<div class="flex justify-between">
<button id="select-all-btn" class="btn-secondary flex items-center">
<i class="fa fa-check-square-o mr-2"></i>
全选
</button>
<button id="upload-btn" class="btn-primary flex items-center">
<i class="fa fa-upload mr-2"></i>
上传选中模型
</button>
</div>
</div>
<!-- 上传进度区域 -->
<div id="upload-progress" class="card hidden">
<h2 class="text-xl font-bold mb-4 flex items-center">
<i class="fa fa-spinner fa-spin mr-2 text-primary"></i>
上传进度
</h2>
<div id="upload-progress-container" class="space-y-6">
<!-- 上传进度条将在这里动态生成 -->
</div>
</div>
</div>
<!-- 删除模型选项卡 -->
<div id="delete-tab" class="tab-content hidden">
<div class="card">
<h2 class="text-xl font-bold mb-4 flex items-center">
<i class="fa fa-trash mr-2 text-danger"></i>
删除模型
</h2>
<div class="mb-4">
<div class="flex justify-between items-center mb-2">
<label class="block text-sm font-medium text-gray-300">
已下载模型列表
</label>
<button id="refresh-delete-models-btn" class="text-sm text-primary hover:text-primary/80 flex items-center">
<i class="fa fa-refresh mr-1"></i>
刷新列表
</button>
</div>
<div id="delete-models-list" class="space-y-2 max-h-96 overflow-y-auto p-2 bg-dark-lighter/30 rounded-lg">
<!-- 删除模型列表将在这里动态生成 -->
<div class="text-center text-gray-400 py-4">
点击"刷新列表"加载已下载模型
</div>
</div>
</div>
<div class="flex justify-between">
<button id="select-all-delete-btn" class="btn-secondary flex items-center">
<i class="fa fa-check-square-o mr-2"></i>
全选
</button>
<button id="delete-btn" class="btn-danger flex items-center">
<i class="fa fa-trash mr-2"></i>
删除选中模型
</button>
</div>
</div>
</div>
<!-- 模型列表选项卡 -->
<div id="list-tab" class="tab-content hidden">
<div class="card">
<h2 class="text-xl font-bold mb-4 flex items-center">
<i class="fa fa-list mr-2 text-primary"></i>
所有模型
</h2>
<div class="mb-4">
<div class="flex justify-between items-center mb-2">
<label class="block text-sm font-medium text-gray-300">
模型列表
</label>
<button id="refresh-all-models-btn" class="text-sm text-primary hover:text-primary/80 flex items-center">
<i class="fa fa-refresh mr-1"></i>
刷新列表
</button>
</div>
<div id="all-models-list" class="space-y-4 max-h-96 overflow-y-auto p-2 bg-dark-lighter/30 rounded-lg">
<!-- 所有模型列表将在这里动态生成 -->
<div class="text-center text-gray-400 py-4">
点击"刷新列表"加载所有模型
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 设置弹窗 -->
<div id="settings-modal" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 hidden">
<div class="glass-effect rounded-xl p-6 max-w-md w-full">
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-bold">设置</h3>
<button id="close-settings-btn" class="text-gray-400 hover:text-white">
<i class="fa fa-times"></i>
</button>
</div>
<div class="space-y-4">
<div>
<label for="default-model-path" class="block text-sm font-medium text-gray-300 mb-1">
默认模型路径
</label>
<input type="text" id="default-model-path" class="input-field w-full"
value="/home/user/models" placeholder="输入默认模型存放路径">
</div>
<div>
<label for="max-retry" class="block text-sm font-medium text-gray-300 mb-1">
最大重试次数
</label>
<input type="number" id="max-retry" class="input-field w-full"
value="10" min="1" max="20" placeholder="输入最大重试次数">
</div>
<div>
<label for="csghub-url" class="block text-sm font-medium text-gray-300 mb-1">
CsgHub API URL
</label>
<input type="text" id="csghub-url" class="input-field w-full"
value="http://10.17.27.227:4997" placeholder="输入CsgHub API URL">
</div>
<div>
<label for="csghub-token" class="block text-sm font-medium text-gray-300 mb-1">
CsgHub Token
</label>
<input type="password" id="csghub-token" class="input-field w-full"
value="f5dad38a9426410aa861155cd184f84a" placeholder="输入CsgHub Token">
</div>
</div>
<div class="mt-6 flex justify-end">
<button id="save-settings-btn" class="btn-primary">
保存设置
</button>
</div>
</div>
</div>
<!-- 删除确认弹窗 -->
<div id="delete-confirm-modal" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 hidden">
<div class="glass-effect rounded-xl p-6 max-w-md w-full">
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-bold text-danger">确认删除</h3>
<button id="close-delete-confirm-btn" class="text-gray-400 hover:text-white">
<i class="fa fa-times"></i>
</button>
</div>
<p class="mb-4">
您确定要删除选中的 <span id="delete-count" class="font-bold">0</span> 个模型吗?
此操作无法撤销。
</p>
<div class="flex justify-end space-x-3">
<button id="cancel-delete-btn" class="px-4 py-2 border border-gray-500 rounded-lg hover:bg-dark-lighter transition-colors">
取消
</button>
<button id="confirm-delete-btn" class="btn-danger">
确认删除
</button>
</div>
</div>
</div>
<!-- 通知弹窗 -->
<div id="notification" class="fixed top-4 right-4 glass-effect rounded-lg p-4 shadow-lg z-50 transform translate-x-full transition-transform duration-300 max-w-sm">
<div class="flex items-start">
<div id="notification-icon" class="flex-shrink-0 mt-0.5">
<i class="fa fa-check-circle text-green-500 text-xl"></i>
</div>
<div class="ml-3 w-0 flex-1">
<p id="notification-title" class="text-sm font-medium text-white">
操作成功
</p>
<p id="notification-message" class="mt-1 text-sm text-gray-300">
操作已成功完成。
</p>
</div>
<div class="ml-4 flex-shrink-0 flex">
<button id="close-notification-btn" class="inline-flex text-gray-400 hover:text-white">
<i class="fa fa-times"></i>
</button>
</div>
</div>
</div>
<script>
// 全局变量
let currentTab = 'download-tab';
let socket = null;
let downloadTasks = {};
let uploadTasks = {};
let selectedModels = [];
let selectedDeleteModels = [];
let downloadQueue = []; // 下载队列
let currentDownloadTask = null; // 当前正在下载的任务
let uploadQueue = []; // 上传队列
let currentUploadTask = null; // 当前正在上传的任务
let settings = {
defaultModelPath: '/data/DataStore/models/exp/models',
maxRetry: 10,
csghubUrl: 'http://10.17.27.227:4997',
csghubToken: 'f5dad38a9426410aa861155cd184f84a'
};
// DOM 元素
const sidebarIcons = document.querySelectorAll('.sidebar-icon');
const tabContents = document.querySelectorAll('.tab-content');
const downloadBtn = document.getElementById('download-btn');
const uploadBtn = document.getElementById('upload-btn');
const deleteBtn = document.getElementById('delete-btn');
const refreshModelsBtn = document.getElementById('refresh-models-btn');
const refreshDeleteModelsBtn = document.getElementById('refresh-delete-models-btn');
const refreshAllModelsBtn = document.getElementById('refresh-all-models-btn');
const selectAllBtn = document.getElementById('select-all-btn');
const selectAllDeleteBtn = document.getElementById('select-all-delete-btn');
const settingsBtn = document.getElementById('settings-btn');
const closeSettingsBtn = document.getElementById('close-settings-btn');
const saveSettingsBtn = document.getElementById('save-settings-btn');
const deleteConfirmBtn = document.getElementById('confirm-delete-btn');
const cancelDeleteBtn = document.getElementById('cancel-delete-btn');
const closeDeleteConfirmBtn = document.getElementById('close-delete-confirm-btn');
const closeNotificationBtn = document.getElementById('close-notification-btn');
const settingsModal = document.getElementById('settings-modal');
const deleteConfirmModal = document.getElementById('delete-confirm-modal');
const notification = document.getElementById('notification');
// 初始化
document.addEventListener('DOMContentLoaded', () => {
console.log('页面加载完成,初始化应用...');
// 加载设置
loadSettings();
// 加载下载任务和上传任务
loadDownloadTasks();
loadUploadTasks();
// 设置侧边栏图标点击事件
sidebarIcons.forEach((icon, index) => {
icon.addEventListener('click', () => {
console.log('点击侧边栏图标:', index);
const tabs = ['download-tab', 'upload-tab', 'delete-tab', 'list-tab'];
if (index < tabs.length) {
switchTab(tabs[index]);
}
});
});
// 设置按钮点击事件
downloadBtn.addEventListener('click', startDownload);
uploadBtn.addEventListener('click', startUpload);
deleteBtn.addEventListener('click', showDeleteConfirm);
refreshModelsBtn.addEventListener('click', loadModelsList);
refreshDeleteModelsBtn.addEventListener('click', loadDeleteModelsList);
refreshAllModelsBtn.addEventListener('click', loadAllModelsList);
selectAllBtn.addEventListener('click', toggleSelectAll);
selectAllDeleteBtn.addEventListener('click', toggleSelectAllDelete);
settingsBtn.addEventListener('click', showSettings);
closeSettingsBtn.addEventListener('click', hideSettings);
saveSettingsBtn.addEventListener('click', saveSettings);
deleteConfirmBtn.addEventListener('click', confirmDelete);
cancelDeleteBtn.addEventListener('click', hideDeleteConfirm);
closeDeleteConfirmBtn.addEventListener('click', hideDeleteConfirm);
closeNotificationBtn.addEventListener('click', hideNotification);
// 连接Socket.IO(暂时禁用,避免版本不兼容问题)
// connectSocketIO();
// 加载系统信息
loadSystemInfo();
console.log('事件监听器设置完成');
});
// 切换选项卡
function switchTab(tabId) {
// 隐藏所有选项卡
tabContents.forEach(tab => tab.classList.add('hidden'));
// 显示选中的选项卡
document.getElementById(tabId).classList.remove('hidden');
currentTab = tabId;
// 根据选项卡加载数据
if (tabId === 'upload-tab') {
loadModelsList();
} else if (tabId === 'delete-tab') {
loadDeleteModelsList();
} else if (tabId === 'list-tab') {
loadAllModelsList();
}
}
// 连接Socket.IO
function connectSocketIO() {
// Socket.IO已禁用,避免版本不兼容问题
console.log('Socket.IO已禁用,避免版本不兼容问题');
window.socket = null;
}
// 处理 WebSocket 消息
function handleWebSocketMessage(message) {
const { type, data } = message;
switch (type) {
case 'task_update':
handleTaskUpdate(data);
break;
case 'download_progress':
updateDownloadProgress(data);
break;
case 'download_complete':
handleDownloadComplete(data);
break;
case 'download_failed':
handleDownloadFailed(data);
break;
case 'upload_progress':
updateUploadProgress(data);
break;
case 'upload_complete':
handleUploadComplete(data);
break;
case 'upload_failed':
handleUploadFailed(data);
break;
case 'models_list':
updateModelsList(data);
break;
case 'delete_models_list':
updateDeleteModelsList(data);
break;
case 'all_models_list':
updateAllModelsList(data);
break;
case 'system_info':
updateSystemInfo(data);
break;
}
}
// 处理任务更新
function handleTaskUpdate(task) {
const taskId = task.task_id;
if (task.type === 'download') {
// 更新下载进度
const progressElement = document.getElementById(`progress_${taskId}`);
if (progressElement) {
const progressText = document.getElementById(`progress_text_${taskId}`);
const progressValue = document.getElementById(`progress_value_${taskId}`);
const progressDetail = document.getElementById(`progress_detail_${taskId}`);
const statusBadge = progressElement.querySelector('.status-badge');
if (progressText) progressText.textContent = `${Math.round(task.progress)}%`;
if (progressValue) progressValue.style.width = `${task.progress}%`;
if (progressDetail) progressDetail.textContent = task.message;
if (statusBadge) {
switch (task.status) {
case 'downloading':
statusBadge.className = 'status-badge status-downloading';
statusBadge.innerHTML = '<i class="fa fa-spinner fa-spin mr-1"></i>下载中...';
break;
case 'completed':
statusBadge.className = 'status-badge status-success';
statusBadge.innerHTML = '<i class="fa fa-check mr-1"></i>下载完成';
break;
case 'failed':
statusBadge.className = 'status-badge status-error';
statusBadge.innerHTML = '<i class="fa fa-times mr-1"></i>下载失败';
break;
case 'pending':
statusBadge.className = 'status-badge status-pending';
statusBadge.innerHTML = '<i class="fa fa-clock-o mr-1"></i>等待中...';
break;
}
}
}
// 保存任务状态到localStorage
saveDownloadTask(task);
} else if (task.type === 'upload') {
// 更新上传进度
const progressElement = document.getElementById(`upload_progress_${taskId}`);
if (progressElement) {
const progressText = document.getElementById(`upload_progress_text_${taskId}`);
const progressValue = document.getElementById(`upload_progress_value_${taskId}`);
const progressDetail = document.getElementById(`upload_progress_detail_${taskId}`);
const statusBadge = progressElement.querySelector('.status-badge');
if (progressText) progressText.textContent = `${Math.round(task.progress)}%`;
if (progressValue) progressValue.style.width = `${task.progress}%`;
if (progressDetail) progressDetail.textContent = task.message;
if (statusBadge) {
switch (task.status) {
case 'uploading':
statusBadge.className = 'status-badge status-uploading';
statusBadge.innerHTML = '<i class="fa fa-spinner fa-spin mr-1"></i>上传中...';
break;
case 'completed':
statusBadge.className = 'status-badge status-success';
statusBadge.innerHTML = '<i class="fa fa-check mr-1"></i>上传完成';
break;
case 'failed':
statusBadge.className = 'status-badge status-error';
statusBadge.innerHTML = '<i class="fa fa-times mr-1"></i>上传失败';
break;
case 'pending':
statusBadge.className = 'status-badge status-pending';
statusBadge.innerHTML = '<i class="fa fa-clock-o mr-1"></i>等待中...';
break;
}
}
}
}
}
// 保存下载任务到localStorage
function saveDownloadTask(task) {
try {
const savedTasks = JSON.parse(localStorage.getItem('downloadTasks') || '{}');
savedTasks[task.task_id] = {
task_id: task.task_id,
model_id: task.model_id,
local_path: task.local_path,
status: task.status,
progress: task.progress,
message: task.message,
retry_count: task.retry_count || 0,
start_time: task.start_time
};
localStorage.setItem('downloadTasks', JSON.stringify(savedTasks));
} catch (error) {
console.error('保存下载任务失败:', error);
}
}
// 从localStorage加载下载任务
function loadDownloadTasks() {
try {
const savedTasks = JSON.parse(localStorage.getItem('downloadTasks') || '{}');
const activeTasks = [];
// 清空进度容器
const progressContainer = document.getElementById('progress-container');
progressContainer.innerHTML = '';
// 用于存储需要验证的任务
const tasksToVerify = [];
// 恢复所有任务,不仅仅是活跃任务
Object.values(savedTasks).forEach(task => {
// 无论状态如何,都显示任务
tasksToVerify.push(task);
});
// 如果有任务需要验证,先向后端确认任务是否还存在
if (tasksToVerify.length > 0) {
// 先显示进度区域
document.getElementById('download-progress').classList.remove('hidden');
let verifiedCount = 0;
tasksToVerify.forEach(task => {
fetch(`/api/task/${task.task_id}`, {
method: 'GET'
})
.then(response => {
if (response.ok) {
return response.json();
}
return null;
})
.then(taskData => {
verifiedCount++;
// 无论任务是否在后端存在,都显示前端存储的任务
activeTasks.push(task);
// 确定任务状态
let taskStatus = task.status;
let taskProgress = task.progress || 0;
let taskMessage = task.message || '恢复任务...';
// 如果后端有任务数据,使用后端数据
if (taskData) {
taskStatus = taskData.status;
taskProgress = taskData.progress || 0;
taskMessage = taskData.message || taskMessage;
}
// 创建进度条元素
const progressElement = document.createElement('div');
progressElement.id = `progress_${task.task_id}`;
progressElement.className = 'model-item';
// 根据任务状态生成不同的HTML
let statusBadge = '';
let buttonsHTML = '';
switch (taskStatus) {
case 'downloading':
statusBadge = '<span class="status-badge status-downloading"><i class="fa fa-spinner fa-spin mr-1"></i>下载中...</span>';
buttonsHTML = `
<button class="priority-btn bg-blue-900/30 hover:bg-blue-900/50 text-blue-300 text-xs px-2 py-1 rounded border border-blue-800/50"
onclick="setPriority('${task.task_id}', '${task.model_id}')">
<i class="fa fa-arrow-up mr-1"></i>优先
</button>
<button class="pause-btn bg-yellow-900/30 hover:bg-yellow-900/50 text-yellow-300 text-xs px-2 py-1 rounded border border-yellow-800/50"
onclick="pauseDownload('${task.task_id}', '${task.model_id}')">
<i class="fa fa-pause mr-1"></i>暂停
</button>
<button class="delete-btn bg-red-900/30 hover:bg-red-900/50 text-red-300 text-xs px-2 py-1 rounded border border-red-800/50"
onclick="deleteDownloadTask('${task.task_id}', '${task.model_id}')">
<i class="fa fa-trash mr-1"></i>删除
</button>
`;
break;
case 'pending':
statusBadge = '<span class="status-badge status-pending"><i class="fa fa-clock-o mr-1"></i>等待中...</span>';
buttonsHTML = `
<button class="priority-btn bg-blue-900/30 hover:bg-blue-900/50 text-blue-300 text-xs px-2 py-1 rounded border border-blue-800/50"
onclick="setPriority('${task.task_id}', '${task.model_id}')">
<i class="fa fa-arrow-up mr-1"></i>优先
</button>
<button class="pause-btn bg-yellow-900/30 hover:bg-yellow-900/50 text-yellow-300 text-xs px-2 py-1 rounded border border-yellow-800/50"
onclick="pauseDownload('${task.task_id}', '${task.model_id}')">
<i class="fa fa-pause mr-1"></i>暂停
</button>
<button class="delete-btn bg-red-900/30 hover:bg-red-900/50 text-red-300 text-xs px-2 py-1 rounded border border-red-800/50"
onclick="deleteDownloadTask('${task.task_id}', '${task.model_id}')">
<i class="fa fa-trash mr-1"></i>删除
</button>
`;
break;
case 'paused':
statusBadge = '<span class="status-badge bg-gray-700/50 text-gray-300 border border-gray-600"><i class="fa fa-pause mr-1"></i>已暂停</span>';
buttonsHTML = `
<button class="priority-btn bg-blue-900/30 hover:bg-blue-900/50 text-blue-300 text-xs px-2 py-1 rounded border border-blue-800/50"
onclick="setPriority('${task.task_id}', '${task.model_id}')">
<i class="fa fa-arrow-up mr-1"></i>优先
</button>
<button class="pause-btn bg-green-900/30 hover:bg-green-900/50 text-green-300 text-xs px-2 py-1 rounded border border-green-800/50"
onclick="resumeDownload('${task.task_id}', '${task.model_id}')">
<i class="fa fa-play mr-1"></i>继续
</button>
<button class="delete-btn bg-red-900/30 hover:bg-red-900/50 text-red-300 text-xs px-2 py-1 rounded border border-red-800/50"
onclick="deleteDownloadTask('${task.task_id}', '${task.model_id}')">
<i class="fa fa-trash mr-1"></i>删除
</button>
`;
break;
case 'downloaded':
statusBadge = '<span class="status-badge status-downloaded"><i class="fa fa-check mr-1"></i>下载完成</span>';
buttonsHTML = `
<button class="delete-btn bg-red-900/30 hover:bg-red-900/50 text-red-300 text-xs px-2 py-1 rounded border border-red-800/50"
onclick="deleteDownloadTask('${task.task_id}', '${task.model_id}')">
<i class="fa fa-trash mr-1"></i>删除
</button>
`;
break;
case 'failed':
statusBadge = '<span class="status-badge bg-red-900/50 text-red-300 border border-red-700"><i class="fa fa-times mr-1"></i>下载失败</span>';
buttonsHTML = `
<button class="delete-btn bg-red-900/30 hover:bg-red-900/50 text-red-300 text-xs px-2 py-1 rounded border border-red-800/50"
onclick="deleteDownloadTask('${task.task_id}', '${task.model_id}')">
<i class="fa fa-trash mr-1"></i>删除
</button>
`;
break;
case 'cancelled':
statusBadge = '<span class="status-badge bg-gray-700/50 text-gray-300 border border-gray-600"><i class="fa fa-ban mr-1"></i>已取消</span>';
buttonsHTML = `
<button class="delete-btn bg-red-900/30 hover:bg-red-900/50 text-red-300 text-xs px-2 py-1 rounded border border-red-800/50"
onclick="deleteDownloadTask('${task.task_id}', '${task.model_id}')">
<i class="fa fa-trash mr-1"></i>删除
</button>
`;
break;
default:
statusBadge = '<span class="status-badge status-pending"><i class="fa fa-clock-o mr-1"></i>等待中...</span>';
buttonsHTML = `
<button class="priority-btn bg-blue-900/30 hover:bg-blue-900/50 text-blue-300 text-xs px-2 py-1 rounded border border-blue-800/50"
onclick="setPriority('${task.task_id}', '${task.model_id}')">
<i class="fa fa-arrow-up mr-1"></i>优先
</button>
<button class="pause-btn bg-yellow-900/30 hover:bg-yellow-900/50 text-yellow-300 text-xs px-2 py-1 rounded border border-yellow-800/50"
onclick="pauseDownload('${task.task_id}', '${task.model_id}')">
<i class="fa fa-pause mr-1"></i>暂停
</button>
<button class="delete-btn bg-red-900/30 hover:bg-red-900/50 text-red-300 text-xs px-2 py-1 rounded border border-red-800/50"
onclick="deleteDownloadTask('${task.task_id}', '${task.model_id}')">
<i class="fa fa-trash mr-1"></i>删除
</button>
`;
}
// 构建完整的HTML
progressElement.innerHTML = `
<div class="flex justify-between items-center mb-2">
<div>
<h3 class="font-medium">${task.model_id}</h3>
${statusBadge}
</div>
<div class="flex items-center space-x-2">
<span class="text-sm text-gray-400" id="progress_text_${task.task_id}">${Math.round(taskProgress)}%</span>
${buttonsHTML}
</div>
</div>
<div class="progress-bar">
<div class="progress-value" id="progress_value_${task.task_id}" style="width: ${taskProgress}%"></div>
</div>
<div class="mt-2 flex justify-between items-center">
<div class="text-xs text-gray-400" id="progress_detail_${task.task_id}">
${taskMessage}
</div>
<div class="flex items-center">
<input type="checkbox" id="auto_upload_${task.task_id}" class="mr-1 h-3 w-3 rounded border-gray-500 text-primary focus:ring-primary"
${task.autoUpload ? 'checked' : ''} onchange="toggleAutoUpload('${task.task_id}', this.checked)">
<label for="auto_upload_${task.task_id}" class="text-xs text-gray-400 cursor-pointer">自动上传</label>
</div>
</div>
`;
progressContainer.appendChild(progressElement);
// 订阅任务更新
if (window.socket && window.socket.connected) {
window.socket.emit('subscribe_task', { task_id: task.task_id });
}
// 恢复任务到全局变量
downloadTasks[task.task_id] = task;
// 如果任务状态是 pending,添加到下载队列
if (task.status === 'pending') {
downloadQueue.push(task.task_id);
}
// 所有验证完成后更新 localStorage 并处理队列
if (verifiedCount === tasksToVerify.length) {
localStorage.setItem('downloadTasks', JSON.stringify(savedTasks));
console.log('已恢复', activeTasks.length, '个下载任务');
// 如果没有活跃任务,隐藏进度区域
if (activeTasks.length === 0) {
document.getElementById('download-progress').classList.add('hidden');
} else {
// 开始处理下载队列
processDownloadQueue();
}
}
})
.catch(error => {
verifiedCount++;
console.error('验证任务失败:', error);
// 即使验证失败,也显示任务
activeTasks.push(task);
// 创建进度条元素
const progressElement = document.createElement('div');
progressElement.id = `progress_${task.task_id}`;
progressElement.className = 'model-item';
progressElement.innerHTML = `
<div class="flex justify-between items-center mb-2">
<div>
<h3 class="font-medium">${task.model_id}</h3>
<span class="status-badge bg-gray-700/50 text-gray-300 border border-gray-600">
<i class="fa fa-exclamation mr-1"></i>状态未知
</span>
</div>
<div class="flex items-center space-x-2">
<span class="text-sm text-gray-400" id="progress_text_${task.task_id}">${Math.round(task.progress || 0)}%</span>
<button class="delete-btn bg-red-900/30 hover:bg-red-900/50 text-red-300 text-xs px-2 py-1 rounded border border-red-800/50"
onclick="deleteDownloadTask('${task.task_id}', '${task.model_id}')">
<i class="fa fa-trash mr-1"></i>删除
</button>
</div>
</div>
<div class="progress-bar">
<div class="progress-value" id="progress_value_${task.task_id}" style="width: ${task.progress || 0}%"></div>
</div>
<div class="mt-2 flex justify-between items-center">
<div class="text-xs text-gray-400" id="progress_detail_${task.task_id}">
任务状态未知
</div>
<div class="flex items-center">
<input type="checkbox" id="auto_upload_${task.task_id}" class="mr-1 h-3 w-3 rounded border-gray-500 text-primary focus:ring-primary"
${task.autoUpload ? 'checked' : ''} onchange="toggleAutoUpload('${task.task_id}', this.checked)">
<label for="auto_upload_${task.task_id}" class="text-xs text-gray-400 cursor-pointer">自动上传</label>
</div>
</div>
`;
progressContainer.appendChild(progressElement);
// 恢复任务到全局变量
downloadTasks[task.task_id] = task;
if (verifiedCount === tasksToVerify.length) {
localStorage.setItem('downloadTasks', JSON.stringify(savedTasks));
console.log('已恢复', activeTasks.length, '个下载任务');
if (activeTasks.length === 0) {
document.getElementById('download-progress').classList.add('hidden');
}
}
});
});
} else {
// 没有任务,隐藏进度区域
document.getElementById('download-progress').classList.add('hidden');
}
} catch (error) {
console.error('加载下载任务失败:', error);
document.getElementById('download-progress').classList.add('hidden');
}
}
// 开始下载
function startDownload() {
const modelIdsInput = document.getElementById('model-ids').value.trim();
const localPath = document.getElementById('local-path').value.trim();
if (!modelIdsInput) {
showNotification('错误', '请输入模型ID', 'error');
return;
}
if (!localPath) {
showNotification('错误', '请输入本地存放路径', 'error');
return;
}
// 分割模型ID
const modelIds = modelIdsInput.split(',').map(id => id.trim()).filter(id => id);
if (modelIds.length === 0) {
showNotification('错误', '请输入有效的模型ID', 'error');
return;
}
// 显示下载进度区域
document.getElementById('download-progress').classList.remove('hidden');
// 为每个模型创建进度条并添加到队列
modelIds.forEach(modelId => {
const taskId = `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
// 保存任务信息 - 确保包含 task_id 字段
const task = {
task_id: taskId,
model_id: modelId,
local_path: localPath,
retryCount: 0,
status: 'pending',
progress: 0,
autoUpload: false
};
downloadTasks[taskId] = task;
downloadQueue.push(taskId);
// 立即保存到 localStorage
const savedTasks = JSON.parse(localStorage.getItem('downloadTasks') || '{}');
savedTasks[taskId] = task;
localStorage.setItem('downloadTasks', JSON.stringify(savedTasks));
// 创建进度条元素
const progressElement = document.createElement('div');
progressElement.id = `progress_${taskId}`;
progressElement.className = 'model-item';
progressElement.innerHTML = `
<div class="flex justify-between items-center mb-2">
<div>
<h3 class="font-medium">${modelId}</h3>
<span class="status-badge status-pending">
<i class="fa fa-clock-o mr-1"></i>
等待中...
</span>
</div>
<div class="flex items-center space-x-2">
<span class="text-sm text-gray-400" id="progress_text_${taskId}">0%</span>
<button class="priority-btn bg-blue-900/30 hover:bg-blue-900/50 text-blue-300 text-xs px-2 py-1 rounded border border-blue-800/50"
onclick="setPriority('${taskId}', '${modelId}')">
<i class="fa fa-arrow-up mr-1"></i>优先
</button>
<button class="pause-btn bg-yellow-900/30 hover:bg-yellow-900/50 text-yellow-300 text-xs px-2 py-1 rounded border border-yellow-800/50"
onclick="pauseDownload('${taskId}', '${modelId}')">
<i class="fa fa-pause mr-1"></i>暂停
</button>
<button class="delete-btn bg-red-900/30 hover:bg-red-900/50 text-red-300 text-xs px-2 py-1 rounded border border-red-800/50"
onclick="deleteDownloadTask('${taskId}', '${modelId}')">
<i class="fa fa-trash mr-1"></i>删除
</button>
</div>
</div>
<div class="progress-bar">
<div class="progress-value" id="progress_value_${taskId}" style="width: 0%"></div>
</div>
<div class="mt-2 flex justify-between items-center">
<div class="text-xs text-gray-400" id="progress_detail_${taskId}">
准备下载...
</div>
<div class="flex items-center">
<input type="checkbox" id="auto_upload_${taskId}" class="mr-1 h-3 w-3 rounded border-gray-500 text-primary focus:ring-primary"
onchange="toggleAutoUpload('${taskId}', this.checked)">
<label for="auto_upload_${taskId}" class="text-xs text-gray-400 cursor-pointer">自动上传</label>
</div>
</div>
`;
document.getElementById('progress-container').appendChild(progressElement);
});
// 开始处理下载队列
processDownloadQueue();
console.log('开始下载模型:', modelIds, '到路径:', localPath);
}
// 处理下载队列
function processDownloadQueue() {
if (currentDownloadTask || downloadQueue.length === 0) {
return;
}
// 获取队列中的第一个任务
const taskId = downloadQueue.shift();
const task = downloadTasks[taskId];
if (!task || task.status === 'cancelled' || task.status === 'failed') {
// 跳过已取消或失败的任务
processDownloadQueue();
return;
}
// 更新任务状态为下载中
task.status = 'downloading';
// 更新UI
const progressElement = document.getElementById(`progress_${taskId}`);
if (progressElement) {
const statusBadge = progressElement.querySelector('.status-badge');
if (statusBadge) {
statusBadge.className = 'status-badge status-downloading';
statusBadge.innerHTML = '<i class="fa fa-spinner fa-spin mr-1"></i>下载中...';
}
}
// 设置当前下载任务
currentDownloadTask = taskId;
// 使用 AbortController 支持取消请求
const controller = new AbortController();
const signal = controller.signal;
task.abortController = controller;
// 设置10秒超时
const timeoutId = setTimeout(() => {
controller.abort();
showNotification('错误', '连接服务器超时,请检查后端服务是否运行', 'error');
task.status = 'failed';
currentDownloadTask = null;
const progressElement = document.getElementById(`progress_${taskId}`);
if (progressElement) {
const statusBadge = progressElement.querySelector('.status-badge');
if (statusBadge) {
statusBadge.className = 'status-badge bg-red-900/50 text-red-300 border border-red-700';
statusBadge.innerHTML = '<i class="fa fa-times mr-1"></i>连接超时';
}
}
// 继续处理队列
processDownloadQueue();
}, 10000);
// 发送请求到后端开始下载
fetch('/api/download', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
model_id: task.model_id,
local_path: task.local_path,
task_id: task.task_id
}),
signal: signal
})
.then(response => {
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error('Network response was not ok: ' + response.status);
}
return response.json();
})
.then(data => {
console.log('下载请求已发送:', data);
if (data.status === 'error') {
throw new Error(data.message || '下载请求失败');
}
})
.catch(error => {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
console.log('下载请求已被取消');
return;
}
console.error('Error starting download:', error);
showNotification('错误', `启动下载失败: ${error.message}`, 'error');
// 更新任务状态为失败
task.status = 'failed';
currentDownloadTask = null;
const progressElement = document.getElementById(`progress_${taskId}`);
if (progressElement) {
const statusBadge = progressElement.querySelector('.status-badge');
const progressDetail = document.getElementById(`progress_detail_${taskId}`);
if (statusBadge) {
statusBadge.className = 'status-badge bg-red-900/50 text-red-300 border border-red-700';
statusBadge.innerHTML = '<i class="fa fa-times mr-1"></i>下载失败';
}
if (progressDetail) progressDetail.textContent = `下载失败: ${error.message}`;
}
// 继续处理队列
processDownloadQueue();
});
}
// 设置下载优先级
function setPriority(taskId, modelId) {
// 从队列中移除任务
downloadQueue = downloadQueue.filter(id => id !== taskId);
// 将任务添加到队列开头
downloadQueue.unshift(taskId);
// 如果当前没有下载任务,立即开始处理
if (!currentDownloadTask) {
processDownloadQueue();
}
// 更新UI,将任务移到列表顶部
const progressElement = document.getElementById(`progress_${taskId}`);
if (progressElement) {
const progressContainer = document.getElementById('progress-container');
progressContainer.insertBefore(progressElement, progressContainer.firstChild);
}
showNotification('提示', `模型 ${modelId} 已设置为最高优先级`, 'info');
}
// 暂停下载
function pauseDownload(taskId, modelId) {
const task = downloadTasks[taskId];
if (!task) return;
if (task.status === 'downloading' && currentDownloadTask === taskId) {
// 取消当前正在下载的任务
if (task.abortController) {
task.abortController.abort();
}
// 发送取消请求到后端
fetch(`/api/download/cancel/${taskId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.catch(error => console.error('取消下载失败:', error));
// 更新任务状态
task.status = 'paused';
currentDownloadTask = null;
// 更新UI
const progressElement = document.getElementById(`progress_${taskId}`);
if (progressElement) {
const statusBadge = progressElement.querySelector('.status-badge');
const progressDetail = document.getElementById(`progress_detail_${taskId}`);
const pauseBtn = progressElement.querySelector('.pause-btn');
if (statusBadge) {
statusBadge.className = 'status-badge bg-gray-700/50 text-gray-300 border border-gray-600';
statusBadge.innerHTML = '<i class="fa fa-pause mr-1"></i>已暂停';
}
if (progressDetail) progressDetail.textContent = '下载已暂停';
if (pauseBtn) {
pauseBtn.innerHTML = '<i class="fa fa-play mr-1"></i>继续';
pauseBtn.onclick = () => resumeDownload(taskId, modelId);
}
}
// 继续处理队列中的下一个任务
processDownloadQueue();
} else if (task.status === 'paused') {
// 任务已暂停,将其添加到队列
downloadQueue.push(taskId);
task.status = 'pending';
// 更新UI
const progressElement = document.getElementById(`progress_${taskId}`);
if (progressElement) {
const statusBadge = progressElement.querySelector('.status-badge');
const progressDetail = document.getElementById(`progress_detail_${taskId}`);
const pauseBtn = progressElement.querySelector('.pause-btn');
if (statusBadge) {
statusBadge.className = 'status-badge status-pending';
statusBadge.innerHTML = '<i class="fa fa-clock-o mr-1"></i>等待中...';
}
if (progressDetail) progressDetail.textContent = '准备下载...';
if (pauseBtn) {
pauseBtn.innerHTML = '<i class="fa fa-pause mr-1"></i>暂停';
pauseBtn.onclick = () => pauseDownload(taskId, modelId);
}
}
// 如果当前没有下载任务,开始处理
if (!currentDownloadTask) {
processDownloadQueue();
}
}
showNotification('提示', `模型 ${modelId} 下载已${task.status === 'paused' ? '暂停' : '继续'}`, 'info');
}
// 恢复下载
function resumeDownload(taskId, modelId) {
const task = downloadTasks[taskId];
if (!task) return;
// 将任务添加到队列
downloadQueue.push(taskId);
task.status = 'pending';
// 更新UI
const progressElement = document.getElementById(`progress_${taskId}`);
if (progressElement) {
const statusBadge = progressElement.querySelector('.status-badge');
const progressDetail = document.getElementById(`progress_detail_${taskId}`);
const pauseBtn = progressElement.querySelector('.pause-btn');
if (statusBadge) {
statusBadge.className = 'status-badge status-pending';
statusBadge.innerHTML = '<i class="fa fa-clock-o mr-1"></i>等待中...';
}
if (progressDetail) progressDetail.textContent = '准备下载...';
if (pauseBtn) {
pauseBtn.innerHTML = '<i class="fa fa-pause mr-1"></i>暂停';
pauseBtn.onclick = () => pauseDownload(taskId, modelId);
}
}
// 如果当前没有下载任务,开始处理
if (!currentDownloadTask) {
processDownloadQueue();
}
showNotification('提示', `模型 ${modelId} 下载已继续`, 'info');
}
// 切换自动上传选项
function toggleAutoUpload(taskId, checked) {
const task = downloadTasks[taskId];
if (task) {
task.autoUpload = checked;
// 保存到localStorage
const savedTasks = JSON.parse(localStorage.getItem('downloadTasks') || '{}');
if (savedTasks[taskId]) {
savedTasks[taskId].autoUpload = checked;
localStorage.setItem('downloadTasks', JSON.stringify(savedTasks));
}
}
}
// 删除下载任务
function deleteDownloadTask(taskId, modelId) {
if (confirm(`确定要删除下载任务 ${modelId} 吗?`)) {
// 从队列中移除任务
downloadQueue = downloadQueue.filter(id => id !== taskId);
// 如果是当前正在下载的任务,取消下载
if (currentDownloadTask === taskId) {
const task = downloadTasks[taskId];
if (task && task.abortController) {
task.abortController.abort();
}
// 发送取消请求到后端
fetch(`/api/download/cancel/${taskId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.catch(error => console.error('取消下载失败:', error));
currentDownloadTask = null;
// 继续处理队列中的下一个任务
processDownloadQueue();
}
// 从任务列表中移除
delete downloadTasks[taskId];
// 从localStorage中移除
const savedTasks = JSON.parse(localStorage.getItem('downloadTasks') || '{}');
delete savedTasks[taskId];
localStorage.setItem('downloadTasks', JSON.stringify(savedTasks));
// 从UI中移除
const progressElement = document.getElementById(`progress_${taskId}`);
if (progressElement) {
progressElement.remove();
}
// 如果没有任务了,隐藏进度区域
if (Object.keys(downloadTasks).length === 0) {
document.getElementById('download-progress').classList.add('hidden');
}
showNotification('提示', `下载任务 ${modelId} 已删除`, 'info');
}
}
// 模拟下载进度
function simulateDownloadProgress(taskId) {
const task = downloadTasks[taskId];
if (!task) return;
// 模拟进度增加
let progress = task.progress;
const interval = setInterval(() => {
// 随机增加进度
const increment = Math.random() * 10;
progress = Math.min(progress + increment, 100);
// 更新任务进度
task.progress = progress;
// 更新UI
const progressText = document.getElementById(`progress_text_${taskId}`);
const progressValue = document.getElementById(`progress_value_${taskId}`);
const progressDetail = document.getElementById(`progress_detail_${taskId}`);
if (progressText) progressText.textContent = `${Math.round(progress)}%`;
if (progressValue) progressValue.style.width = `${progress}%`;
// 随机更新详细信息
const details = [
`下载中: ${Math.round(progress)}%`,
`正在获取模型文件...`,
`已下载 ${Math.round(progress * 100 / 100)}MB / 100MB`,
`正在验证文件完整性...`
];
if (progressDetail) {
progressDetail.textContent = details[Math.floor(Math.random() * details.length)];
}
// 检查是否完成
if (progress >= 100) {
clearInterval(interval);
// 模拟下载完成
setTimeout(() => {
handleDownloadComplete({
taskId,
modelId: task.modelId,
localPath: task.localPath
});
}, 500);
}
}, 1000);
// 保存定时器ID
task.interval = interval;
}
// 更新下载进度
function updateDownloadProgress(data) {
const { taskId, progress, detail } = data;
const task = downloadTasks[taskId];
if (task) {
task.progress = progress;
// 更新UI
const progressText = document.getElementById(`progress_text_${taskId}`);
const progressValue = document.getElementById(`progress_value_${taskId}`);
const progressDetail = document.getElementById(`progress_detail_${taskId}`);
if (progressText) progressText.textContent = `${Math.round(progress)}%`;
if (progressValue) progressValue.style.width = `${progress}%`;
if (progressDetail && detail) progressDetail.textContent = detail;
}
}
// 处理下载完成
function handleDownloadComplete(data) {
const { taskId, modelId, localPath } = data;
const task = downloadTasks[taskId];
if (task) {
// 清除定时器
if (task.interval) {
clearInterval(task.interval);
}
// 更新任务状态
task.status = 'downloaded';
task.progress = 100;
// 更新UI
const progressElement = document.getElementById(`progress_${taskId}`);
if (progressElement) {
const statusBadge = progressElement.querySelector('.status-badge');
const progressText = document.getElementById(`progress_text_${taskId}`);
const progressValue = document.getElementById(`progress_value_${taskId}`);
const progressDetail = document.getElementById(`progress_detail_${taskId}`);
if (statusBadge) {
statusBadge.className = 'status-badge status-downloaded';
statusBadge.innerHTML = '<i class="fa fa-check mr-1"></i>下载完成';
}
if (progressText) progressText.textContent = '100%';
if (progressValue) {
progressValue.style.width = '100%';
progressValue.classList.add('bg-success');
}
if (progressDetail) progressDetail.textContent = `模型已保存到: ${localPath}/${modelId}`;
}
// 显示通知
showNotification('成功', `模型 ${modelId} 下载完成`, 'success');
// 检查是否需要自动上传
if (task.autoUpload) {
showNotification('提示', `开始自动上传模型 ${modelId}`, 'info');
// 调用上传单个模型的函数
uploadSingleModel(modelId);
}
// 清空当前下载任务
currentDownloadTask = null;
// 继续处理队列中的下一个任务
processDownloadQueue();
// 如果是最后一个任务,重新加载模型列表
if (Object.values(downloadTasks).every(t => t.status === 'downloaded' || t.status === 'failed' || t.status === 'cancelled')) {
setTimeout(() => {
if (currentTab === 'upload-tab') {
loadModelsList();
} else if (currentTab === 'delete-tab') {
loadDeleteModelsList();
} else if (currentTab === 'list-tab') {
loadAllModelsList();
}
}, 1000);
}
}
}
// 取消下载
function cancelDownload(taskId, modelId) {
if (confirm(`确定要取消下载模型 ${modelId} 吗?`)) {
console.log(`取消下载任务: ${taskId}, 模型ID: ${modelId}`);
// 先尝试取消前端的 fetch 请求
const task = downloadTasks[taskId];
if (task && task.abortController) {
task.abortController.abort();
}
// 使用 AbortController 支持超时取消
const controller = new AbortController();
const signal = controller.signal;
// 设置5秒超时
const timeoutId = setTimeout(() => {
controller.abort();
}, 5000);
// 发送取消请求到后端
fetch(`/api/download/cancel/${taskId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
signal: signal
})
.then(response => {
clearTimeout(timeoutId);
// 即使任务不存在,也清理前端界面
return response.json().catch(() => ({ success: false, message: '任务不存在' }));
})
.then(data => {
console.log('取消下载响应:', data);
// 无论后端返回什么,都清理前端界面
const task = downloadTasks[taskId];
if (task) {
// 清除定时器
if (task.interval) {
clearInterval(task.interval);
}
task.status = 'cancelled';
// 更新UI
const progressElement = document.getElementById(`progress_${taskId}`);
if (progressElement) {
const statusBadge = progressElement.querySelector('.status-badge');
const progressDetail = document.getElementById(`progress_detail_${taskId}`);
const cancelBtn = progressElement.querySelector('.cancel-btn');
if (statusBadge) {
statusBadge.className = 'status-badge bg-gray-700/50 text-gray-300 border border-gray-600';
statusBadge.innerHTML = '<i class="fa fa-ban mr-1"></i>已取消';
}
if (progressDetail) progressDetail.textContent = '下载已取消';
// 隐藏取消按钮
if (cancelBtn) {
cancelBtn.disabled = true;
cancelBtn.textContent = '已取消';
cancelBtn.classList.remove('hover:bg-red-900/50');
cancelBtn.classList.add('bg-gray-800/50', 'text-gray-400', 'cursor-not-allowed');
}
}
// 显示通知
showNotification('提示', `模型 ${modelId} 下载已取消`, 'info');
// 从任务列表中移除
setTimeout(() => {
delete downloadTasks[taskId];
const progressElement = document.getElementById(`progress_${taskId}`);
if (progressElement) {
progressElement.remove();
}
// 如果没有活跃任务,隐藏进度区域
if (Object.keys(downloadTasks).length === 0) {
document.getElementById('download-progress').classList.add('hidden');
}
}, 2000);
}
})
.catch(error => {
clearTimeout(timeoutId);
// 即使请求失败,也清理前端界面
const task = downloadTasks[taskId];
if (task) {
if (task.interval) {
clearInterval(task.interval);
}
task.status = 'cancelled';
const progressElement = document.getElementById(`progress_${taskId}`);
if (progressElement) {
const statusBadge = progressElement.querySelector('.status-badge');
if (statusBadge) {
statusBadge.className = 'status-badge bg-gray-700/50 text-gray-300 border border-gray-600';
statusBadge.innerHTML = '<i class="fa fa-ban mr-1"></i>已取消';
}
}
setTimeout(() => {
delete downloadTasks[taskId];
const progressElement = document.getElementById(`progress_${taskId}`);
if (progressElement) {
progressElement.remove();
}
if (Object.keys(downloadTasks).length === 0) {
document.getElementById('download-progress').classList.add('hidden');
}
}, 2000);
}
if (error.name === 'AbortError') {
console.error('取消请求超时');
showNotification('错误', '取消请求超时,请重试', 'error');
} else {
console.error('取消下载失败:', error);
showNotification('提示', `模型 ${modelId} 下载已取消`, 'info');
}
});
}
}
// 处理下载失败
function handleDownloadFailed(data) {
const { taskId, modelId, error } = data;
const task = downloadTasks[taskId];
if (task) {
// 清除定时器
if (task.interval) {
clearInterval(task.interval);
}
// 增加重试次数
task.retryCount++;
// 检查是否超过最大重试次数
if (task.retryCount < settings.maxRetry) {
// 更新任务状态
task.status = 'retrying';
// 更新UI
const progressElement = document.getElementById(`progress_${taskId}`);
if (progressElement) {
const statusBadge = progressElement.querySelector('.status-badge');
const progressDetail = document.getElementById(`progress_detail_${taskId}`);
if (statusBadge) {
statusBadge.className = 'status-badge status-downloading';
statusBadge.innerHTML = `<i class="fa fa-refresh fa-spin mr-1"></i>重试中 (${task.retryCount}/${settings.maxRetry})`;
}
if (progressDetail) progressDetail.textContent = `下载失败: ${error}. 正在重试...`;
}
// 重新开始下载
setTimeout(() => {
fetch('/api/download', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
model_id: task.model_id,
local_path: task.localPath,
task_id: taskId
})
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
console.log('重试下载请求已发送:', data);
})
.catch(error => {
console.error('Error retrying download:', error);
});
}, 2000);
} else {
// 更新任务状态
task.status = 'failed';
// 更新UI
const progressElement = document.getElementById(`progress_${taskId}`);
if (progressElement) {
const statusBadge = progressElement.querySelector('.status-badge');
const progressValue = document.getElementById(`progress_value_${taskId}`);
const progressDetail = document.getElementById(`progress_detail_${taskId}`);
if (statusBadge) {
statusBadge.className = 'status-badge bg-red-900/50 text-red-300 border border-red-700';
statusBadge.innerHTML = '<i class="fa fa-times mr-1"></i>下载失败';
}
if (progressValue) {
progressValue.classList.add('bg-danger');
}
if (progressDetail) progressDetail.textContent = `下载失败: ${error}. 已重试 ${settings.maxRetry} 次`;
}
// 显示通知
showNotification('错误', `模型 ${modelId} 下载失败,已重试 ${settings.maxRetry} 次`, 'error');
}
}
}
// 加载未上传模型列表
function loadModelsList() {
const modelsList = document.getElementById('models-list');
// 显示加载状态
modelsList.innerHTML = `
<div class="text-center text-gray-400 py-4">
<i class="fa fa-spinner fa-spin mr-2"></i>
加载中...
</div>
`;
// 从后端获取未上传模型列表,传递当前配置的模型路径
const modelPath = settings.defaultModelPath;
// 使用AbortController设置超时
const controller = new AbortController();
const signal = controller.signal;
// 设置30秒超时
const timeoutId = setTimeout(() => {
controller.abort();
modelsList.innerHTML = `
<div class="text-center text-red-400 py-4">
<i class="fa fa-exclamation-triangle mr-2"></i>
加载超时: 请检查后端服务是否运行
</div>
`;
}, 30000);
fetch(`/api/models?status=downloaded&path=${encodeURIComponent(modelPath)}`, {
signal: signal
})
.then(response => {
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`Network response was not ok: ${response.status}`);
}
return response.json();
})
.then(data => {
clearTimeout(timeoutId);
const models = data.models || [];
// 更新模型列表
updateModelsList(models);
})
.catch(error => {
clearTimeout(timeoutId);
console.error('Error loading models list:', error);
let errorMessage = error.message;
if (error.name === 'AbortError') {
errorMessage = '请求超时';
}
modelsList.innerHTML = `
<div class="text-center text-red-400 py-4">
<i class="fa fa-exclamation-triangle mr-2"></i>
加载失败: ${errorMessage}
</div>
`;
});
}
// 更新未上传模型列表
function updateModelsList(models) {
const modelsList = document.getElementById('models-list');
if (models.length === 0) {
modelsList.innerHTML = `
<div class="text-center text-gray-400 py-4">
没有未上传的模型
</div>
`;
return;
}
// 清空列表
modelsList.innerHTML = '';
// 添加模型项
models.forEach(model => {
const modelElement = document.createElement('div');
modelElement.className = 'model-item flex items-center justify-between';
modelElement.innerHTML = `
<div class="flex items-center">
<input type="checkbox" id="model_${model.id}" class="model-checkbox mr-3 h-4 w-4 rounded border-gray-500 text-primary focus:ring-primary" value="${model.id}">
<div>
<label for="model_${model.id}" class="font-medium cursor-pointer">${model.id}</label>
<div class="text-xs text-gray-400">${model.path} (${model.size})</div>
</div>
</div>
<button class="upload-single-btn text-primary hover:text-primary/80" data-model-id="${model.id}">
<i class="fa fa-upload"></i>
</button>
`;
modelsList.appendChild(modelElement);
// 添加复选框事件
const checkbox = modelElement.querySelector('.model-checkbox');
checkbox.addEventListener('change', () => {
if (checkbox.checked) {
selectedModels.push(model.id);
} else {
selectedModels = selectedModels.filter(id => id !== model.id);
}
});
// 添加单个上传按钮事件
const uploadSingleBtn = modelElement.querySelector('.upload-single-btn');
uploadSingleBtn.addEventListener('click', () => {
uploadSingleModel(model.id);
});
});
}
// 加载删除模型列表
function loadDeleteModelsList() {
const deleteModelsList = document.getElementById('delete-models-list');
// 显示加载状态
deleteModelsList.innerHTML = `
<div class="text-center text-gray-400 py-4">
<i class="fa fa-spinner fa-spin mr-2"></i>
加载中...
</div>
`;
// 从后端获取所有模型列表,传递当前配置的模型路径
const modelPath = settings.defaultModelPath;
// 使用AbortController设置超时
const controller = new AbortController();
const signal = controller.signal;
// 设置30秒超时
const timeoutId = setTimeout(() => {
controller.abort();
deleteModelsList.innerHTML = `
<div class="text-center text-red-400 py-4">
<i class="fa fa-exclamation-triangle mr-2"></i>
加载超时: 请检查后端服务是否运行
</div>
`;
}, 30000);
fetch(`/api/models?all=true&path=${encodeURIComponent(modelPath)}`, {
signal: signal
})
.then(response => {
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`Network response was not ok: ${response.status}`);
}
return response.json();
})
.then(data => {
clearTimeout(timeoutId);
const models = data.models || [];
// 更新删除模型列表
updateDeleteModelsList(models);
})
.catch(error => {
clearTimeout(timeoutId);
console.error('Error loading delete models list:', error);
let errorMessage = error.message;
if (error.name === 'AbortError') {
errorMessage = '请求超时';
}
deleteModelsList.innerHTML = `
<div class="text-center text-red-400 py-4">
<i class="fa fa-exclamation-triangle mr-2"></i>
加载失败: ${errorMessage}
</div>
`;
});
}
// 更新删除模型列表
function updateDeleteModelsList(models) {
const deleteModelsList = document.getElementById('delete-models-list');
if (models.length === 0) {
deleteModelsList.innerHTML = `
<div class="text-center text-gray-400 py-4">
没有已下载的模型
</div>
`;
return;
}
// 清空列表
deleteModelsList.innerHTML = '';
// 添加模型项
models.forEach(model => {
const modelElement = document.createElement('div');
modelElement.className = 'model-item flex items-center justify-between';
// 确定状态标签
let statusBadge = '';
if (model.status === 'uploaded') {
statusBadge = '<span class="status-badge status-uploaded"><i class="fa fa-cloud-upload mr-1"></i>已上传</span>';
} else if (model.status === 'downloading') {
statusBadge = '<span class="status-badge status-downloading"><i class="fa fa-spinner fa-spin mr-1"></i>下载中</span>';
} else if (model.status === 'uploading') {
statusBadge = '<span class="status-badge status-uploading"><i class="fa fa-spinner fa-spin mr-1"></i>上传中</span>';
} else {
statusBadge = '<span class="status-badge status-downloaded"><i class="fa fa-check mr-1"></i>已下载</span>';
}
modelElement.innerHTML = `
<div class="flex items-center">
<input type="checkbox" id="delete_model_${model.id}" class="delete-model-checkbox mr-3 h-4 w-4 rounded border-gray-500 text-danger focus:ring-danger" value="${model.id}">
<div>
<label for="delete_model_${model.id}" class="font-medium cursor-pointer">${model.id}</label>
<div class="text-xs text-gray-400">${model.path} (${model.size})</div>
</div>
</div>
<div>
${statusBadge}
</div>
`;
deleteModelsList.appendChild(modelElement);
// 添加复选框事件
const checkbox = modelElement.querySelector('.delete-model-checkbox');
checkbox.addEventListener('change', () => {
if (checkbox.checked) {
selectedDeleteModels.push(model.id);
} else {
selectedDeleteModels = selectedDeleteModels.filter(id => id !== model.id);
}
});
});
}
// 检查进行中的任务
function checkInProgressTasks() {
console.log('检查进行中的任务...');
const modelPath = settings.defaultModelPath;
fetch(`/api/models?all=true&path=${encodeURIComponent(modelPath)}`)
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
const models = data.models || [];
console.log('获取到模型列表:', models);
// 检查是否有正在下载或上传的模型
const downloadingModels = models.filter(model => model.status === 'downloading' && model.progress !== undefined);
const uploadingModels = models.filter(model => model.status === 'uploading' && model.progress !== undefined);
console.log('正在下载的模型:', downloadingModels);
console.log('正在上传的模型:', uploadingModels);
// 如果有正在下载的模型,显示下载进度区域
if (downloadingModels.length > 0) {
const downloadProgressArea = document.getElementById('download-progress');
const progressContainer = document.getElementById('progress-container');
// 显示下载进度区域
downloadProgressArea.classList.remove('hidden');
// 为每个下载中的模型创建进度条
downloadingModels.forEach(model => {
// 使用固定的任务ID格式,基于模型ID
const taskId = `task_download_${model.id}`;
// 存储任务信息
downloadTasks[taskId] = {
task_id: taskId,
model_id: model.id,
local_path: model.path,
type: 'download',
status: 'downloading',
progress: model.progress || 0,
message: model.message || '正在下载...',
start_time: model.downloadTime || new Date().toISOString()
};
// 创建进度条元素
createDownloadProgressElement(taskId, model.id);
// 立即更新进度显示
updateDownloadProgress(taskId, model.progress || 0, model.message || '正在下载...');
// 如果Socket.IO连接已建立,订阅任务更新
if (window.socket) {
window.socket.emit('subscribe_task', { task_id: taskId });
}
// 主动查询任务状态
fetchTaskStatus(taskId, 'download');
});
}
// 如果有正在上传的模型,显示上传进度区域
if (uploadingModels.length > 0) {
const uploadProgressArea = document.getElementById('upload-progress');
const uploadProgressContainer = document.getElementById('upload-progress-container');
// 显示上传进度区域
uploadProgressArea.classList.remove('hidden');
// 为每个上传中的模型创建进度条
uploadingModels.forEach(model => {
// 使用固定的任务ID格式,基于模型ID
const taskId = `task_upload_${model.id}`;
// 存储任务信息
uploadTasks[taskId] = {
task_id: taskId,
model_id: model.id,
local_path: model.path,
type: 'upload',
status: 'uploading',
progress: model.progress || 0,
message: model.message || '正在上传...',
start_time: model.uploadTime || new Date().toISOString()
};
// 创建进度条元素
createUploadProgressElement(taskId, model.id);
// 立即更新进度显示
updateUploadProgress(taskId, model.progress || 0, model.message || '正在上传...');
// 如果Socket.IO连接已建立,订阅任务更新
if (window.socket) {
window.socket.emit('subscribe_task', { task_id: taskId });
}
// 主动查询任务状态
fetchTaskStatus(taskId, 'upload');
});
}
})
.catch(error => {
console.error('检查进行中任务失败:', error);
});
}
// 取消上传
function cancelUpload(taskId, modelId) {
if (confirm(`确定要取消上传模型 ${modelId} 吗?`)) {
console.log(`取消上传任务: ${taskId}, 模型ID: ${modelId}`);
// 从队列中移除任务
uploadQueue = uploadQueue.filter(id => id !== taskId);
// 如果是当前正在上传的任务,取消上传
if (currentUploadTask === taskId) {
// 发送取消请求到后端
fetch(`/api/upload/cancel/${taskId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.catch(error => {
console.error('取消上传失败:', error);
showNotification('错误', '取消上传失败', 'error');
});
currentUploadTask = null;
// 继续处理队列中的下一个任务
processUploadQueue();
}
// 更新任务状态
const task = uploadTasks[taskId];
if (task) {
task.status = 'cancelled';
// 更新UI
const progressElement = document.getElementById(`upload_progress_${taskId}`);
if (progressElement) {
const statusBadge = progressElement.querySelector('.status-badge');
const progressDetail = document.getElementById(`upload_progress_detail_${taskId}`);
const cancelBtn = progressElement.querySelector('.cancel-btn');
if (statusBadge) {
statusBadge.className = 'status-badge bg-gray-700/50 text-gray-300 border border-gray-600';
statusBadge.innerHTML = '<i class="fa fa-ban mr-1"></i>已取消';
}
if (progressDetail) progressDetail.textContent = '上传已取消';
// 隐藏取消按钮
if (cancelBtn) {
cancelBtn.disabled = true;
cancelBtn.textContent = '已取消';
cancelBtn.classList.remove('hover:bg-red-900/50');
cancelBtn.classList.add('bg-gray-800/50', 'text-gray-400', 'cursor-not-allowed');
}
}
// 显示通知
showNotification('提示', `模型 ${modelId} 上传已取消`, 'info');
// 从任务列表中移除
setTimeout(() => {
delete uploadTasks[taskId];
const progressElement = document.getElementById(`upload_progress_${taskId}`);
if (progressElement) {
progressElement.remove();
}
// 如果没有活跃任务,隐藏进度区域
if (Object.keys(uploadTasks).length === 0) {
document.getElementById('upload-progress').classList.add('hidden');
}
// 重新加载模型列表
loadModelsList();
}, 2000);
}
}
}
// 主动查询任务状态
function fetchTaskStatus(taskId, taskType) {
console.log(`[DEBUG] 主动查询任务状态: ${taskId}, 类型: ${taskType}`);
fetch(`/api/task/${taskId}`)
.then(response => {
if (!response.ok) {
throw new Error(`Network response was not ok: ${response.status}`);
}
return response.json();
})
.then(taskData => {
console.log(`[DEBUG] 收到任务状态:`, taskData);
// 更新任务信息
if (taskType === 'download') {
downloadTasks[taskId] = taskData;
updateDownloadProgress(taskId, taskData.progress, taskData.message);
// 如果任务已完成或失败,停止查询
if (taskData.status === 'completed' || taskData.status === 'failed') {
return;
}
} else if (taskType === 'upload') {
uploadTasks[taskId] = taskData;
updateUploadProgress(taskId, taskData.progress, taskData.message);
// 如果任务已完成或失败,停止查询
if (taskData.status === 'uploaded' || taskData.status === 'failed') {
return;
}
}
// 继续定期查询
setTimeout(() => {
fetchTaskStatus(taskId, taskType);
}, 3000); // 每3秒查询一次
})
.catch(error => {
console.error(`[DEBUG] 查询任务状态失败: ${error.message}`);
// 错误后重试
setTimeout(() => {
fetchTaskStatus(taskId, taskType);
}, 5000); // 错误后5秒重试
});
}
// 加载所有模型列表
function loadAllModelsList() {
const allModelsList = document.getElementById('all-models-list');
// 显示加载状态
allModelsList.innerHTML = `
<div class="text-center text-gray-400 py-4">
<i class="fa fa-spinner fa-spin mr-2"></i>
加载中...
</div>
`;
// 从后端获取所有模型列表,传递当前配置的模型路径
const modelPath = settings.defaultModelPath;
// 使用AbortController设置超时
const controller = new AbortController();
const signal = controller.signal;
// 设置30秒超时
const timeoutId = setTimeout(() => {
controller.abort();
allModelsList.innerHTML = `
<div class="text-center text-red-400 py-4">
<i class="fa fa-exclamation-triangle mr-2"></i>
加载超时: 请检查后端服务是否运行
</div>
`;
}, 30000);
fetch(`/api/models?all=true&path=${encodeURIComponent(modelPath)}`, {
signal: signal
})
.then(response => {
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`Network response was not ok: ${response.status}`);
}
return response.json();
})
.then(data => {
clearTimeout(timeoutId);
const models = data.models || [];
// 更新所有模型列表
updateAllModelsList(models);
})
.catch(error => {
clearTimeout(timeoutId);
console.error('Error loading all models list:', error);
let errorMessage = error.message;
if (error.name === 'AbortError') {
errorMessage = '请求超时';
}
allModelsList.innerHTML = `
<div class="text-center text-red-400 py-4">
<i class="fa fa-exclamation-triangle mr-2"></i>
加载失败: ${errorMessage}
</div>
`;
});
}
// 更新所有模型列表
function updateAllModelsList(models) {
const allModelsList = document.getElementById('all-models-list');
if (models.length === 0) {
allModelsList.innerHTML = `
<div class="text-center text-gray-400 py-4">
没有模型
</div>
`;
return;
}
// 清空列表
allModelsList.innerHTML = '';
// 添加模型项
models.forEach(model => {
const modelElement = document.createElement('div');
modelElement.className = 'model-item';
// 确定状态标签
let statusBadge = '';
if (model.status === 'uploaded') {
statusBadge = '<span class="status-badge status-uploaded"><i class="fa fa-cloud-upload mr-1"></i>已上传</span>';
} else if (model.status === 'downloading') {
statusBadge = '<span class="status-badge status-downloading"><i class="fa fa-spinner fa-spin mr-1"></i>下载中</span>';
} else if (model.status === 'uploading') {
statusBadge = '<span class="status-badge status-uploading"><i class="fa fa-spinner fa-spin mr-1"></i>上传中</span>';
} else {
statusBadge = '<span class="status-badge status-downloaded"><i class="fa fa-check mr-1"></i>已下载</span>';
}
modelElement.innerHTML = `
<div class="flex justify-between items-center mb-2">
<h3 class="font-medium">${model.id}</h3>
${statusBadge}
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-2 text-sm">
<div class="text-gray-400">
<span class="text-gray-500">路径:</span> ${model.path}
</div>
<div class="text-gray-400">
<span class="text-gray-500">大小:</span> ${model.size}
</div>
<div class="text-gray-400">
<span class="text-gray-500">下载时间:</span> ${model.downloadTime}
</div>
<div class="text-gray-400">
<span class="text-gray-500">上传时间:</span> ${model.uploadTime || '未上传'}
</div>
</div>
`;
allModelsList.appendChild(modelElement);
});
}
// 开始上传
function startUpload() {
if (selectedModels.length === 0) {
showNotification('错误', '请选择要上传的模型', 'error');
return;
}
// 显示上传进度区域
document.getElementById('upload-progress').classList.remove('hidden');
// 清空上传队列
uploadQueue = [];
// 为每个模型创建上传进度条并添加到队列
selectedModels.forEach(modelId => {
const taskId = `upload_task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
// 保存任务信息
const task = {
task_id: taskId,
modelId,
status: 'pending',
progress: 0,
message: '准备上传...'
};
uploadTasks[taskId] = task;
uploadQueue.push(taskId);
// 保存到localStorage
saveUploadTask(task);
// 创建进度条元素
const progressElement = document.createElement('div');
progressElement.id = `upload_progress_${taskId}`;
progressElement.className = 'model-item';
progressElement.innerHTML = `
<div class="flex justify-between items-center mb-2">
<div>
<h3 class="font-medium">${modelId}</h3>
<span class="status-badge status-pending">
<i class="fa fa-clock-o mr-1"></i>
等待中...
</span>
</div>
<div class="flex items-center space-x-2">
<span class="text-sm text-gray-400" id="upload_progress_text_${taskId}">0%</span>
<button class="delete-btn bg-red-900/30 hover:bg-red-900/50 text-red-300 text-xs px-2 py-1 rounded border border-red-800/50"
onclick="deleteUploadTask('${taskId}', '${modelId}')">
<i class="fa fa-trash mr-1"></i>删除
</button>
</div>
</div>
<div class="progress-bar">
<div class="progress-value" id="upload_progress_value_${taskId}" style="width: 0%"></div>
</div>
<div class="mt-2 text-xs text-gray-400" id="upload_progress_detail_${taskId}">
准备上传...
</div>
`;
document.getElementById('upload-progress-container').appendChild(progressElement);
});
// 开始处理上传队列
processUploadQueue();
// 清空选中的模型
selectedModels = [];
console.log('开始上传模型:', selectedModels);
}
// 保存上传任务到localStorage
function saveUploadTask(task) {
try {
const savedTasks = JSON.parse(localStorage.getItem('uploadTasks') || '{}');
savedTasks[task.task_id] = {
task_id: task.task_id,
modelId: task.modelId,
status: task.status,
progress: task.progress,
message: task.message
};
localStorage.setItem('uploadTasks', JSON.stringify(savedTasks));
} catch (error) {
console.error('保存上传任务失败:', error);
}
}
// 从localStorage加载上传任务
function loadUploadTasks() {
try {
const savedTasks = JSON.parse(localStorage.getItem('uploadTasks') || '{}');
const activeTasks = [];
// 清空进度容器
const uploadProgressContainer = document.getElementById('upload-progress-container');
uploadProgressContainer.innerHTML = '';
// 用于存储需要验证的任务
const tasksToVerify = [];
// 恢复所有任务
Object.values(savedTasks).forEach(task => {
tasksToVerify.push(task);
});
// 如果有任务需要验证,先向后端确认任务是否还存在
if (tasksToVerify.length > 0) {
// 先显示进度区域
document.getElementById('upload-progress').classList.remove('hidden');
let verifiedCount = 0;
tasksToVerify.forEach(task => {
fetch(`/api/task/${task.task_id}`, {
method: 'GET'
})
.then(response => {
if (response.ok) {
return response.json();
}
return null;
})
.then(taskData => {
verifiedCount++;
// 无论任务是否在后端存在,都显示前端存储的任务
activeTasks.push(task);
// 确定任务状态
let taskStatus = task.status;
let taskProgress = task.progress || 0;
let taskMessage = task.message || '恢复任务...';
// 如果后端有任务数据,使用后端数据
if (taskData) {
taskStatus = taskData.status;
taskProgress = taskData.progress || 0;
taskMessage = taskData.message || taskMessage;
}
// 创建进度条元素
const progressElement = document.createElement('div');
progressElement.id = `upload_progress_${task.task_id}`;
progressElement.className = 'model-item';
// 根据任务状态生成不同的HTML
let statusBadge = '';
let buttonsHTML = '';
switch (taskStatus) {
case 'uploading':
statusBadge = '<span class="status-badge status-uploading"><i class="fa fa-spinner fa-spin mr-1"></i>上传中...</span>';
buttonsHTML = `
<button class="delete-btn bg-red-900/30 hover:bg-red-900/50 text-red-300 text-xs px-2 py-1 rounded border border-red-800/50"
onclick="deleteUploadTask('${task.task_id}', '${task.modelId}')">
<i class="fa fa-trash mr-1"></i>删除
</button>
`;
break;
case 'pending':
statusBadge = '<span class="status-badge status-pending"><i class="fa fa-clock-o mr-1"></i>等待中...</span>';
buttonsHTML = `
<button class="delete-btn bg-red-900/30 hover:bg-red-900/50 text-red-300 text-xs px-2 py-1 rounded border border-red-800/50"
onclick="deleteUploadTask('${task.task_id}', '${task.modelId}')">
<i class="fa fa-trash mr-1"></i>删除
</button>
`;
break;
case 'completed':
statusBadge = '<span class="status-badge bg-green-900/50 text-green-300 border border-green-700"><i class="fa fa-check mr-1"></i>上传完成</span>';
buttonsHTML = `
<button class="delete-btn bg-red-900/30 hover:bg-red-900/50 text-red-300 text-xs px-2 py-1 rounded border border-red-800/50"
onclick="deleteUploadTask('${task.task_id}', '${task.modelId}')">
<i class="fa fa-trash mr-1"></i>删除
</button>
`;
break;
case 'failed':
statusBadge = '<span class="status-badge bg-red-900/50 text-red-300 border border-red-700"><i class="fa fa-times mr-1"></i>上传失败</span>';
buttonsHTML = `
<button class="delete-btn bg-red-900/30 hover:bg-red-900/50 text-red-300 text-xs px-2 py-1 rounded border border-red-800/50"
onclick="deleteUploadTask('${task.task_id}', '${task.modelId}')">
<i class="fa fa-trash mr-1"></i>删除
</button>
`;
break;
case 'cancelled':
statusBadge = '<span class="status-badge bg-gray-700/50 text-gray-300 border border-gray-600"><i class="fa fa-ban mr-1"></i>已取消</span>';
buttonsHTML = `
<button class="delete-btn bg-red-900/30 hover:bg-red-900/50 text-red-300 text-xs px-2 py-1 rounded border border-red-800/50"
onclick="deleteUploadTask('${task.task_id}', '${task.modelId}')">
<i class="fa fa-trash mr-1"></i>删除
</button>
`;
break;
default:
statusBadge = '<span class="status-badge status-pending"><i class="fa fa-clock-o mr-1"></i>等待中...</span>';
buttonsHTML = `
<button class="delete-btn bg-red-900/30 hover:bg-red-900/50 text-red-300 text-xs px-2 py-1 rounded border border-red-800/50"
onclick="deleteUploadTask('${task.task_id}', '${task.modelId}')">
<i class="fa fa-trash mr-1"></i>删除
</button>
`;
}
// 构建完整的HTML
progressElement.innerHTML = `
<div class="flex justify-between items-center mb-2">
<div>
<h3 class="font-medium">${task.modelId}</h3>
${statusBadge}
</div>
<div class="flex items-center space-x-2">
<span class="text-sm text-gray-400" id="upload_progress_text_${task.task_id}">${Math.round(taskProgress)}%</span>
${buttonsHTML}
</div>
</div>
<div class="progress-bar">
<div class="progress-value" id="upload_progress_value_${task.task_id}" style="width: ${taskProgress}%"></div>
</div>
<div class="mt-2 text-xs text-gray-400" id="upload_progress_detail_${task.task_id}">
${taskMessage}
</div>
`;
uploadProgressContainer.appendChild(progressElement);
// 恢复任务到全局变量
uploadTasks[task.task_id] = task;
// 如果任务状态是 pending,添加到上传队列
if (task.status === 'pending') {
uploadQueue.push(task.task_id);
}
// 所有验证完成后更新 localStorage 并处理队列
if (verifiedCount === tasksToVerify.length) {
localStorage.setItem('uploadTasks', JSON.stringify(savedTasks));
console.log('已恢复', activeTasks.length, '个上传任务');
// 如果没有活跃任务,隐藏进度区域
if (activeTasks.length === 0) {
document.getElementById('upload-progress').classList.add('hidden');
} else {
// 开始处理上传队列
processUploadQueue();
}
}
})
.catch(error => {
verifiedCount++;
console.error('验证上传任务失败:', error);
// 即使验证失败,也显示任务
activeTasks.push(task);
// 创建进度条元素
const progressElement = document.createElement('div');
progressElement.id = `upload_progress_${task.task_id}`;
progressElement.className = 'model-item';
progressElement.innerHTML = `
<div class="flex justify-between items-center mb-2">
<div>
<h3 class="font-medium">${task.modelId}</h3>
<span class="status-badge bg-gray-700/50 text-gray-300 border border-gray-600">
<i class="fa fa-exclamation mr-1"></i>状态未知
</span>
</div>
<div class="flex items-center space-x-2">
<span class="text-sm text-gray-400" id="upload_progress_text_${task.task_id}">${Math.round(task.progress || 0)}%</span>
<button class="delete-btn bg-red-900/30 hover:bg-red-900/50 text-red-300 text-xs px-2 py-1 rounded border border-red-800/50"
onclick="deleteUploadTask('${task.task_id}', '${task.modelId}')">
<i class="fa fa-trash mr-1"></i>删除
</button>
</div>
</div>
<div class="progress-bar">
<div class="progress-value" id="upload_progress_value_${task.task_id}" style="width: ${task.progress || 0}%"></div>
</div>
<div class="mt-2 text-xs text-gray-400" id="upload_progress_detail_${task.task_id}">
任务状态未知
</div>
`;
uploadProgressContainer.appendChild(progressElement);
// 恢复任务到全局变量
uploadTasks[task.task_id] = task;
if (verifiedCount === tasksToVerify.length) {
localStorage.setItem('uploadTasks', JSON.stringify(savedTasks));
console.log('已恢复', activeTasks.length, '个上传任务');
if (activeTasks.length === 0) {
document.getElementById('upload-progress').classList.add('hidden');
}
}
});
});
} else {
// 没有任务,隐藏进度区域
document.getElementById('upload-progress').classList.add('hidden');
}
} catch (error) {
console.error('加载上传任务失败:', error);
document.getElementById('upload-progress').classList.add('hidden');
}
}
// 处理上传队列
function processUploadQueue() {
if (currentUploadTask || uploadQueue.length === 0) {
return;
}
// 获取队列中的第一个任务
const taskId = uploadQueue.shift();
const task = uploadTasks[taskId];
if (!task || task.status === 'cancelled' || task.status === 'failed') {
// 跳过已取消或失败的任务
processUploadQueue();
return;
}
// 更新任务状态为上传中
task.status = 'uploading';
// 更新UI
const progressElement = document.getElementById(`upload_progress_${taskId}`);
if (progressElement) {
const statusBadge = progressElement.querySelector('.status-badge');
if (statusBadge) {
statusBadge.className = 'status-badge status-uploading';
statusBadge.innerHTML = '<i class="fa fa-spinner fa-spin mr-1"></i>上传中...';
}
}
// 设置当前上传任务
currentUploadTask = taskId;
// 发送请求到后端开始上传
fetch('/api/upload', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
model_ids: [task.modelId],
create_repo_flag: true
})
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
console.log('上传请求已发送:', data);
if (data.error) {
throw new Error(data.error);
}
})
.catch(error => {
console.error('Error starting upload:', error);
showNotification('错误', `启动上传失败: ${error.message}`, 'error');
// 更新任务状态为失败
task.status = 'failed';
currentUploadTask = null;
const progressElement = document.getElementById(`upload_progress_${taskId}`);
if (progressElement) {
const statusBadge = progressElement.querySelector('.status-badge');
const progressDetail = document.getElementById(`upload_progress_detail_${taskId}`);
if (statusBadge) {
statusBadge.className = 'status-badge bg-red-900/50 text-red-300 border border-red-700';
statusBadge.innerHTML = '<i class="fa fa-times mr-1"></i>上传失败';
}
if (progressDetail) progressDetail.textContent = `上传失败: ${error.message}`;
}
// 继续处理队列
processUploadQueue();
});
}
// 删除上传任务
function deleteUploadTask(taskId, modelId) {
if (confirm(`确定要删除上传任务 ${modelId} 吗?`)) {
// 从队列中移除任务
uploadQueue = uploadQueue.filter(id => id !== taskId);
// 如果是当前正在上传的任务,取消它
if (currentUploadTask === taskId) {
// 发送取消请求到后端
fetch(`/api/upload/cancel/${taskId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.catch(error => console.error('取消上传失败:', error));
currentUploadTask = null;
}
// 从uploadTasks对象中删除任务
delete uploadTasks[taskId];
// 更新localStorage
const savedTasks = JSON.parse(localStorage.getItem('uploadTasks') || '{}');
delete savedTasks[taskId];
localStorage.setItem('uploadTasks', JSON.stringify(savedTasks));
// 从UI中移除任务
const progressElement = document.getElementById(`upload_progress_${taskId}`);
if (progressElement) {
progressElement.remove();
}
// 显示通知
showNotification('提示', `上传任务 ${modelId} 已删除`, 'info');
// 继续处理队列
processUploadQueue();
// 如果没有活跃任务,隐藏进度区域
if (Object.keys(uploadTasks).length === 0) {
document.getElementById('upload-progress').classList.add('hidden');
}
}
}
// 上传单个模型
function uploadSingleModel(modelId) {
// 显示上传进度区域
document.getElementById('upload-progress').classList.remove('hidden');
// 创建任务ID
const taskId = `upload_task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
// 保存任务信息
uploadTasks[taskId] = {
modelId,
status: 'uploading',
progress: 0
};
// 清空上传进度容器
const uploadProgressContainer = document.getElementById('upload-progress-container');
// 创建进度条元素
const progressElement = document.createElement('div');
progressElement.id = `upload_progress_${taskId}`;
progressElement.className = 'model-item';
progressElement.innerHTML = `
<div class="flex justify-between items-center mb-2">
<div>
<h3 class="font-medium">${modelId}</h3>
<span class="status-badge status-uploading">
<i class="fa fa-spinner fa-spin mr-1"></i>
上传中...
</span>
</div>
<span class="text-sm text-gray-400" id="upload_progress_text_${taskId}">0%</span>
</div>
<div class="progress-bar">
<div class="progress-value" id="upload_progress_value_${taskId}" style="width: 0%"></div>
</div>
<div class="mt-2 text-xs text-gray-400" id="upload_progress_detail_${taskId}">
准备上传...
</div>
`;
uploadProgressContainer.appendChild(progressElement);
// 发送请求到后端开始上传
fetch('/api/upload', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
model_ids: [modelId],
create_repo_flag: true
})
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
console.log('上传请求已发送:', data);
if (data.error) {
throw new Error(data.error);
}
})
.catch(error => {
console.error('Error starting upload:', error);
showNotification('错误', `启动上传失败: ${error.message}`, 'error');
// 更新任务状态为失败
task.status = 'failed';
const progressElement = document.getElementById(`upload_progress_${taskId}`);
if (progressElement) {
const statusBadge = progressElement.querySelector('.status-badge');
const progressDetail = document.getElementById(`upload_progress_detail_${taskId}`);
if (statusBadge) {
statusBadge.className = 'status-badge bg-red-900/50 text-red-300 border border-red-700';
statusBadge.innerHTML = '<i class="fa fa-times mr-1"></i>上传失败';
}
if (progressDetail) progressDetail.textContent = `上传失败: ${error.message}`;
}
});
console.log('开始上传模型:', modelId);
}
// 模拟上传进度
function simulateUploadProgress(taskId) {
const task = uploadTasks[taskId];
if (!task) return;
// 模拟进度增加
let progress = task.progress;
const interval = setInterval(() => {
// 随机增加进度
const increment = Math.random() * 8;
progress = Math.min(progress + increment, 100);
// 更新任务进度
task.progress = progress;
// 更新UI
const progressText = document.getElementById(`upload_progress_text_${taskId}`);
const progressValue = document.getElementById(`upload_progress_value_${taskId}`);
const progressDetail = document.getElementById(`upload_progress_detail_${taskId}`);
if (progressText) progressText.textContent = `${Math.round(progress)}%`;
if (progressValue) progressValue.style.width = `${progress}%`;
// 随机更新详细信息
const details = [
`上传中: ${Math.round(progress)}%`,
`正在上传模型文件...`,
`已上传 ${Math.round(progress * 100 / 100)}MB / 100MB`,
`正在验证上传文件...`
];
if (progressDetail) {
progressDetail.textContent = details[Math.floor(Math.random() * details.length)];
}
// 检查是否完成
if (progress >= 100) {
clearInterval(interval);
// 模拟上传完成
setTimeout(() => {
handleUploadComplete({
taskId,
modelId: task.modelId
});
}, 500);
}
}, 1000);
// 保存定时器ID
task.interval = interval;
}
// 更新上传进度
function updateUploadProgress(data) {
const { taskId, progress, detail } = data;
const task = uploadTasks[taskId];
if (task) {
task.progress = progress;
// 更新UI
const progressText = document.getElementById(`upload_progress_text_${taskId}`);
const progressValue = document.getElementById(`upload_progress_value_${taskId}`);
const progressDetail = document.getElementById(`upload_progress_detail_${taskId}`);
if (progressText) progressText.textContent = `${Math.round(progress)}%`;
if (progressValue) progressValue.style.width = `${progress}%`;
if (progressDetail && detail) progressDetail.textContent = detail;
}
}
// 处理上传完成
function handleUploadComplete(data) {
const { taskId, modelId } = data;
const task = uploadTasks[taskId];
if (task) {
// 清除定时器
if (task.interval) {
clearInterval(task.interval);
}
// 更新任务状态
task.status = 'uploaded';
task.progress = 100;
// 更新UI
const progressElement = document.getElementById(`upload_progress_${taskId}`);
if (progressElement) {
const statusBadge = progressElement.querySelector('.status-badge');
const progressText = document.getElementById(`upload_progress_text_${taskId}`);
const progressValue = document.getElementById(`upload_progress_value_${taskId}`);
const progressDetail = document.getElementById(`upload_progress_detail_${taskId}`);
if (statusBadge) {
statusBadge.className = 'status-badge status-uploaded';
statusBadge.innerHTML = '<i class="fa fa-check mr-1"></i>上传完成';
}
if (progressText) progressText.textContent = '100%';
if (progressValue) {
progressValue.style.width = '100%';
progressValue.classList.add('bg-secondary');
}
if (progressDetail) progressDetail.textContent = `模型已成功上传到 CsgHub`;
}
// 显示通知
showNotification('成功', `模型 ${modelId} 上传完成`, 'success');
// 清空当前上传任务
currentUploadTask = null;
// 继续处理队列中的下一个任务
processUploadQueue();
// 如果是最后一个任务,重新加载模型列表
if (Object.values(uploadTasks).every(t => t.status === 'uploaded' || t.status === 'failed' || t.status === 'cancelled')) {
setTimeout(() => {
loadModelsList();
loadAllModelsList();
}, 1000);
}
}
}
// 处理上传失败
function handleUploadFailed(data) {
const { taskId, modelId, error } = data;
const task = uploadTasks[taskId];
if (task) {
// 清除定时器
if (task.interval) {
clearInterval(task.interval);
}
// 更新任务状态
task.status = 'failed';
// 更新UI
const progressElement = document.getElementById(`upload_progress_${taskId}`);
if (progressElement) {
const statusBadge = progressElement.querySelector('.status-badge');
const progressValue = document.getElementById(`upload_progress_value_${taskId}`);
const progressDetail = document.getElementById(`upload_progress_detail_${taskId}`);
if (statusBadge) {
statusBadge.className = 'status-badge bg-red-900/50 text-red-300 border border-red-700';
statusBadge.innerHTML = '<i class="fa fa-times mr-1"></i>上传失败';
}
if (progressValue) {
progressValue.classList.add('bg-danger');
}
if (progressDetail) progressDetail.textContent = `上传失败: ${error}`;
}
// 显示通知
showNotification('错误', `模型 ${modelId} 上传失败`, 'error');
// 清空当前上传任务
currentUploadTask = null;
// 继续处理队列中的下一个任务
processUploadQueue();
}
}
// 显示删除确认对话框
function showDeleteConfirm() {
if (selectedDeleteModels.length === 0) {
showNotification('错误', '请选择要删除的模型', 'error');
return;
}
// 更新删除数量
document.getElementById('delete-count').textContent = selectedDeleteModels.length;
// 显示确认对话框
deleteConfirmModal.classList.remove('hidden');
}
// 确认删除
function confirmDelete() {
console.log('[DEBUG] confirmDelete函数被调用');
console.log('[DEBUG] selectedDeleteModels:', selectedDeleteModels);
if (selectedDeleteModels.length === 0) {
console.log('[DEBUG] 没有选中的模型,直接返回');
return;
}
// 隐藏确认对话框
hideDeleteConfirm();
// 显示加载状态
const deleteBtn = document.querySelector('#close-delete-confirm-btn');
const originalText = deleteBtn.textContent;
deleteBtn.disabled = true;
deleteBtn.innerHTML = '<i class="fa fa-spinner fa-spin mr-2"></i>删除中...';
// 发送请求到后端删除模型
console.log('[DEBUG] 准备发送删除请求到后端');
console.log('[DEBUG] 请求URL: /api/delete');
console.log('[DEBUG] 请求数据:', { model_ids: selectedDeleteModels });
fetch('/api/delete', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
model_ids: selectedDeleteModels
})
})
.then(response => {
console.log('[DEBUG] 收到后端响应:', response);
console.log('[DEBUG] 响应状态码:', response.status);
console.log('[DEBUG] 响应状态文本:', response.statusText);
if (!response.ok) {
throw new Error(`Network response was not ok: ${response.status} ${response.statusText}`);
}
return response.json();
})
.then(data => {
console.log('[DEBUG] 删除结果:', data);
if (data.deleted && data.deleted.length > 0) {
showNotification('成功', `已成功删除 ${data.deleted.length} 个模型`, 'success');
// 重新加载模型列表
setTimeout(() => {
loadDeleteModelsList();
loadAllModelsList();
}, 500);
}
if (data.errors && data.errors.length > 0) {
showNotification('警告', `部分模型删除失败: ${data.errors.join(', ')}`, 'warning');
}
// 清空选中的模型
selectedDeleteModels = [];
})
.catch(error => {
console.error('[DEBUG] 删除模型失败:', error);
console.error('[DEBUG] 错误详情:', error.stack);
showNotification('错误', `删除模型失败: ${error.message}`, 'error');
})
.finally(() => {
// 恢复按钮状态
deleteBtn.disabled = false;
deleteBtn.textContent = originalText;
});
}
// 隐藏删除确认对话框
function hideDeleteConfirm() {
deleteConfirmModal.classList.add('hidden');
}
// 显示设置对话框
function showSettings() {
// 加载当前设置
document.getElementById('default-model-path').value = settings.defaultModelPath;
document.getElementById('max-retry').value = settings.maxRetry;
document.getElementById('csghub-url').value = settings.csghubUrl;
document.getElementById('csghub-token').value = settings.csghubToken;
// 显示设置对话框
settingsModal.classList.remove('hidden');
}
// 隐藏设置对话框
function hideSettings() {
settingsModal.classList.add('hidden');
}
// 保存设置
function saveSettings() {
const defaultModelPath = document.getElementById('default-model-path').value.trim();
const maxRetry = parseInt(document.getElementById('max-retry').value);
const csghubUrl = document.getElementById('csghub-url').value.trim();
const csghubToken = document.getElementById('csghub-token').value.trim();
if (!defaultModelPath) {
showNotification('错误', '请输入默认模型路径', 'error');
return;
}
if (isNaN(maxRetry) || maxRetry < 1 || maxRetry > 20) {
showNotification('错误', '最大重试次数必须在 1-20 之间', 'error');
return;
}
if (!csghubUrl) {
showNotification('错误', '请输入 CsgHub API URL', 'error');
return;
}
if (!csghubToken) {
showNotification('错误', '请输入 CsgHub Token', 'error');
return;
}
// 更新设置
settings = {
defaultModelPath,
maxRetry,
csghubUrl,
csghubToken
};
// 保存到本地存储
localStorage.setItem('modelManagerSettings', JSON.stringify(settings));
// 更新本地路径输入框
document.getElementById('local-path').value = settings.defaultModelPath;
// 更新顶部状态栏的模型目录显示
const modelDirElement = document.getElementById('model-dir');
if (modelDirElement) {
modelDirElement.textContent = settings.defaultModelPath;
}
// 隐藏设置对话框
hideSettings();
// 显示通知
showNotification('成功', '设置已保存', 'success');
console.log('保存设置:', settings);
}
// 加载设置
function loadSettings() {
const savedSettings = localStorage.getItem('modelManagerSettings');
if (savedSettings) {
try {
settings = JSON.parse(savedSettings);
// 更新本地路径输入框
document.getElementById('local-path').value = settings.defaultModelPath;
} catch (error) {
console.error('加载设置失败:', error);
}
}
}
// 切换全选
function toggleSelectAll() {
const checkboxes = document.querySelectorAll('.model-checkbox');
const isAllSelected = selectedModels.length === checkboxes.length;
checkboxes.forEach(checkbox => {
checkbox.checked = !isAllSelected;
});
// 更新选中模型数组
selectedModels = isAllSelected ? [] : Array.from(checkboxes).map(cb => cb.value);
}
// 切换删除全选
function toggleSelectAllDelete() {
const checkboxes = document.querySelectorAll('.delete-model-checkbox');
const isAllSelected = selectedDeleteModels.length === checkboxes.length;
checkboxes.forEach(checkbox => {
checkbox.checked = !isAllSelected;
});
// 更新选中模型数组
selectedDeleteModels = isAllSelected ? [] : Array.from(checkboxes).map(cb => cb.value);
}
// 显示通知
function showNotification(title, message, type = 'info') {
const notificationTitle = document.getElementById('notification-title');
const notificationMessage = document.getElementById('notification-message');
const notificationIcon = document.getElementById('notification-icon');
// 设置通知内容
notificationTitle.textContent = title;
notificationMessage.textContent = message;
// 设置图标
let iconClass = 'fa-info-circle text-blue-500';
if (type === 'success') {
iconClass = 'fa-check-circle text-green-500';
} else if (type === 'error') {
iconClass = 'fa-times-circle text-red-500';
} else if (type === 'warning') {
iconClass = 'fa-exclamation-triangle text-yellow-500';
}
notificationIcon.innerHTML = `<i class="fa ${iconClass} text-xl"></i>`;
// 显示通知
notification.classList.remove('translate-x-full');
// 3秒后自动隐藏
setTimeout(() => {
hideNotification();
}, 3000);
}
// 隐藏通知
function hideNotification() {
notification.classList.add('translate-x-full');
}
// 加载系统信息
function loadSystemInfo() {
// 模拟加载系统信息
setTimeout(() => {
const systemInfo = {
os: 'Linux Ubuntu 22.04',
modelDir: '/home/user/models',
diskUsage: '65%',
memoryUsage: '42%'
};
// 更新系统信息
document.getElementById('system-info').textContent = systemInfo.os;
document.getElementById('model-dir').textContent = systemInfo.modelDir;
}, 500);
}
// 更新系统信息
function updateSystemInfo(data) {
if (data.os) {
document.getElementById('system-info').textContent = data.os;
}
if (data.modelDir) {
document.getElementById('model-dir').textContent = data.modelDir;
}
}
// 页面加载完成后初始化 - 已移至主初始化函数
</script>
</body>
</html>
\ No newline at end of file
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment