加密模型
parent
c81035f06c
commit
89e8252a97
|
|
@ -9,4 +9,4 @@ dist.zip
|
|||
__pycache__
|
||||
models
|
||||
*.log
|
||||
*.yolo_detection
|
||||
yolo_detection.log
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
13
config.py
13
config.py
|
|
@ -69,5 +69,16 @@ def get_default_config():
|
|||
'check_interval': 5,
|
||||
'adjust_threshold': 5
|
||||
},
|
||||
'model_path': 'models'
|
||||
'model_path': 'models',
|
||||
# 添加上传配置
|
||||
'upload': {
|
||||
'enabled': True,
|
||||
'max_file_size': 1024 * 1024 * 1024, # 1GB
|
||||
'chunk_size': 1024 * 1024 * 5, # 5MB
|
||||
'allowed_extensions': ['.pt', '.pth', '.onnx', '.engine'],
|
||||
'uploads_dir': 'uploads',
|
||||
'temp_dir': 'temp_uploads',
|
||||
'encrypted_models_dir': 'encrypted_models',
|
||||
'session_expire_hours': 24
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import datetime
|
||||
import gc
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import queue
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import traceback
|
||||
|
|
@ -17,89 +19,131 @@ from _minio import MinioUploader
|
|||
from log import logger
|
||||
from global_data import gd
|
||||
from detection_render import OptimizedDetectionRenderer
|
||||
from mandatory_model_crypto import MandatoryModelEncryptor
|
||||
|
||||
|
||||
# detectionThread.py - 修改 ModelManager 类
|
||||
|
||||
class ModelManager:
|
||||
"""模型管理器,支持多模型和加密模型"""
|
||||
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
self.models_dir = "models"
|
||||
self.encrypted_models_dir = config.get('upload', {}).get('encrypted_models_dir', 'encrypted_models')
|
||||
|
||||
# 确保目录存在
|
||||
os.makedirs(self.models_dir, exist_ok=True)
|
||||
os.makedirs(self.encrypted_models_dir, exist_ok=True)
|
||||
|
||||
# 模型加载缓存(避免重复解密)
|
||||
self.model_cache = {}
|
||||
self.cache_lock = threading.Lock()
|
||||
|
||||
def load_model(self, model_config, require_verification=False):
|
||||
"""加载单个模型(支持加密),可选密钥验证"""
|
||||
"""加载单个模型 - 从本地加载加密模型"""
|
||||
try:
|
||||
model_path = model_config['path']
|
||||
encrypted = model_config.get('encrypted', False)
|
||||
encryption_key = model_config.get('encryption_key')
|
||||
|
||||
# 构建本地路径
|
||||
local_path = os.path.join(self.models_dir, os.path.basename(model_path))
|
||||
|
||||
# 下载模型(如果不存在且提供下载地址)
|
||||
if not os.path.exists(local_path):
|
||||
download_url = model_config.get('download_url')
|
||||
if download_url:
|
||||
if not self.download_model(model_config, local_path):
|
||||
logger.warning(f"模型文件不存在且下载失败: {local_path}")
|
||||
return None, None # 返回模型和验证结果
|
||||
if encrypted:
|
||||
# 加密模型从加密模型目录加载
|
||||
if model_path.startswith('encrypted_models/'):
|
||||
# 相对路径
|
||||
local_path = os.path.join(self.encrypted_models_dir, os.path.basename(model_path))
|
||||
elif os.path.isabs(model_path):
|
||||
# 绝对路径
|
||||
local_path = model_path
|
||||
else:
|
||||
logger.warning(f"模型文件不存在: {local_path}")
|
||||
return None, None
|
||||
# 尝试在加密目录中查找
|
||||
model_filename = os.path.basename(model_path)
|
||||
if not model_filename.endswith('.enc'):
|
||||
model_filename += '.enc'
|
||||
local_path = os.path.join(self.encrypted_models_dir, model_filename)
|
||||
else:
|
||||
# 普通模型从普通模型目录加载
|
||||
local_path = os.path.join(self.models_dir, os.path.basename(model_path))
|
||||
|
||||
# 检查模型文件是否存在
|
||||
if not os.path.exists(local_path):
|
||||
logger.error(f"模型文件不存在: {local_path}")
|
||||
return None, {'success': False, 'error': f'模型文件不存在: {local_path}'}
|
||||
|
||||
# 检查缓存
|
||||
cache_key = f"{local_path}_{hashlib.md5(encryption_key.encode()).hexdigest()[:8]}" if encryption_key else local_path
|
||||
|
||||
with self.cache_lock:
|
||||
if cache_key in self.model_cache:
|
||||
logger.info(f"使用缓存的模型: {model_path}")
|
||||
cached_info = self.model_cache[cache_key]
|
||||
return cached_info['model'], cached_info.get('verification_result', {'success': True})
|
||||
|
||||
# 验证加密模型密钥(如果需要)
|
||||
verification_result = None
|
||||
model = None
|
||||
|
||||
if encrypted and encryption_key:
|
||||
if require_verification:
|
||||
verification_result = self.verify_model_key(local_path, encryption_key)
|
||||
if not verification_result.get('success', False):
|
||||
logger.error(f"加密模型密钥验证失败: {model_path}")
|
||||
return None, verification_result
|
||||
logger.info(f"加密模型密钥验证成功: {model_path}")
|
||||
|
||||
# 解密并加载模型
|
||||
# 创建临时解密模型
|
||||
try:
|
||||
from mandatory_model_crypto import MandatoryModelEncryptor
|
||||
encryptor = MandatoryModelEncryptor()
|
||||
from mandatory_model_crypto import MandatoryModelValidator
|
||||
validator = MandatoryModelValidator()
|
||||
|
||||
# 解密模型
|
||||
decrypt_result = encryptor.decrypt_model(local_path, encryption_key,
|
||||
verify_key=require_verification)
|
||||
# 解密模型到内存
|
||||
decrypt_result = validator.decrypt_and_verify(local_path, encryption_key)
|
||||
|
||||
if not decrypt_result.get('success', False):
|
||||
logger.error(f"解密模型失败: {model_path}")
|
||||
if not decrypt_result['success']:
|
||||
logger.error(f"解密模型失败: {model_path} - {decrypt_result.get('error', '未知错误')}")
|
||||
return None, decrypt_result
|
||||
|
||||
decrypted_path = decrypt_result.get('temp_path')
|
||||
if not decrypted_path:
|
||||
logger.error(f"未获取到解密后的模型路径: {model_path}")
|
||||
return None, decrypt_result
|
||||
verification_result = {
|
||||
'success': True,
|
||||
'model_hash': decrypt_result.get('model_hash', ''),
|
||||
'original_size': decrypt_result.get('original_size', 0)
|
||||
}
|
||||
|
||||
logger.info(f"解密模型成功: {model_path}")
|
||||
model = YOLO(decrypted_path)
|
||||
# 解密数据
|
||||
decrypted_data = decrypt_result['decrypted_data']
|
||||
|
||||
# 保存到临时文件并加载
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix='.pt') as tmp:
|
||||
tmp.write(decrypted_data)
|
||||
temp_path = tmp.name
|
||||
|
||||
# 加载YOLO模型
|
||||
model = YOLO(temp_path)
|
||||
|
||||
# 清理临时文件
|
||||
try:
|
||||
os.unlink(decrypted_path)
|
||||
os.unlink(temp_path)
|
||||
except Exception as e:
|
||||
logger.warning(f"清理临时文件失败: {str(e)}")
|
||||
|
||||
logger.info(f"加密模型解密加载成功: {model_path}")
|
||||
|
||||
except ImportError:
|
||||
logger.error("mandatory_model_crypto模块未找到,无法处理加密模型")
|
||||
return None, {'success': False, 'error': '加密模块未找到'}
|
||||
except Exception as e:
|
||||
logger.error(f"加密模型处理失败: {str(e)}")
|
||||
return None, {'success': False, 'error': str(e)}
|
||||
elif encrypted and not encryption_key:
|
||||
# 加密模型但没有密钥
|
||||
logger.error(f"加密模型但未提供密钥: {model_path}")
|
||||
return None, {'success': False, 'error': '加密模型需要密钥'}
|
||||
else:
|
||||
# 普通模型加载
|
||||
if encrypted and not encryption_key:
|
||||
logger.warning(f"模型标记为加密但未提供密钥: {model_path}")
|
||||
model = YOLO(local_path)
|
||||
verification_result = {'success': True} if not encrypted else {
|
||||
'success': False,
|
||||
'error': '模型标记为加密但未提供密钥'
|
||||
}
|
||||
try:
|
||||
model = YOLO(local_path)
|
||||
logger.info(f"普通模型加载成功: {local_path}")
|
||||
verification_result = {'success': True}
|
||||
except Exception as e:
|
||||
logger.error(f"加载普通模型失败: {str(e)}")
|
||||
return None, {'success': False, 'error': str(e)}
|
||||
|
||||
if model is None:
|
||||
return None, verification_result or {'success': False, 'error': '模型加载失败'}
|
||||
|
||||
# 应用设备配置
|
||||
device = model_config.get('device', 'cuda:0' if torch.cuda.is_available() else 'cpu')
|
||||
|
|
@ -110,59 +154,37 @@ class ModelManager:
|
|||
model = model.half()
|
||||
logger.info(f"启用半精度推理: {model_path}")
|
||||
|
||||
# 缓存模型
|
||||
with self.cache_lock:
|
||||
self.model_cache[cache_key] = {
|
||||
'model': model,
|
||||
'verification_result': verification_result,
|
||||
'device': device,
|
||||
'cached_at': time.time()
|
||||
}
|
||||
|
||||
logger.info(f"模型加载成功: {model_path} -> {device}")
|
||||
return model, verification_result
|
||||
return model, verification_result or {'success': True}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"加载模型失败 {model_config.get('path')}: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
return None, {'success': False, 'error': str(e)}
|
||||
|
||||
def download_model(self, model_config, save_path):
|
||||
"""下载模型文件"""
|
||||
try:
|
||||
download_url = model_config.get('download_url')
|
||||
def clear_cache(self):
|
||||
"""清空模型缓存"""
|
||||
with self.cache_lock:
|
||||
self.model_cache.clear()
|
||||
logger.info("模型缓存已清空")
|
||||
|
||||
if not download_url:
|
||||
logger.error(f"模型无下载地址: {model_config['path']}")
|
||||
return False
|
||||
|
||||
logger.info(f"下载模型: {download_url} -> {save_path}")
|
||||
|
||||
response = requests.get(download_url, stream=True, timeout=30)
|
||||
response.raise_for_status()
|
||||
|
||||
total_size = int(response.headers.get('content-length', 0))
|
||||
downloaded = 0
|
||||
|
||||
with open(save_path, 'wb') as f:
|
||||
for chunk in response.iter_content(chunk_size=8192):
|
||||
if chunk:
|
||||
downloaded += len(chunk)
|
||||
f.write(chunk)
|
||||
|
||||
if total_size > 0:
|
||||
progress = (downloaded * 100) // total_size
|
||||
if progress % 25 == 0: # 每25%输出一次
|
||||
logger.info(f"下载进度: {progress}%")
|
||||
|
||||
logger.info(f"模型下载完成: {save_path} ({downloaded} 字节)")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"下载模型失败: {str(e)}")
|
||||
return False
|
||||
|
||||
def verify_model_key(self, path, encryption_key):
|
||||
"""验证模型密钥"""
|
||||
try:
|
||||
from mandatory_model_crypto import MandatoryModelEncryptor
|
||||
encryptor = MandatoryModelEncryptor()
|
||||
return encryptor.decrypt_model(path, encryption_key, verify_key=True)
|
||||
except ImportError:
|
||||
return {'success': False, 'error': '加密模块未找到'}
|
||||
except Exception as e:
|
||||
return {'success': False, 'error': str(e)}
|
||||
def get_cache_info(self):
|
||||
"""获取缓存信息"""
|
||||
with self.cache_lock:
|
||||
return {
|
||||
'cache_size': len(self.model_cache),
|
||||
'cached_models': list(self.model_cache.keys()),
|
||||
'total_size': sum(info.get('original_size', 0) for info in self.model_cache.values())
|
||||
}
|
||||
|
||||
|
||||
class DetectionThread(threading.Thread):
|
||||
|
|
@ -296,14 +318,14 @@ class DetectionThread(threading.Thread):
|
|||
return os.name == 'nt' or os.name == 'win32'
|
||||
|
||||
def load_models(self):
|
||||
"""加载多个模型 - 简化版本"""
|
||||
"""加载多个模型 - 优化版本,从本地加载"""
|
||||
try:
|
||||
models_config = self.config.get('models', [])
|
||||
if not models_config or not isinstance(models_config, list):
|
||||
logger.error("未找到有效的models配置列表")
|
||||
return False
|
||||
|
||||
logger.info(f"开始加载 {len(models_config)} 个模型")
|
||||
logger.info(f"开始从本地加载 {len(models_config)} 个模型")
|
||||
|
||||
loaded_models = []
|
||||
key_verification_results = {}
|
||||
|
|
@ -317,14 +339,11 @@ class DetectionThread(threading.Thread):
|
|||
model_path = model_config.get('path', 'unknown')
|
||||
model_name = os.path.basename(model_path).split('.')[0]
|
||||
|
||||
# 强制验证加密模型密钥
|
||||
require_verification = model_config.get('encrypted', False)
|
||||
|
||||
# 加载模型(包含密钥验证)
|
||||
# 加载模型(从本地)
|
||||
logger.info(f"加载模型 {i}: {model_name}")
|
||||
model, verification_result = self.model_manager.load_model(
|
||||
model_config,
|
||||
require_verification=require_verification
|
||||
require_verification=True # 总是验证密钥
|
||||
)
|
||||
|
||||
# 记录验证结果
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
202
model_crypto.py
202
model_crypto.py
|
|
@ -1,202 +0,0 @@
|
|||
# model_crypto.py
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import requests
|
||||
from cryptography.fernet import Fernet
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||
import base64
|
||||
import pickle
|
||||
from pathlib import Path
|
||||
from log import logger
|
||||
|
||||
|
||||
class ModelEncryptor:
|
||||
"""模型加密/解密器"""
|
||||
|
||||
@staticmethod
|
||||
def generate_key(password: str, salt: bytes = None):
|
||||
"""生成加密密钥"""
|
||||
if salt is None:
|
||||
salt = os.urandom(16)
|
||||
|
||||
kdf = PBKDF2HMAC(
|
||||
algorithm=hashes.SHA256(),
|
||||
length=32,
|
||||
salt=salt,
|
||||
iterations=100000,
|
||||
)
|
||||
key = base64.urlsafe_b64encode(kdf.derive(password.encode()))
|
||||
return key, salt
|
||||
|
||||
@staticmethod
|
||||
def encrypt_model(model_path: str, output_path: str, password: str):
|
||||
"""加密模型文件"""
|
||||
try:
|
||||
# 读取模型文件
|
||||
with open(model_path, 'rb') as f:
|
||||
model_data = f.read()
|
||||
|
||||
# 生成密钥
|
||||
key, salt = ModelEncryptor.generate_key(password)
|
||||
fernet = Fernet(key)
|
||||
|
||||
# 加密数据
|
||||
encrypted_data = fernet.encrypt(model_data)
|
||||
|
||||
# 保存加密数据(包含salt)
|
||||
encrypted_payload = {
|
||||
'salt': salt,
|
||||
'data': encrypted_data,
|
||||
'original_size': len(model_data)
|
||||
}
|
||||
|
||||
with open(output_path, 'wb') as f:
|
||||
pickle.dump(encrypted_payload, f)
|
||||
|
||||
logger.info(f"模型加密成功: {model_path} -> {output_path}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"模型加密失败: {str(e)}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def decrypt_model(encrypted_path: str, password: str):
|
||||
"""解密模型到内存"""
|
||||
try:
|
||||
# 读取加密文件
|
||||
with open(encrypted_path, 'rb') as f:
|
||||
encrypted_payload = pickle.load(f)
|
||||
|
||||
salt = encrypted_payload['salt']
|
||||
encrypted_data = encrypted_payload['data']
|
||||
|
||||
# 生成密钥
|
||||
key, _ = ModelEncryptor.generate_key(password, salt)
|
||||
fernet = Fernet(key)
|
||||
|
||||
# 解密数据
|
||||
decrypted_data = fernet.decrypt(encrypted_data)
|
||||
|
||||
# 保存到临时文件
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix='.pt') as tmp:
|
||||
tmp.write(decrypted_data)
|
||||
temp_path = tmp.name
|
||||
|
||||
logger.info(f"模型解密成功: {encrypted_path}")
|
||||
return temp_path
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"模型解密失败: {str(e)}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def is_encrypted(model_path: str):
|
||||
"""检查模型是否加密"""
|
||||
try:
|
||||
with open(model_path, 'rb') as f:
|
||||
# 尝试读取加密格式
|
||||
data = pickle.load(f)
|
||||
return isinstance(data, dict) and 'salt' in data and 'data' in data
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
class ModelManager:
|
||||
"""模型管理器,支持加密模型加载"""
|
||||
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
self.models_dir = "models"
|
||||
os.makedirs(self.models_dir, exist_ok=True)
|
||||
|
||||
def load_model(self, model_config):
|
||||
"""加载模型(支持加密)"""
|
||||
model_path = model_config['path']
|
||||
encrypted = model_config.get('encrypted', False)
|
||||
encryption_key = model_config.get('encryption_key')
|
||||
|
||||
local_path = os.path.join(self.models_dir, os.path.basename(model_path))
|
||||
|
||||
# 下载模型(如果不存在)
|
||||
if not os.path.exists(local_path):
|
||||
if not self.download_model(model_config):
|
||||
return None
|
||||
|
||||
# 如果是加密模型,需要解密
|
||||
if encrypted and encryption_key:
|
||||
if ModelEncryptor.is_encrypted(local_path):
|
||||
decrypted_path = ModelEncryptor.decrypt_model(local_path, encryption_key)
|
||||
if decrypted_path:
|
||||
try:
|
||||
from ultralytics import YOLO
|
||||
model = YOLO(decrypted_path).to(model_config['device'])
|
||||
|
||||
# 清理临时文件
|
||||
try:
|
||||
os.unlink(decrypted_path)
|
||||
except:
|
||||
pass
|
||||
|
||||
return model
|
||||
except Exception as e:
|
||||
logger.error(f"加载解密模型失败: {str(e)}")
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
logger.warning(f"模型未加密或密钥错误: {local_path}")
|
||||
return None
|
||||
else:
|
||||
# 普通模型加载
|
||||
try:
|
||||
from ultralytics import YOLO
|
||||
model = YOLO(local_path).to(model_config['device'])
|
||||
|
||||
# 应用配置
|
||||
if model_config.get('half', False) and 'cuda' in model_config['device']:
|
||||
model = model.half()
|
||||
|
||||
return model
|
||||
except Exception as e:
|
||||
logger.error(f"加载模型失败: {str(e)}")
|
||||
return None
|
||||
|
||||
def download_model(self, model_config):
|
||||
"""下载模型"""
|
||||
try:
|
||||
model_path = model_config['path']
|
||||
download_url = model_config.get('download_url')
|
||||
|
||||
if not download_url:
|
||||
logger.error(f"模型无下载地址: {model_path}")
|
||||
return False
|
||||
|
||||
local_path = os.path.join(self.models_dir, os.path.basename(model_path))
|
||||
|
||||
logger.info(f"下载模型: {download_url} -> {local_path}")
|
||||
|
||||
response = requests.get(download_url, stream=True, timeout=30)
|
||||
response.raise_for_status()
|
||||
|
||||
total_size = int(response.headers.get('content-length', 0))
|
||||
downloaded = 0
|
||||
|
||||
with open(local_path, 'wb') as f:
|
||||
for chunk in response.iter_content(chunk_size=8192):
|
||||
downloaded += len(chunk)
|
||||
f.write(chunk)
|
||||
|
||||
if total_size > 0:
|
||||
progress = downloaded * 100 // total_size
|
||||
if progress % 10 == 0:
|
||||
logger.info(f"下载进度: {progress}%")
|
||||
|
||||
logger.info(f"模型下载完成: {local_path} ({downloaded} 字节)")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"下载模型失败: {str(e)}")
|
||||
return False
|
||||
|
|
@ -0,0 +1,358 @@
|
|||
# model_upload_manager.py
|
||||
import os
|
||||
import hashlib
|
||||
import json
|
||||
import tempfile
|
||||
import time
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from log import logger
|
||||
from cryptography.fernet import Fernet
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||
import base64
|
||||
import pickle
|
||||
|
||||
|
||||
class ChunkedUploadManager:
|
||||
"""分片上传管理器"""
|
||||
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
self.uploads_dir = config.get('uploads_dir', 'uploads')
|
||||
self.temp_dir = config.get('temp_dir', 'temp_uploads')
|
||||
self.encrypted_models_dir = config.get('encrypted_models_dir', 'encrypted_models')
|
||||
|
||||
# 创建必要的目录
|
||||
os.makedirs(self.uploads_dir, exist_ok=True)
|
||||
os.makedirs(self.temp_dir, exist_ok=True)
|
||||
os.makedirs(self.encrypted_models_dir, exist_ok=True)
|
||||
|
||||
# 存储上传状态
|
||||
self.upload_sessions = {}
|
||||
self.lock = threading.Lock()
|
||||
|
||||
# 清理过期的上传会话(每10分钟)
|
||||
self._start_cleanup_thread()
|
||||
|
||||
def _start_cleanup_thread(self):
|
||||
"""启动清理线程"""
|
||||
|
||||
def cleanup():
|
||||
while True:
|
||||
try:
|
||||
self.cleanup_expired_sessions()
|
||||
time.sleep(600) # 每10分钟清理一次
|
||||
except Exception as e:
|
||||
logger.error(f"清理上传会话失败: {str(e)}")
|
||||
time.sleep(60)
|
||||
|
||||
thread = threading.Thread(target=cleanup, daemon=True)
|
||||
thread.start()
|
||||
|
||||
def create_upload_session(self, filename, total_size, chunk_size, encryption_key=None):
|
||||
"""创建上传会话"""
|
||||
try:
|
||||
# 生成唯一的session_id
|
||||
session_id = hashlib.md5(f"{filename}_{time.time()}".encode()).hexdigest()
|
||||
|
||||
# 创建临时目录用于存储分片
|
||||
session_dir = os.path.join(self.temp_dir, session_id)
|
||||
os.makedirs(session_dir, exist_ok=True)
|
||||
|
||||
# 计算总分片数
|
||||
total_chunks = (total_size + chunk_size - 1) // chunk_size
|
||||
|
||||
session_info = {
|
||||
'session_id': session_id,
|
||||
'filename': filename,
|
||||
'original_filename': filename,
|
||||
'total_size': total_size,
|
||||
'chunk_size': chunk_size,
|
||||
'total_chunks': total_chunks,
|
||||
'received_chunks': set(),
|
||||
'received_size': 0,
|
||||
'created_at': time.time(),
|
||||
'last_activity': time.time(),
|
||||
'status': 'uploading',
|
||||
'session_dir': session_dir,
|
||||
'encryption_key': encryption_key,
|
||||
'encrypted': encryption_key is not None,
|
||||
'merged_file': None,
|
||||
'encrypted_file': None
|
||||
}
|
||||
|
||||
with self.lock:
|
||||
self.upload_sessions[session_id] = session_info
|
||||
|
||||
logger.info(f"创建上传会话: {session_id}, 文件: {filename}, 总分片: {total_chunks}")
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'session_id': session_id,
|
||||
'total_chunks': total_chunks
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"创建上传会话失败: {str(e)}")
|
||||
return {'success': False, 'error': str(e)}
|
||||
|
||||
def upload_chunk(self, session_id, chunk_index, chunk_data):
|
||||
"""上传分片"""
|
||||
try:
|
||||
with self.lock:
|
||||
if session_id not in self.upload_sessions:
|
||||
return {'success': False, 'error': '会话不存在或已过期'}
|
||||
|
||||
session = self.upload_sessions[session_id]
|
||||
|
||||
# 检查分片索引是否有效
|
||||
if chunk_index < 0 or chunk_index >= session['total_chunks']:
|
||||
return {'success': False, 'error': f'无效的分片索引: {chunk_index}'}
|
||||
|
||||
# 检查分片是否已接收
|
||||
if chunk_index in session['received_chunks']:
|
||||
return {'success': False, 'error': f'分片 {chunk_index} 已接收'}
|
||||
|
||||
# 保存分片
|
||||
chunk_filename = os.path.join(session['session_dir'], f'chunk_{chunk_index:06d}')
|
||||
with open(chunk_filename, 'wb') as f:
|
||||
f.write(chunk_data)
|
||||
|
||||
# 更新会话状态
|
||||
session['received_chunks'].add(chunk_index)
|
||||
session['received_size'] += len(chunk_data)
|
||||
session['last_activity'] = time.time()
|
||||
|
||||
# 计算进度
|
||||
progress = (len(session['received_chunks']) / session['total_chunks']) * 100
|
||||
|
||||
logger.debug(f"上传分片: {session_id} - 分片 {chunk_index}, 进度: {progress:.1f}%")
|
||||
|
||||
# 检查是否所有分片都已上传完成
|
||||
if len(session['received_chunks']) == session['total_chunks']:
|
||||
session['status'] = 'merging'
|
||||
# 启动合并线程
|
||||
threading.Thread(target=self._merge_and_encrypt, args=(session_id,), daemon=True).start()
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'progress': progress,
|
||||
'received_chunks': len(session['received_chunks']),
|
||||
'total_chunks': session['total_chunks']
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"上传分片失败: {str(e)}")
|
||||
return {'success': False, 'error': str(e)}
|
||||
|
||||
def _merge_and_encrypt(self, session_id):
|
||||
"""合并分片并加密"""
|
||||
try:
|
||||
with self.lock:
|
||||
session = self.upload_sessions.get(session_id)
|
||||
if not session:
|
||||
return
|
||||
|
||||
session['status'] = 'merging'
|
||||
logger.info(f"开始合并分片: {session_id}, 文件: {session['filename']}")
|
||||
|
||||
# 合并分片
|
||||
merged_path = os.path.join(session['session_dir'], 'merged_file')
|
||||
with open(merged_path, 'wb') as output:
|
||||
for chunk_idx in range(session['total_chunks']):
|
||||
chunk_file = os.path.join(session['session_dir'], f'chunk_{chunk_idx:06d}')
|
||||
with open(chunk_file, 'rb') as input_chunk:
|
||||
output.write(input_chunk.read())
|
||||
|
||||
# 更新会话状态
|
||||
with self.lock:
|
||||
session['merged_file'] = merged_path
|
||||
session['status'] = 'encrypting'
|
||||
logger.info(f"分片合并完成: {session_id}, 开始加密")
|
||||
|
||||
# 加密文件
|
||||
if session['encryption_key']:
|
||||
encrypted_result = self._encrypt_in_memory(merged_path, session['encryption_key'])
|
||||
|
||||
with self.lock:
|
||||
if encrypted_result['success']:
|
||||
session['encrypted_file'] = encrypted_result['encrypted_path']
|
||||
session['model_hash'] = encrypted_result['model_hash']
|
||||
session['key_hash'] = encrypted_result['key_hash']
|
||||
session['status'] = 'completed'
|
||||
session['last_activity'] = time.time()
|
||||
|
||||
# 清理合并的原始文件(不在磁盘保存)
|
||||
if os.path.exists(merged_path):
|
||||
os.remove(merged_path)
|
||||
|
||||
# 清理分片文件
|
||||
self._cleanup_chunks(session['session_dir'])
|
||||
|
||||
logger.info(f"文件加密完成: {session_id}, 加密文件: {session['encrypted_file']}")
|
||||
else:
|
||||
session['status'] = 'failed'
|
||||
session['error'] = encrypted_result.get('error', '加密失败')
|
||||
logger.error(f"文件加密失败: {session_id}, 错误: {session['error']}")
|
||||
else:
|
||||
# 如果没有提供密钥,直接保存原始文件(不推荐)
|
||||
with self.lock:
|
||||
session['status'] = 'failed'
|
||||
session['error'] = '未提供加密密钥'
|
||||
logger.warning(f"未提供加密密钥: {session_id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"合并加密过程失败: {str(e)}")
|
||||
with self.lock:
|
||||
if session_id in self.upload_sessions:
|
||||
self.upload_sessions[session_id]['status'] = 'failed'
|
||||
self.upload_sessions[session_id]['error'] = str(e)
|
||||
|
||||
def _encrypt_in_memory(self, file_path, password):
|
||||
"""在内存中加密文件"""
|
||||
try:
|
||||
# 读取文件到内存
|
||||
with open(file_path, 'rb') as f:
|
||||
model_data = f.read()
|
||||
|
||||
# 计算模型哈希
|
||||
model_hash = hashlib.sha256(model_data).hexdigest()
|
||||
|
||||
# 生成盐和密钥
|
||||
salt = os.urandom(16)
|
||||
kdf = PBKDF2HMAC(
|
||||
algorithm=hashes.SHA256(),
|
||||
length=32,
|
||||
salt=salt,
|
||||
iterations=100000,
|
||||
)
|
||||
key = base64.urlsafe_b64encode(kdf.derive(password.encode()))
|
||||
key_hash = hashlib.sha256(key).hexdigest()
|
||||
|
||||
# 加密数据
|
||||
fernet = Fernet(key)
|
||||
encrypted_data = fernet.encrypt(model_data)
|
||||
|
||||
# 创建加密数据包
|
||||
encrypted_payload = {
|
||||
'salt': salt,
|
||||
'data': encrypted_data,
|
||||
'model_hash': model_hash,
|
||||
'original_size': len(model_data),
|
||||
'encrypted': True,
|
||||
'version': '2.0', # 新版本标识
|
||||
'created_at': time.time(),
|
||||
'key_hash': key_hash[:16] # 保存密钥哈希的前16位用于验证
|
||||
}
|
||||
|
||||
# 生成加密文件名
|
||||
encrypted_filename = f"{model_hash[:16]}.enc"
|
||||
encrypted_path = os.path.join(self.encrypted_models_dir, encrypted_filename)
|
||||
|
||||
# 保存加密文件
|
||||
with open(encrypted_path, 'wb') as f:
|
||||
pickle.dump(encrypted_payload, f)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'encrypted_path': encrypted_path,
|
||||
'model_hash': model_hash,
|
||||
'key_hash': key_hash,
|
||||
'filename': encrypted_filename
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"内存加密失败: {str(e)}")
|
||||
return {'success': False, 'error': str(e)}
|
||||
|
||||
def _cleanup_chunks(self, session_dir):
|
||||
"""清理分片文件"""
|
||||
try:
|
||||
for item in os.listdir(session_dir):
|
||||
item_path = os.path.join(session_dir, item)
|
||||
if os.path.isfile(item_path):
|
||||
os.remove(item_path)
|
||||
os.rmdir(session_dir)
|
||||
except Exception as e:
|
||||
logger.warning(f"清理分片文件失败: {str(e)}")
|
||||
|
||||
def get_upload_status(self, session_id):
|
||||
"""获取上传状态"""
|
||||
with self.lock:
|
||||
if session_id not in self.upload_sessions:
|
||||
return {'success': False, 'error': '会话不存在'}
|
||||
|
||||
session = self.upload_sessions[session_id]
|
||||
|
||||
# 构建返回数据
|
||||
result = {
|
||||
'session_id': session_id,
|
||||
'filename': session['filename'],
|
||||
'status': session['status'],
|
||||
'progress': (len(session['received_chunks']) / session['total_chunks']) * 100,
|
||||
'received_chunks': len(session['received_chunks']),
|
||||
'total_chunks': session['total_chunks'],
|
||||
'received_size': session['received_size'],
|
||||
'total_size': session['total_size'],
|
||||
'encrypted': session['encrypted'],
|
||||
'created_at': session['created_at'],
|
||||
'last_activity': session['last_activity']
|
||||
}
|
||||
|
||||
if session['status'] == 'completed':
|
||||
result['encrypted_file'] = session['encrypted_file']
|
||||
result['model_hash'] = session.get('model_hash')
|
||||
result['key_hash'] = session.get('key_hash')
|
||||
result['relative_path'] = os.path.basename(session['encrypted_file'])
|
||||
elif session['status'] == 'failed':
|
||||
result['error'] = session.get('error', '未知错误')
|
||||
|
||||
return {'success': True, 'data': result}
|
||||
|
||||
def cleanup_expired_sessions(self, expire_hours=24):
|
||||
"""清理过期的上传会话"""
|
||||
try:
|
||||
current_time = time.time()
|
||||
expired_sessions = []
|
||||
|
||||
with self.lock:
|
||||
for session_id, session in list(self.upload_sessions.items()):
|
||||
# 清理超过24小时无活动的会话
|
||||
if current_time - session['last_activity'] > expire_hours * 3600:
|
||||
expired_sessions.append(session_id)
|
||||
|
||||
for session_id in expired_sessions:
|
||||
session = self.upload_sessions.pop(session_id)
|
||||
# 清理临时文件
|
||||
if os.path.exists(session['session_dir']):
|
||||
try:
|
||||
self._cleanup_chunks(session['session_dir'])
|
||||
except:
|
||||
pass
|
||||
logger.info(f"清理过期会话: {session_id}")
|
||||
|
||||
return len(expired_sessions)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"清理过期会话失败: {str(e)}")
|
||||
return 0
|
||||
|
||||
|
||||
# 全局上传管理器实例
|
||||
_upload_manager = None
|
||||
|
||||
|
||||
def get_upload_manager(config=None):
|
||||
"""获取上传管理器单例"""
|
||||
global _upload_manager
|
||||
if _upload_manager is None:
|
||||
if config is None:
|
||||
config = {
|
||||
'uploads_dir': 'uploads',
|
||||
'temp_dir': 'temp_uploads',
|
||||
'encrypted_models_dir': 'encrypted_models'
|
||||
}
|
||||
_upload_manager = ChunkedUploadManager(config)
|
||||
return _upload_manager
|
||||
|
||||
782
server.py
782
server.py
|
|
@ -1,5 +1,8 @@
|
|||
# server.py
|
||||
import hashlib
|
||||
import os
|
||||
import pickle
|
||||
import secrets
|
||||
from datetime import datetime
|
||||
|
||||
import torch
|
||||
|
|
@ -8,11 +11,13 @@ from flask_socketio import SocketIO
|
|||
from flask_cors import CORS
|
||||
from config import get_default_config
|
||||
from mandatory_model_crypto import MandatoryModelEncryptor
|
||||
from model_upload_manager import get_upload_manager
|
||||
from task_manager import task_manager # 导入任务管理器
|
||||
from global_data import gd
|
||||
from log import logger
|
||||
import time
|
||||
import traceback
|
||||
from mandatory_model_crypto import ModelEncryptionService, validate_models_before_task, verify_single_model_api
|
||||
|
||||
# Flask初始化
|
||||
app = Flask(__name__, static_url_path='/static')
|
||||
|
|
@ -49,6 +54,8 @@ def video_player():
|
|||
return render_template("flv2.html")
|
||||
|
||||
|
||||
# server.py - 修改 create_task 函数
|
||||
|
||||
@app.route('/api/tasks/create', methods=['POST'])
|
||||
def create_task():
|
||||
"""创建新任务 - 强制模型加密和密钥验证"""
|
||||
|
|
@ -73,6 +80,46 @@ def create_task():
|
|||
if len(data['models']) == 0:
|
||||
return jsonify({"status": "error", "message": "models列表不能为空"}), 400
|
||||
|
||||
# ================= 关键修改:创建任务前的模型验证 =================
|
||||
logger.info("开始创建任务前的模型验证...")
|
||||
|
||||
# 1. 检查所有模型是否都有加密密钥
|
||||
for i, model_data in enumerate(data['models']):
|
||||
if 'encryption_key' not in model_data:
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": f"模型 {i} 必须提供encryption_key"
|
||||
}), 400
|
||||
|
||||
# 2. 验证所有模型的密钥
|
||||
task_config = {
|
||||
'models': data['models']
|
||||
}
|
||||
|
||||
validation_result = validate_models_before_task(task_config)
|
||||
|
||||
if not validation_result['success']:
|
||||
logger.error(f"模型验证失败: {validation_result.get('error', '未知错误')}")
|
||||
|
||||
# 提供详细的验证结果
|
||||
error_details = []
|
||||
for result in validation_result.get('validation_results', []):
|
||||
if not result.get('key_valid', False):
|
||||
error_details.append(f"模型 {result['model_index']}: {result.get('error', '验证失败')}")
|
||||
|
||||
error_message = validation_result.get('error', '模型验证失败')
|
||||
if error_details:
|
||||
error_message += f" | 详情: {', '.join(error_details)}"
|
||||
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": error_message,
|
||||
"data": validation_result
|
||||
}), 400
|
||||
|
||||
logger.info(f"模型验证通过: {validation_result['valid_models']}/{validation_result['total_models']} 个模型有效")
|
||||
# ================= 验证结束 =================
|
||||
|
||||
# 更新配置
|
||||
config['rtmp']['url'] = data['rtmp_url']
|
||||
|
||||
|
|
@ -87,37 +134,28 @@ def create_task():
|
|||
if 'AlgoId' in data:
|
||||
config['task']['aiid'] = data['AlgoId']
|
||||
|
||||
# 处理多模型配置 - 强制加密验证
|
||||
# 处理多模型配置 - 使用已验证的模型
|
||||
config['models'] = []
|
||||
encryption_checker = MandatoryModelEncryptor()
|
||||
|
||||
for i, model_data in enumerate(data['models']):
|
||||
# 必须提供加密密钥
|
||||
encryption_key = model_data.get('encryption_key')
|
||||
if not encryption_key:
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": f"模型 {i} ({model_data.get('path', 'unknown')}) 必须提供encryption_key"
|
||||
}), 400
|
||||
# 此时密钥已验证通过
|
||||
encryption_key = model_data['encryption_key']
|
||||
|
||||
model_path = model_data.get('path', f'model_{i}.pt')
|
||||
model_name = os.path.basename(model_path).split('.')[0]
|
||||
# 使用加密模型文件名(相对路径)
|
||||
model_path = model_data.get('path', f'model_{i}.enc')
|
||||
|
||||
# 检查模型文件是否加密(如果是本地文件)
|
||||
local_model_path = os.path.join(os.path.basename(model_path))
|
||||
# 如果本地文件存在,验证加密格式
|
||||
if os.path.exists(local_model_path):
|
||||
if not encryption_checker.is_properly_encrypted(local_model_path):
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": f"模型 {i} ({model_name}) 未正确加密"
|
||||
}), 400
|
||||
# 确保路径是加密模型目录中的文件
|
||||
if not model_path.startswith('encrypted_models/'):
|
||||
model_filename = os.path.basename(model_path)
|
||||
if not model_filename.endswith('.enc'):
|
||||
model_filename += '.enc'
|
||||
model_path = f"encrypted_models/{model_filename}"
|
||||
|
||||
# 构建模型配置
|
||||
model_config = {
|
||||
'path': model_path,
|
||||
'encryption_key': encryption_key, # 必须提供
|
||||
'encrypted': True, # 强制加密
|
||||
'encryption_key': encryption_key,
|
||||
'encrypted': True,
|
||||
'tags': model_data.get('tags', {}),
|
||||
'conf_thres': float(model_data.get('conf_thres', 0.25)),
|
||||
'iou_thres': float(model_data.get('iou_thres', 0.45)),
|
||||
|
|
@ -127,11 +165,11 @@ def create_task():
|
|||
'device': model_data.get('device', 'cuda:0' if torch.cuda.is_available() else 'cpu'),
|
||||
'half': model_data.get('half', True),
|
||||
'enabled': model_data.get('enabled', True),
|
||||
'download_url': model_data.get('download_url') # 可选的下载地址
|
||||
# 注意:不再需要 download_url,模型从本地加载
|
||||
}
|
||||
|
||||
config['models'].append(model_config)
|
||||
logger.info(f"添加加密模型 {i}: {model_name}")
|
||||
logger.info(f"添加已验证的加密模型 {i}: {model_path}")
|
||||
|
||||
# 在创建任务前清理已停止的任务,释放资源
|
||||
logger.info("创建任务前清理已停止的任务...")
|
||||
|
|
@ -167,7 +205,7 @@ def create_task():
|
|||
}), 503
|
||||
|
||||
# 创建任务
|
||||
logger.info(f"开始创建任务,包含 {len(config['models'])} 个加密模型...")
|
||||
logger.info(f"开始创建任务,包含 {len(config['models'])} 个已验证的加密模型...")
|
||||
|
||||
try:
|
||||
task_id = task_manager.create_task(config, socketio)
|
||||
|
|
@ -185,12 +223,21 @@ def create_task():
|
|||
|
||||
if success:
|
||||
logger.info(f"任务启动成功: {task_id}")
|
||||
|
||||
# 获取任务详细信息
|
||||
task_status = task_manager.get_task_status(task_id)
|
||||
|
||||
return jsonify({
|
||||
"status": "success",
|
||||
"message": "任务创建并启动成功",
|
||||
"task_id": task_id,
|
||||
"models_count": len(config['models']),
|
||||
"encryption_required": True
|
||||
"data": {
|
||||
"task_id": task_id,
|
||||
"models_count": len(config['models']),
|
||||
"encryption_required": True,
|
||||
"key_validated": True,
|
||||
"validation_result": validation_result,
|
||||
"task_info": task_status
|
||||
}
|
||||
})
|
||||
else:
|
||||
logger.error(f"任务启动失败: {task_id}")
|
||||
|
|
@ -266,14 +313,6 @@ def encrypt_model():
|
|||
return jsonify({"status": "error", "message": "缺少必要参数"}), 400
|
||||
local_path = os.path.join(config['model_path'], model_path)
|
||||
output_path = os.path.join(config['model_path'], output_path)
|
||||
# 验证输入文件是否存在
|
||||
if not os.path.exists(model_path):
|
||||
from model_crypto import ModelManager
|
||||
model_d = ModelManager(data)
|
||||
down_status = model_d.download_model({"path": model_path, "download_url": download_url})
|
||||
if not down_status:
|
||||
return jsonify({"status": "error", "message": f"模型文件不存在: {model_path}"}), 400
|
||||
|
||||
# 使用强制加密器
|
||||
from mandatory_model_crypto import MandatoryModelEncryptor
|
||||
encryptor = MandatoryModelEncryptor()
|
||||
|
|
@ -741,40 +780,6 @@ def get_task_stream_status(task_id):
|
|||
}), 500
|
||||
|
||||
|
||||
@app.route('/api/tasks/<task_id>/stream/restart', methods=['POST'])
|
||||
def restart_task_stream(task_id):
|
||||
"""重启任务推流"""
|
||||
try:
|
||||
from task_stream_manager import task_stream_manager
|
||||
|
||||
# 检查任务是否存在
|
||||
if task_id not in task_manager.tasks:
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": f"任务不存在: {task_id}"
|
||||
}), 404
|
||||
|
||||
# 重启推流
|
||||
success = task_stream_manager._restart_task_streamer(task_id)
|
||||
|
||||
if success:
|
||||
return jsonify({
|
||||
"status": "success",
|
||||
"message": f"任务推流重启成功: {task_id}"
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": f"重启失败: {task_id}"
|
||||
}), 500
|
||||
except Exception as e:
|
||||
logger.error(f"重启任务推流失败: {str(e)}")
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": f"重启失败: {str(e)}"
|
||||
}), 500
|
||||
|
||||
|
||||
@app.route('/api/system/streams/info', methods=['GET'])
|
||||
def get_all_streams_info():
|
||||
"""获取所有任务推流信息"""
|
||||
|
|
@ -799,10 +804,645 @@ def get_all_streams_info():
|
|||
}), 500
|
||||
|
||||
|
||||
# 在适当位置初始化上传管理器
|
||||
upload_manager = None
|
||||
|
||||
|
||||
def init_upload_manager(config):
|
||||
"""初始化上传管理器"""
|
||||
global upload_manager
|
||||
if not upload_manager:
|
||||
upload_manager = get_upload_manager(config['upload'])
|
||||
return upload_manager
|
||||
|
||||
|
||||
# 添加上传相关路由
|
||||
@app.route('/api/models/upload/start', methods=['POST'])
|
||||
def start_model_upload():
|
||||
"""开始模型文件上传"""
|
||||
try:
|
||||
data = request.json
|
||||
if not data:
|
||||
return jsonify({"status": "error", "message": "请求数据不能为空"}), 400
|
||||
|
||||
filename = data.get('filename')
|
||||
total_size = data.get('total_size')
|
||||
encryption_key = data.get('encryption_key') # 可选,但建议提供
|
||||
|
||||
if not all([filename, total_size]):
|
||||
return jsonify({"status": "error", "message": "缺少必要参数"}), 400
|
||||
|
||||
# 初始化上传管理器
|
||||
config = get_default_config()
|
||||
upload_mgr = init_upload_manager(config)
|
||||
|
||||
# 获取分片大小配置
|
||||
chunk_size = config['upload'].get('chunk_size', 5 * 1024 * 1024)
|
||||
|
||||
# 创建上传会话
|
||||
result = upload_mgr.create_upload_session(
|
||||
filename=filename,
|
||||
total_size=total_size,
|
||||
chunk_size=chunk_size,
|
||||
encryption_key=encryption_key
|
||||
)
|
||||
|
||||
if result['success']:
|
||||
return jsonify({
|
||||
"status": "success",
|
||||
"message": "上传会话创建成功",
|
||||
"data": {
|
||||
"session_id": result['session_id'],
|
||||
"total_chunks": result['total_chunks'],
|
||||
"chunk_size": chunk_size
|
||||
}
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": f"创建上传会话失败: {result.get('error', '未知错误')}"
|
||||
}), 500
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"开始上传失败: {str(e)}")
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": f"开始上传失败: {str(e)}"
|
||||
}), 500
|
||||
|
||||
|
||||
@app.route('/api/models/upload/chunk', methods=['POST'])
|
||||
def upload_model_chunk():
|
||||
"""上传模型文件分片"""
|
||||
try:
|
||||
# 获取表单数据
|
||||
session_id = request.form.get('session_id')
|
||||
chunk_index = int(request.form.get('chunk_index', 0))
|
||||
|
||||
if not session_id:
|
||||
return jsonify({"status": "error", "message": "缺少session_id"}), 400
|
||||
|
||||
# 获取文件数据
|
||||
if 'chunk' not in request.files:
|
||||
return jsonify({"status": "error", "message": "未找到文件分片"}), 400
|
||||
|
||||
chunk_file = request.files['chunk']
|
||||
chunk_data = chunk_file.read()
|
||||
|
||||
# 获取上传管理器
|
||||
config = get_default_config()
|
||||
upload_mgr = init_upload_manager(config)
|
||||
|
||||
# 上传分片
|
||||
result = upload_mgr.upload_chunk(session_id, chunk_index, chunk_data)
|
||||
|
||||
if result['success']:
|
||||
return jsonify({
|
||||
"status": "success",
|
||||
"message": "分片上传成功",
|
||||
"data": {
|
||||
"progress": result['progress'],
|
||||
"received_chunks": result['received_chunks'],
|
||||
"total_chunks": result['total_chunks'],
|
||||
"chunk_index": chunk_index
|
||||
}
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": f"分片上传失败: {result.get('error', '未知错误')}"
|
||||
}), 500
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"上传分片失败: {str(e)}")
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": f"上传分片失败: {str(e)}"
|
||||
}), 500
|
||||
|
||||
|
||||
@app.route('/api/models/upload/status/<session_id>', methods=['GET'])
|
||||
def get_upload_status(session_id):
|
||||
"""获取上传状态"""
|
||||
try:
|
||||
config = get_default_config()
|
||||
upload_mgr = init_upload_manager(config)
|
||||
|
||||
result = upload_mgr.get_upload_status(session_id)
|
||||
|
||||
if result['success']:
|
||||
return jsonify({
|
||||
"status": "success",
|
||||
"data": result['data']
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": result.get('error', '获取状态失败')
|
||||
}), 404
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取上传状态失败: {str(e)}")
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": f"获取上传状态失败: {str(e)}"
|
||||
}), 500
|
||||
|
||||
|
||||
@app.route('/api/models/upload/cancel/<session_id>', methods=['POST'])
|
||||
def cancel_upload(session_id):
|
||||
"""取消上传"""
|
||||
try:
|
||||
# 实现取消逻辑
|
||||
config = get_default_config()
|
||||
upload_mgr = init_upload_manager(config)
|
||||
|
||||
# 这里需要在上传管理器中添加取消功能
|
||||
# upload_mgr.cancel_upload(session_id)
|
||||
|
||||
return jsonify({
|
||||
"status": "success",
|
||||
"message": f"上传已取消: {session_id}"
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"取消上传失败: {str(e)}")
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": f"取消上传失败: {str(e)}"
|
||||
}), 500
|
||||
|
||||
|
||||
@app.route('/api/models/list', methods=['GET'])
|
||||
def list_encrypted_models():
|
||||
"""列出所有已加密的模型文件"""
|
||||
try:
|
||||
config = get_default_config()
|
||||
encrypted_dir = config['upload']['encrypted_models_dir']
|
||||
|
||||
if not os.path.exists(encrypted_dir):
|
||||
return jsonify({
|
||||
"status": "success",
|
||||
"data": {
|
||||
"models": [],
|
||||
"total": 0
|
||||
}
|
||||
})
|
||||
|
||||
models = []
|
||||
for filename in os.listdir(encrypted_dir):
|
||||
if filename.endswith('.enc'):
|
||||
filepath = os.path.join(encrypted_dir, filename)
|
||||
stats = os.stat(filepath)
|
||||
models.append({
|
||||
'filename': filename,
|
||||
'path': filepath,
|
||||
'size': stats.st_size,
|
||||
'modified': stats.st_mtime,
|
||||
'encrypted': True
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
"status": "success",
|
||||
"data": {
|
||||
"models": models,
|
||||
"total": len(models),
|
||||
"encrypted_dir": encrypted_dir
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"列出模型失败: {str(e)}")
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": f"列出模型失败: {str(e)}"
|
||||
}), 500
|
||||
|
||||
|
||||
# 在 server.py 中添加上传页面路由
|
||||
@app.route('/model_upload')
|
||||
def model_upload_page():
|
||||
"""模型上传页面"""
|
||||
return render_template("model_upload.html")
|
||||
|
||||
|
||||
# server.py - 添加以下路由
|
||||
|
||||
from mandatory_model_crypto import ModelEncryptionService, validate_models_before_task, verify_single_model_api
|
||||
|
||||
|
||||
@app.route('/api/models/process/start', methods=['POST'])
|
||||
def start_model_processing():
|
||||
"""开始模型处理流程:生成密钥 -> 上传模型"""
|
||||
try:
|
||||
data = request.json
|
||||
|
||||
# 选项1:客户端提供自己的密钥
|
||||
client_key = data.get('encryption_key')
|
||||
|
||||
# 选项2:服务器生成密钥
|
||||
generate_new = data.get('generate_key', False)
|
||||
|
||||
response_data = {}
|
||||
|
||||
if generate_new and not client_key:
|
||||
# 服务器生成新密钥
|
||||
key_info = ModelEncryptionService.generate_secure_key()
|
||||
response_data['key_info'] = {
|
||||
'key': key_info['key'],
|
||||
'key_hash': key_info['key_hash'],
|
||||
'short_hash': key_info['short_hash'],
|
||||
'generated_by': 'server'
|
||||
}
|
||||
|
||||
elif client_key:
|
||||
# 验证客户端提供的密钥
|
||||
key_valid, key_msg = ModelEncryptionService.validate_key_strength(client_key)
|
||||
if not key_valid:
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": f"密钥强度不足: {key_msg}"
|
||||
}), 400
|
||||
|
||||
response_data['key_info'] = {
|
||||
'key': client_key,
|
||||
'key_hash': hashlib.sha256(client_key.encode()).hexdigest(),
|
||||
'short_hash': hashlib.sha256(client_key.encode()).hexdigest()[:16],
|
||||
'generated_by': 'client'
|
||||
}
|
||||
else:
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": "请提供加密密钥或选择生成新密钥"
|
||||
}), 400
|
||||
|
||||
# 生成上传令牌
|
||||
upload_token = secrets.token_urlsafe(32)
|
||||
response_data['upload_token'] = upload_token
|
||||
response_data['token_expires'] = time.time() + 3600 # 1小时有效期
|
||||
|
||||
# 存储上传会话(简化版,生产环境应使用数据库)
|
||||
upload_sessions = gd.get_or_create_dict('upload_sessions')
|
||||
upload_sessions[upload_token] = {
|
||||
'key_info': response_data['key_info'],
|
||||
'created_at': time.time(),
|
||||
'status': 'pending'
|
||||
}
|
||||
|
||||
return jsonify({
|
||||
"status": "success",
|
||||
"message": "模型处理流程已启动",
|
||||
"data": response_data
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"启动模型处理流程失败: {str(e)}")
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": f"启动流程失败: {str(e)}"
|
||||
}), 500
|
||||
|
||||
|
||||
@app.route('/api/models/process/verify_key', methods=['POST'])
|
||||
def verify_encryption_key():
|
||||
"""验证加密密钥"""
|
||||
try:
|
||||
data = request.json
|
||||
encryption_key = data.get('encryption_key')
|
||||
model_path = data.get('model_path') # 可选,如果有具体模型
|
||||
|
||||
if not encryption_key:
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": "请提供加密密钥"
|
||||
}), 400
|
||||
|
||||
# 验证密钥强度
|
||||
key_valid, key_msg = ModelEncryptionService.validate_key_strength(encryption_key)
|
||||
|
||||
if not key_valid:
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": key_msg,
|
||||
"data": {
|
||||
"valid": False,
|
||||
"strength": "weak"
|
||||
}
|
||||
}), 400
|
||||
|
||||
response_data = {
|
||||
"valid": True,
|
||||
"strength": "strong",
|
||||
"key_hash": hashlib.sha256(encryption_key.encode()).hexdigest()[:16]
|
||||
}
|
||||
|
||||
# 如果有具体模型,尝试解密验证
|
||||
if model_path:
|
||||
verify_result = verify_single_model_api(model_path, encryption_key)
|
||||
response_data['model_verification'] = verify_result
|
||||
|
||||
return jsonify({
|
||||
"status": "success",
|
||||
"message": "密钥验证成功",
|
||||
"data": response_data
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"验证密钥失败: {str(e)}")
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": f"验证失败: {str(e)}"
|
||||
}), 500
|
||||
|
||||
|
||||
@app.route('/api/models/process/validate_task', methods=['POST'])
|
||||
def validate_task_models():
|
||||
"""创建任务前的模型验证"""
|
||||
try:
|
||||
data = request.json
|
||||
|
||||
if not data:
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": "请求数据不能为空"
|
||||
}), 400
|
||||
|
||||
# 提取任务配置
|
||||
task_config = {
|
||||
'models': data.get('models', [])
|
||||
}
|
||||
|
||||
# 验证所有模型
|
||||
validation_result = validate_models_before_task(task_config)
|
||||
|
||||
if validation_result['success']:
|
||||
return jsonify({
|
||||
"status": "success",
|
||||
"message": f"模型验证通过 ({validation_result['valid_models']}/{validation_result['total_models']})",
|
||||
"data": validation_result
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": f"模型验证失败: {validation_result.get('error', '未知错误')}",
|
||||
"data": validation_result
|
||||
}), 400
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"验证任务模型失败: {str(e)}")
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": f"验证失败: {str(e)}"
|
||||
}), 500
|
||||
|
||||
|
||||
@app.route('/api/models/encrypted/list_available', methods=['GET'])
|
||||
def list_available_encrypted_models():
|
||||
"""列出可用的加密模型(用于任务创建选择)"""
|
||||
try:
|
||||
config = get_default_config()
|
||||
encrypted_dir = config['upload']['encrypted_models_dir']
|
||||
|
||||
if not os.path.exists(encrypted_dir):
|
||||
return jsonify({
|
||||
"status": "success",
|
||||
"data": {
|
||||
"models": [],
|
||||
"total": 0
|
||||
}
|
||||
})
|
||||
|
||||
models = []
|
||||
for filename in os.listdir(encrypted_dir):
|
||||
if filename.endswith('.enc'):
|
||||
filepath = os.path.join(encrypted_dir, filename)
|
||||
stats = os.stat(filepath)
|
||||
|
||||
# 尝试读取模型基本信息(不验证密钥)
|
||||
try:
|
||||
with open(filepath, 'rb') as f:
|
||||
encrypted_data = pickle.load(f)
|
||||
|
||||
model_info = {
|
||||
'filename': filename,
|
||||
'path': f"encrypted_models/{filename}", # 相对路径
|
||||
'size': stats.st_size,
|
||||
'modified': stats.st_mtime,
|
||||
'encrypted': True,
|
||||
'model_hash': encrypted_data.get('model_hash', '')[:16] if isinstance(encrypted_data,
|
||||
dict) else '',
|
||||
'version': encrypted_data.get('version', 'unknown') if isinstance(encrypted_data, dict) else '',
|
||||
'original_size': encrypted_data.get('original_size', 0) if isinstance(encrypted_data,
|
||||
dict) else 0
|
||||
}
|
||||
|
||||
models.append(model_info)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"读取模型信息失败 {filename}: {str(e)}")
|
||||
models.append({
|
||||
'filename': filename,
|
||||
'path': f"encrypted_models/{filename}",
|
||||
'size': stats.st_size,
|
||||
'modified': stats.st_mtime,
|
||||
'encrypted': True,
|
||||
'error': '无法读取模型信息'
|
||||
})
|
||||
|
||||
# 按修改时间排序
|
||||
models.sort(key=lambda x: x['modified'], reverse=True)
|
||||
|
||||
return jsonify({
|
||||
"status": "success",
|
||||
"data": {
|
||||
"models": models,
|
||||
"total": len(models),
|
||||
"directory": encrypted_dir
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"列出可用模型失败: {str(e)}")
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": f"列出模型失败: {str(e)}"
|
||||
}), 500
|
||||
|
||||
|
||||
@app.route('/api/models/process/test_decrypt', methods=['POST'])
|
||||
def test_model_decryption():
|
||||
"""测试模型解密(不实际加载YOLO模型)"""
|
||||
try:
|
||||
data = request.json
|
||||
model_path = data.get('model_path')
|
||||
encryption_key = data.get('encryption_key')
|
||||
|
||||
if not all([model_path, encryption_key]):
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": "请提供模型路径和加密密钥"
|
||||
}), 400
|
||||
|
||||
# 构建完整路径
|
||||
config = get_default_config()
|
||||
encrypted_dir = config['upload']['encrypted_models_dir']
|
||||
|
||||
if not os.path.isabs(model_path):
|
||||
model_filename = os.path.basename(model_path)
|
||||
full_model_path = os.path.join(encrypted_dir, model_filename)
|
||||
else:
|
||||
full_model_path = model_path
|
||||
|
||||
# 检查文件是否存在
|
||||
if not os.path.exists(full_model_path):
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": f"模型文件不存在: {full_model_path}"
|
||||
}), 404
|
||||
|
||||
# 测试解密
|
||||
from mandatory_model_crypto import MandatoryModelValidator
|
||||
validator = MandatoryModelValidator()
|
||||
|
||||
start_time = time.time()
|
||||
decrypt_result = validator.decrypt_and_verify(full_model_path, encryption_key)
|
||||
elapsed_time = time.time() - start_time
|
||||
|
||||
if decrypt_result['success']:
|
||||
return jsonify({
|
||||
"status": "success",
|
||||
"message": "解密测试成功",
|
||||
"data": {
|
||||
'success': True,
|
||||
'model_hash': decrypt_result.get('model_hash', '')[:16],
|
||||
'model_size': decrypt_result.get('original_size', 0),
|
||||
'decryption_time': elapsed_time,
|
||||
'file_path': full_model_path
|
||||
}
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": f"解密测试失败: {decrypt_result.get('error', '未知错误')}",
|
||||
"data": {
|
||||
'success': False,
|
||||
'error': decrypt_result.get('error', '未知错误'),
|
||||
'decryption_time': elapsed_time
|
||||
}
|
||||
}), 400
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"测试解密失败: {str(e)}")
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": f"测试失败: {str(e)}"
|
||||
}), 500
|
||||
|
||||
|
||||
# server.py - 添加任务创建页面路由
|
||||
@app.route('/task_create')
|
||||
def task_create_page():
|
||||
"""任务创建页面"""
|
||||
return render_template("task_create.html")
|
||||
|
||||
|
||||
# server.py - 添加资源检查接口
|
||||
|
||||
@app.route('/api/system/check_resources', methods=['GET'])
|
||||
def check_system_resources():
|
||||
"""检查系统资源是否足够创建新任务"""
|
||||
try:
|
||||
import psutil
|
||||
import torch
|
||||
|
||||
# 获取系统资源
|
||||
cpu_percent = psutil.cpu_percent(interval=0.1)
|
||||
memory_info = psutil.virtual_memory()
|
||||
memory_percent = memory_info.percent
|
||||
|
||||
# GPU信息
|
||||
gpu_info = []
|
||||
gpu_available = torch.cuda.is_available()
|
||||
if gpu_available:
|
||||
for i in range(torch.cuda.device_count()):
|
||||
gpu_memory_used = torch.cuda.memory_allocated(i) / 1024 ** 2 # MB
|
||||
gpu_memory_total = torch.cuda.get_device_properties(i).total_memory / 1024 ** 2 # MB
|
||||
gpu_memory_percent = (gpu_memory_used / gpu_memory_total) * 100 if gpu_memory_total > 0 else 0
|
||||
|
||||
gpu_info.append({
|
||||
'id': i,
|
||||
'name': torch.cuda.get_device_name(i),
|
||||
'memory_used': gpu_memory_used,
|
||||
'memory_total': gpu_memory_total,
|
||||
'memory_percent': gpu_memory_percent
|
||||
})
|
||||
|
||||
# 获取当前任务信息
|
||||
active_tasks = task_manager.get_active_tasks_count()
|
||||
max_tasks = task_manager.get_current_max_tasks()
|
||||
|
||||
# 资源阈值
|
||||
cpu_threshold = 80 # CPU使用率阈值
|
||||
memory_threshold = 85 # 内存使用率阈值
|
||||
gpu_memory_threshold = 90 # GPU内存使用率阈值
|
||||
|
||||
# 检查资源状态
|
||||
resources_ok = True
|
||||
warnings = []
|
||||
|
||||
if cpu_percent > cpu_threshold:
|
||||
resources_ok = False
|
||||
warnings.append(f"CPU使用率过高: {cpu_percent:.1f}% > {cpu_threshold}%")
|
||||
|
||||
if memory_percent > memory_threshold:
|
||||
resources_ok = False
|
||||
warnings.append(f"内存使用率过高: {memory_percent:.1f}% > {memory_threshold}%")
|
||||
|
||||
if gpu_available and gpu_info:
|
||||
max_gpu_memory = max(gpu['memory_percent'] for gpu in gpu_info)
|
||||
if max_gpu_memory > gpu_memory_threshold:
|
||||
warnings.append(f"GPU内存使用率过高: {max_gpu_memory:.1f}% > {gpu_memory_threshold}%")
|
||||
# GPU内存高不是致命错误,只是警告
|
||||
|
||||
if active_tasks >= max_tasks:
|
||||
resources_ok = False
|
||||
warnings.append(f"任务数达到上限: {active_tasks}/{max_tasks}")
|
||||
|
||||
return jsonify({
|
||||
"status": "success",
|
||||
"data": {
|
||||
"resources_available": resources_ok,
|
||||
"active_tasks": active_tasks,
|
||||
"max_tasks": max_tasks,
|
||||
"slots_available": max(0, max_tasks - active_tasks),
|
||||
"cpu_percent": cpu_percent,
|
||||
"memory_percent": memory_percent,
|
||||
"memory_used": memory_info.used / 1024 ** 2, # MB
|
||||
"memory_total": memory_info.total / 1024 ** 2, # MB
|
||||
"gpu_available": gpu_available,
|
||||
"gpu_info": gpu_info,
|
||||
"warnings": warnings,
|
||||
"thresholds": {
|
||||
"cpu": cpu_threshold,
|
||||
"memory": memory_threshold,
|
||||
"gpu_memory": gpu_memory_threshold
|
||||
},
|
||||
"timestamp": time.time()
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"检查系统资源失败: {str(e)}")
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": f"检查资源失败: {str(e)}"
|
||||
}), 500
|
||||
|
||||
|
||||
# 初始化函数,可在主程序中调用
|
||||
def init_app():
|
||||
"""初始化应用程序"""
|
||||
with app.app_context():
|
||||
gd.set_value('task_manager', task_manager)
|
||||
logger.info("任务管理器初始化完成")
|
||||
return app
|
||||
return app
|
||||
|
|
|
|||
|
|
@ -116,6 +116,8 @@ class TaskManager:
|
|||
logger.error(f"检查任务创建条件失败: {str(e)}")
|
||||
return False # 出错时保守拒绝
|
||||
|
||||
# task_manager.py - 修改 create_task 方法
|
||||
|
||||
def create_task(self, config, socketio):
|
||||
"""创建新任务 - 强制加密验证"""
|
||||
try:
|
||||
|
|
@ -131,11 +133,23 @@ class TaskManager:
|
|||
if 'models' not in config or not isinstance(config['models'], list):
|
||||
raise Exception("配置必须包含models列表")
|
||||
|
||||
# ============ 增强验证 ============
|
||||
# 验证所有模型都有加密密钥
|
||||
for i, model_cfg in enumerate(config['models']):
|
||||
if not model_cfg.get('encryption_key'):
|
||||
raise Exception(f"模型 {i} ({model_cfg.get('path', 'unknown')}) 必须提供加密密钥")
|
||||
|
||||
# 验证模型路径是否指向加密模型目录
|
||||
model_path = model_cfg.get('path', '')
|
||||
if not model_path.startswith('encrypted_models/') and not os.path.isabs(model_path):
|
||||
# 尝试在加密目录中查找
|
||||
model_filename = os.path.basename(model_path)
|
||||
if not model_filename.endswith('.enc'):
|
||||
model_filename += '.enc'
|
||||
|
||||
# 更新配置中的路径
|
||||
model_cfg['path'] = f"encrypted_models/{model_filename}"
|
||||
|
||||
# 准备任务信息
|
||||
task_info = {
|
||||
'task_id': task_id,
|
||||
|
|
@ -152,9 +166,16 @@ class TaskManager:
|
|||
'start_time': time.time(),
|
||||
'models_loaded': len(config['models']),
|
||||
'encrypted_models': len(config['models']), # 所有模型都加密
|
||||
'key_validation_required': True
|
||||
'key_validation_required': True,
|
||||
'key_validated': True, # 创建时已验证
|
||||
'validation_time': time.time()
|
||||
},
|
||||
'key_validation': {} # 存储密钥验证结果
|
||||
'key_validation': {
|
||||
'required': True,
|
||||
'validated': True,
|
||||
'validated_at': time.time(),
|
||||
'models_count': len(config['models'])
|
||||
}
|
||||
}
|
||||
|
||||
# 更新配置中的任务ID
|
||||
|
|
@ -164,7 +185,7 @@ class TaskManager:
|
|||
self.tasks[task_id] = task_info
|
||||
gd.get_or_create_dict('tasks')[task_id] = task_info
|
||||
|
||||
logger.info(f"创建加密任务成功: {task_id}, 加密模型数: {len(config['models'])}")
|
||||
logger.info(f"创建加密任务成功: {task_id}, 加密模型数: {len(config['models'])}, 密钥已验证")
|
||||
return task_id
|
||||
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,444 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>模型文件上传</title>
|
||||
<meta charset="UTF-8">
|
||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.upload-section {
|
||||
border: 2px dashed #ccc;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.upload-section.dragover {
|
||||
border-color: #007bff;
|
||||
background-color: #f0f8ff;
|
||||
}
|
||||
.progress-container {
|
||||
margin-top: 20px;
|
||||
display: none;
|
||||
}
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background-color: #007bff;
|
||||
width: 0%;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
.status-info {
|
||||
margin-top: 10px;
|
||||
color: #666;
|
||||
}
|
||||
.upload-list {
|
||||
margin-top: 30px;
|
||||
}
|
||||
.model-item {
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
}
|
||||
.btn:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
.btn:disabled {
|
||||
background-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
input[type="text"], input[type="password"] {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.error {
|
||||
color: #dc3545;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.success {
|
||||
color: #28a745;
|
||||
margin-top: 5px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>加密模型文件上传</h1>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="encryptionKey">加密密钥(可选,建议使用)</label>
|
||||
<input type="password" id="encryptionKey" placeholder="输入加密密钥(如果不提供将自动生成)">
|
||||
<button onclick="generateKey()" class="btn">生成密钥</button>
|
||||
<div id="keyInfo" class="success" style="display: none;"></div>
|
||||
</div>
|
||||
|
||||
<div class="upload-section" id="dropArea">
|
||||
<h3>拖放模型文件到此处,或点击选择文件</h3>
|
||||
<p>支持 .pt, .pth, .onnx, .engine 格式,最大 1GB</p>
|
||||
<input type="file" id="fileInput" style="display: none;">
|
||||
<button onclick="document.getElementById('fileInput').click()" class="btn">选择文件</button>
|
||||
<div id="fileInfo" style="margin-top: 10px;"></div>
|
||||
</div>
|
||||
|
||||
<div class="progress-container" id="progressContainer">
|
||||
<h3>上传进度</h3>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" id="progressFill"></div>
|
||||
</div>
|
||||
<div class="status-info">
|
||||
<span id="progressText">0%</span>
|
||||
<span id="chunkInfo">(0/0 分片)</span>
|
||||
</div>
|
||||
<div id="uploadStatus" class="status-info"></div>
|
||||
<button onclick="cancelUpload()" class="btn" id="cancelBtn" style="display: none; margin-top: 10px;">取消上传</button>
|
||||
</div>
|
||||
|
||||
<div class="upload-list">
|
||||
<h3>已上传的模型文件</h3>
|
||||
<button onclick="loadModels()" class="btn">刷新列表</button>
|
||||
<div id="modelList"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentSessionId = null;
|
||||
let currentFile = null;
|
||||
let encryptionKey = '';
|
||||
let uploadInProgress = false;
|
||||
|
||||
// 元素引用
|
||||
const dropArea = document.getElementById('dropArea');
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
const progressContainer = document.getElementById('progressContainer');
|
||||
const progressFill = document.getElementById('progressFill');
|
||||
const progressText = document.getElementById('progressText');
|
||||
const chunkInfo = document.getElementById('chunkInfo');
|
||||
const uploadStatus = document.getElementById('uploadStatus');
|
||||
const cancelBtn = document.getElementById('cancelBtn');
|
||||
const fileInfo = document.getElementById('fileInfo');
|
||||
const keyInfo = document.getElementById('keyInfo');
|
||||
const modelList = document.getElementById('modelList');
|
||||
const encryptionKeyInput = document.getElementById('encryptionKey');
|
||||
|
||||
// 生成加密密钥
|
||||
async function generateKey() {
|
||||
try {
|
||||
const response = await axios.post('/api/models/generate_key');
|
||||
if (response.data.status === 'success') {
|
||||
const key = response.data.data.key;
|
||||
encryptionKeyInput.value = key;
|
||||
keyInfo.textContent = `已生成密钥: ${key.substring(0, 20)}...`;
|
||||
keyInfo.style.display = 'block';
|
||||
keyInfo.className = 'success';
|
||||
}
|
||||
} catch (error) {
|
||||
keyInfo.textContent = '生成密钥失败: ' + (error.response?.data?.message || error.message);
|
||||
keyInfo.style.display = 'block';
|
||||
keyInfo.className = 'error';
|
||||
}
|
||||
}
|
||||
|
||||
// 拖放功能
|
||||
dropArea.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropArea.classList.add('dragover');
|
||||
});
|
||||
|
||||
dropArea.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dropArea.classList.remove('dragover');
|
||||
});
|
||||
|
||||
dropArea.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropArea.classList.remove('dragover');
|
||||
|
||||
if (e.dataTransfer.files.length > 0) {
|
||||
handleFileSelect(e.dataTransfer.files[0]);
|
||||
}
|
||||
});
|
||||
|
||||
// 文件选择
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
if (e.target.files.length > 0) {
|
||||
handleFileSelect(e.target.files[0]);
|
||||
}
|
||||
});
|
||||
|
||||
// 处理文件选择
|
||||
function handleFileSelect(file) {
|
||||
if (uploadInProgress) {
|
||||
alert('请等待当前上传完成');
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查文件扩展名
|
||||
const allowedExtensions = ['.pt', '.pth', '.onnx', '.engine'];
|
||||
const fileExt = '.' + file.name.split('.').pop().toLowerCase();
|
||||
if (!allowedExtensions.includes(fileExt)) {
|
||||
alert('不支持的文件格式,请上传模型文件');
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查文件大小
|
||||
const maxSize = 1024 * 1024 * 1024; // 1GB
|
||||
if (file.size > maxSize) {
|
||||
alert('文件太大,最大支持1GB');
|
||||
return;
|
||||
}
|
||||
|
||||
currentFile = file;
|
||||
encryptionKey = encryptionKeyInput.value.trim();
|
||||
|
||||
fileInfo.innerHTML = `
|
||||
<div class="success">
|
||||
<strong>已选择文件:</strong> ${file.name}<br>
|
||||
<strong>文件大小:</strong> ${formatFileSize(file.size)}<br>
|
||||
<strong>加密密钥:</strong> ${encryptionKey ? '已提供' : '未提供(建议提供)'}
|
||||
</div>
|
||||
<button onclick="startUpload()" class="btn" style="margin-top: 10px;">开始上传</button>
|
||||
`;
|
||||
}
|
||||
|
||||
// 开始上传
|
||||
async function startUpload() {
|
||||
if (!currentFile) {
|
||||
alert('请先选择文件');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
uploadInProgress = true;
|
||||
|
||||
// 1. 创建上传会话
|
||||
const startResponse = await axios.post('/api/models/upload/start', {
|
||||
filename: currentFile.name,
|
||||
total_size: currentFile.size,
|
||||
encryption_key: encryptionKey || null
|
||||
});
|
||||
|
||||
if (startResponse.data.status !== 'success') {
|
||||
throw new Error(startResponse.data.message);
|
||||
}
|
||||
|
||||
const { session_id, total_chunks, chunk_size } = startResponse.data.data;
|
||||
currentSessionId = session_id;
|
||||
|
||||
// 显示进度条
|
||||
progressContainer.style.display = 'block';
|
||||
cancelBtn.style.display = 'block';
|
||||
|
||||
// 2. 分片上传
|
||||
for (let chunkIndex = 0; chunkIndex < total_chunks; chunkIndex++) {
|
||||
if (!uploadInProgress) {
|
||||
break; // 用户取消了上传
|
||||
}
|
||||
|
||||
const start = chunkIndex * chunk_size;
|
||||
const end = Math.min(start + chunk_size, currentFile.size);
|
||||
const chunk = currentFile.slice(start, end);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('session_id', session_id);
|
||||
formData.append('chunk_index', chunkIndex);
|
||||
formData.append('chunk', chunk);
|
||||
|
||||
try {
|
||||
const chunkResponse = await axios.post('/api/models/upload/chunk', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
onUploadProgress: (progressEvent) => {
|
||||
const chunkProgress = Math.round((progressEvent.loaded / progressEvent.total) * 100);
|
||||
updateProgress(chunkIndex + 1, total_chunks, chunkProgress);
|
||||
}
|
||||
});
|
||||
|
||||
if (chunkResponse.data.status !== 'success') {
|
||||
throw new Error(chunkResponse.data.message);
|
||||
}
|
||||
|
||||
// 更新整体进度
|
||||
const progress = chunkResponse.data.data.progress;
|
||||
updateProgress(chunkIndex + 1, total_chunks, progress);
|
||||
|
||||
} catch (chunkError) {
|
||||
console.error('分片上传失败:', chunkError);
|
||||
uploadStatus.innerHTML = `<div class="error">分片 ${chunkIndex} 上传失败: ${chunkError.message}</div>`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (uploadInProgress) {
|
||||
// 3. 轮询上传状态直到完成
|
||||
await pollUploadStatus(session_id);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('上传失败:', error);
|
||||
uploadStatus.innerHTML = `<div class="error">上传失败: ${error.message}</div>`;
|
||||
} finally {
|
||||
if (!uploadInProgress) {
|
||||
uploadStatus.innerHTML = `<div class="error">上传已取消</div>`;
|
||||
}
|
||||
loadModels(); // 刷新模型列表
|
||||
}
|
||||
}
|
||||
|
||||
// 更新进度显示
|
||||
function updateProgress(currentChunk, totalChunks, progress) {
|
||||
const overallProgress = Math.round(((currentChunk - 1) / totalChunks) * 100 + (progress / totalChunks));
|
||||
progressFill.style.width = overallProgress + '%';
|
||||
progressText.textContent = overallProgress + '%';
|
||||
chunkInfo.textContent = `(${currentChunk}/${totalChunks} 分片)`;
|
||||
}
|
||||
|
||||
// 轮询上传状态
|
||||
async function pollUploadStatus(sessionId) {
|
||||
let attempts = 0;
|
||||
const maxAttempts = 60; // 最多等待5分钟 (60 * 5秒)
|
||||
|
||||
while (attempts < maxAttempts && uploadInProgress) {
|
||||
try {
|
||||
const statusResponse = await axios.get(`/api/models/upload/status/${sessionId}`);
|
||||
|
||||
if (statusResponse.data.status === 'success') {
|
||||
const statusData = statusResponse.data.data;
|
||||
|
||||
if (statusData.status === 'completed') {
|
||||
uploadStatus.innerHTML = `
|
||||
<div class="success">
|
||||
<strong>上传完成!</strong><br>
|
||||
加密文件: ${statusData.relative_path}<br>
|
||||
模型哈希: ${statusData.model_hash}<br>
|
||||
密钥哈希: ${statusData.key_hash}
|
||||
</div>
|
||||
`;
|
||||
currentSessionId = null;
|
||||
uploadInProgress = false;
|
||||
return;
|
||||
} else if (statusData.status === 'failed') {
|
||||
uploadStatus.innerHTML = `<div class="error">上传失败: ${statusData.error}</div>`;
|
||||
currentSessionId = null;
|
||||
uploadInProgress = false;
|
||||
return;
|
||||
} else if (statusData.status === 'merging' || statusData.status === 'encrypting') {
|
||||
uploadStatus.innerHTML = `<div class="success">正在${statusData.status === 'merging' ? '合并' : '加密'}文件...</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
attempts++;
|
||||
await sleep(5000); // 每5秒轮询一次
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取状态失败:', error);
|
||||
await sleep(5000);
|
||||
}
|
||||
}
|
||||
|
||||
if (attempts >= maxAttempts) {
|
||||
uploadStatus.innerHTML = `<div class="error">上传超时</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 取消上传
|
||||
function cancelUpload() {
|
||||
if (currentSessionId && uploadInProgress) {
|
||||
if (confirm('确定要取消上传吗?')) {
|
||||
uploadInProgress = false;
|
||||
axios.post(`/api/models/upload/cancel/${currentSessionId}`);
|
||||
uploadStatus.innerHTML = `<div class="error">上传已取消</div>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 加载模型列表
|
||||
async function loadModels() {
|
||||
try {
|
||||
const response = await axios.get('/api/models/list');
|
||||
if (response.data.status === 'success') {
|
||||
const models = response.data.data.models;
|
||||
|
||||
if (models.length === 0) {
|
||||
modelList.innerHTML = '<p>暂无模型文件</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '<table style="width:100%; border-collapse: collapse;">';
|
||||
html += '<tr><th>文件名</th><th>大小</th><th>修改时间</th><th>操作</th></tr>';
|
||||
|
||||
models.forEach(model => {
|
||||
const modifiedDate = new Date(model.modified * 1000).toLocaleString();
|
||||
html += `
|
||||
<tr class="model-item">
|
||||
<td>${model.filename}</td>
|
||||
<td>${formatFileSize(model.size)}</td>
|
||||
<td>${modifiedDate}</td>
|
||||
<td>
|
||||
<button onclick="copyPath('${model.filename}')" class="btn" style="padding: 5px 10px; font-size: 12px;">复制路径</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
|
||||
html += '</table>';
|
||||
modelList.innerHTML = html;
|
||||
}
|
||||
} catch (error) {
|
||||
modelList.innerHTML = `<div class="error">加载模型列表失败: ${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 辅助函数
|
||||
function formatFileSize(bytes) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function copyPath(filename) {
|
||||
const path = `encrypted_models/${filename}`;
|
||||
navigator.clipboard.writeText(path).then(() => {
|
||||
alert('路径已复制到剪贴板: ' + path);
|
||||
});
|
||||
}
|
||||
|
||||
// 页面加载时加载模型列表
|
||||
window.onload = loadModels;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,896 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>创建AI检测任务</title>
|
||||
<meta charset="UTF-8">
|
||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.container {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
.left-panel {
|
||||
flex: 3;
|
||||
}
|
||||
.right-panel {
|
||||
flex: 2;
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
.card {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
input[type="text"], input[type="password"], select, textarea {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
textarea {
|
||||
min-height: 100px;
|
||||
resize: vertical;
|
||||
}
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
}
|
||||
.btn:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
.btn:disabled {
|
||||
background-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.btn-success {
|
||||
background-color: #28a745;
|
||||
}
|
||||
.btn-success:hover {
|
||||
background-color: #218838;
|
||||
}
|
||||
.btn-danger {
|
||||
background-color: #dc3545;
|
||||
}
|
||||
.btn-danger:hover {
|
||||
background-color: #c82333;
|
||||
}
|
||||
.btn-sm {
|
||||
padding: 5px 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.error {
|
||||
color: #dc3545;
|
||||
font-size: 14px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.success {
|
||||
color: #28a745;
|
||||
font-size: 14px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.warning {
|
||||
color: #ffc107;
|
||||
font-size: 14px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.model-item {
|
||||
border: 1px solid #ddd;
|
||||
padding: 15px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 4px;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
.model-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.model-status {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.status-valid {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
.status-invalid {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
.status-pending {
|
||||
background-color: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
.step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.step-number {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
background-color: #6c757d;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.step-number.active {
|
||||
background-color: #007bff;
|
||||
}
|
||||
.step-number.completed {
|
||||
background-color: #28a745;
|
||||
}
|
||||
.step-content {
|
||||
flex: 1;
|
||||
}
|
||||
.progress-bar {
|
||||
height: 4px;
|
||||
background-color: #e9ecef;
|
||||
border-radius: 2px;
|
||||
margin: 20px 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background-color: #007bff;
|
||||
width: 0%;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
.task-preview {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.task-info-item {
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.task-info-label {
|
||||
font-weight: bold;
|
||||
color: #666;
|
||||
}
|
||||
.model-selector {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
}
|
||||
.model-option {
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
.model-option:hover {
|
||||
background-color: #f0f8ff;
|
||||
}
|
||||
.model-option.selected {
|
||||
background-color: #e7f3ff;
|
||||
border-left: 3px solid #007bff;
|
||||
}
|
||||
.model-details {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.validation-result {
|
||||
padding: 10px;
|
||||
margin-top: 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.validation-success {
|
||||
background-color: #d4edda;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
.validation-error {
|
||||
background-color: #f8d7da;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<h1>创建AI检测任务</h1>
|
||||
|
||||
<!-- 步骤指示器 -->
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" :style="{width: (currentStep / 4 * 100) + '%'}"></div>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<div class="step-number" :class="{'active': currentStep === 1, 'completed': currentStep > 1}">1</div>
|
||||
<div class="step-content">
|
||||
<h3>选择模型</h3>
|
||||
<p v-if="currentStep === 1">选择要使用的加密模型文件</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<div class="step-number" :class="{'active': currentStep === 2, 'completed': currentStep > 2}">2</div>
|
||||
<div class="step-content">
|
||||
<h3>配置密钥</h3>
|
||||
<p v-if="currentStep === 2">为每个模型配置加密密钥</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<div class="step-number" :class="{'active': currentStep === 3, 'completed': currentStep > 3}">3</div>
|
||||
<div class="step-content">
|
||||
<h3>任务设置</h3>
|
||||
<p v-if="currentStep === 3">配置RTMP流和任务参数</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<div class="step-number" :class="{'active': currentStep === 4, 'completed': currentStep > 4}">4</div>
|
||||
<div class="step-content">
|
||||
<h3>验证和创建</h3>
|
||||
<p v-if="currentStep === 4">验证所有配置并创建任务</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<!-- 左侧主内容 -->
|
||||
<div class="left-panel">
|
||||
<!-- 步骤1: 选择模型 -->
|
||||
<div class="card" v-if="currentStep === 1">
|
||||
<h3>选择加密模型</h3>
|
||||
<div class="form-group">
|
||||
<label>可用加密模型:</label>
|
||||
<div class="model-selector">
|
||||
<div v-if="loadingModels">加载模型中...</div>
|
||||
<div v-else-if="availableModels.length === 0">没有找到加密模型文件</div>
|
||||
<div v-for="model in availableModels"
|
||||
:key="model.filename"
|
||||
class="model-option"
|
||||
:class="{'selected': selectedModels.some(m => m.filename === model.filename)}"
|
||||
@click="toggleModelSelection(model)">
|
||||
<div><strong>{{ model.filename }}</strong></div>
|
||||
<div class="model-details">
|
||||
大小: {{ formatFileSize(model.size) }} |
|
||||
哈希: {{ model.model_hash || '未知' }} |
|
||||
修改: {{ formatTimestamp(model.modified) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedModels.length > 0">
|
||||
<h4>已选模型 ({{ selectedModels.length }})</h4>
|
||||
<div v-for="(model, index) in selectedModels" :key="index" class="model-item">
|
||||
<div class="model-header">
|
||||
<div><strong>{{ model.filename }}</strong></div>
|
||||
<button class="btn btn-sm btn-danger" @click="removeModel(index)">移除</button>
|
||||
</div>
|
||||
<div class="model-details">
|
||||
路径: {{ model.path }}<br>
|
||||
模型哈希: {{ model.model_hash || '未知' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<button class="btn" @click="loadAvailableModels" :disabled="loadingModels">
|
||||
{{ loadingModels ? '加载中...' : '刷新列表' }}
|
||||
</button>
|
||||
<button class="btn btn-success" @click="nextStep" :disabled="selectedModels.length === 0">
|
||||
下一步: 配置密钥
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 步骤2: 配置密钥 -->
|
||||
<div class="card" v-if="currentStep === 2">
|
||||
<h3>配置模型加密密钥</h3>
|
||||
<p>为每个选中的模型配置加密密钥。密钥将在创建任务前进行验证。</p>
|
||||
|
||||
<div v-for="(model, index) in selectedModels" :key="index" class="model-item">
|
||||
<div class="model-header">
|
||||
<div>
|
||||
<strong>{{ model.filename }}</strong>
|
||||
<span class="model-status" :class="{
|
||||
'status-valid': model.validation.valid,
|
||||
'status-invalid': model.validation.checked && !model.validation.valid,
|
||||
'status-pending': !model.validation.checked
|
||||
}">
|
||||
{{ model.validation.checked ? (model.validation.valid ? '有效' : '无效') : '待验证' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>加密密钥:</label>
|
||||
<input type="password"
|
||||
v-model="model.encryption_key"
|
||||
placeholder="输入加密密钥"
|
||||
@input="validateSingleModel(index)">
|
||||
|
||||
<div v-if="model.validation.checked" class="validation-result"
|
||||
:class="{'validation-success': model.validation.valid, 'validation-error': !model.validation.valid}">
|
||||
<div v-if="model.validation.valid">
|
||||
✓ 密钥验证成功<br>
|
||||
<small>模型哈希: {{ model.validation.model_hash }}</small>
|
||||
</div>
|
||||
<div v-else>
|
||||
✗ 密钥验证失败: {{ model.validation.error }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<button class="btn btn-sm" @click="testModelDecryption(index)"
|
||||
:disabled="!model.encryption_key">
|
||||
测试解密
|
||||
</button>
|
||||
<button class="btn btn-sm" @click="generateKeyForModel(index)">
|
||||
生成新密钥
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<button class="btn" @click="prevStep">上一步</button>
|
||||
<button class="btn btn-success" @click="validateAllModels"
|
||||
:disabled="!allModelsHaveKeys || validatingAll">
|
||||
{{ validatingAll ? '验证中...' : '验证所有模型' }}
|
||||
</button>
|
||||
<button class="btn" @click="nextStep" :disabled="!allModelsValid">
|
||||
下一步: 任务设置
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 步骤3: 任务设置 -->
|
||||
<div class="card" v-if="currentStep === 3">
|
||||
<h3>任务配置</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<label>任务名称:</label>
|
||||
<input type="text" v-model="taskConfig.taskname" placeholder="输入任务名称">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>RTMP视频流地址:</label>
|
||||
<input type="text" v-model="taskConfig.rtmp_url"
|
||||
placeholder="rtmp://server:port/stream/key">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>推流地址 (可选):</label>
|
||||
<input type="text" v-model="taskConfig.push_url"
|
||||
placeholder="rtmp://server:port/stream/key">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>算法ID (可选):</label>
|
||||
<input type="text" v-model="taskConfig.AlgoId" placeholder="输入算法ID">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>模型配置:</label>
|
||||
<div class="task-preview">
|
||||
<div class="task-info-item">
|
||||
<span class="task-info-label">模型数量:</span>
|
||||
<span>{{ selectedModels.length }}</span>
|
||||
</div>
|
||||
<div class="task-info-item">
|
||||
<span class="task-info-label">已验证模型:</span>
|
||||
<span>{{ validModelsCount }} / {{ selectedModels.length }}</span>
|
||||
</div>
|
||||
<div class="task-info-item">
|
||||
<span class="task-info-label">加密要求:</span>
|
||||
<span>强制加密 ✓</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<button class="btn" @click="prevStep">上一步</button>
|
||||
<button class="btn btn-success" @click="nextStep">
|
||||
下一步: 验证和创建
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 步骤4: 验证和创建 -->
|
||||
<div class="card" v-if="currentStep === 4">
|
||||
<h3>验证并创建任务</h3>
|
||||
|
||||
<div v-if="creatingTask" class="validation-result validation-success">
|
||||
<h4>正在创建任务...</h4>
|
||||
<p>{{ createTaskMessage }}</p>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<h4>配置概览</h4>
|
||||
<div class="task-preview">
|
||||
<div class="task-info-item">
|
||||
<span class="task-info-label">任务名称:</span>
|
||||
<span>{{ taskConfig.taskname }}</span>
|
||||
</div>
|
||||
<div class="task-info-item">
|
||||
<span class="task-info-label">视频流:</span>
|
||||
<span>{{ taskConfig.rtmp_url }}</span>
|
||||
</div>
|
||||
<div class="task-info-item">
|
||||
<span class="task-info-label">推流地址:</span>
|
||||
<span>{{ taskConfig.push_url || '未设置' }}</span>
|
||||
</div>
|
||||
<div class="task-info-item">
|
||||
<span class="task-info-label">模型数量:</span>
|
||||
<span>{{ selectedModels.length }} 个</span>
|
||||
</div>
|
||||
<div class="task-info-item">
|
||||
<span class="task-info-label">密钥验证:</span>
|
||||
<span>{{ validModelsCount === selectedModels.length ? '全部通过 ✓' : '未通过 ✗' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4>最终验证</h4>
|
||||
<div class="form-group">
|
||||
<button class="btn" @click="performFinalValidation"
|
||||
:disabled="validatingFinal || creatingTask">
|
||||
{{ validatingFinal ? '验证中...' : '执行最终验证' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="finalValidationResult" class="validation-result"
|
||||
:class="{'validation-success': finalValidationResult.success, 'validation-error': !finalValidationResult.success}">
|
||||
<h4 v-if="finalValidationResult.success">✓ 验证通过</h4>
|
||||
<h4 v-else>✗ 验证失败</h4>
|
||||
|
||||
<div v-if="finalValidationResult.success">
|
||||
<p>所有模型验证成功,可以创建任务</p>
|
||||
<p>有效模型: {{ finalValidationResult.valid_models }} / {{ finalValidationResult.total_models }}</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p>验证失败: {{ finalValidationResult.error }}</p>
|
||||
<div v-if="finalValidationResult.validation_results">
|
||||
<p>详细错误:</p>
|
||||
<ul>
|
||||
<li v-for="(result, idx) in finalValidationResult.validation_results"
|
||||
v-if="!result.key_valid">
|
||||
模型 {{ result.model_index }}: {{ result.error }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<button class="btn" @click="prevStep">上一步</button>
|
||||
<button class="btn btn-success" @click="createTask"
|
||||
:disabled="!finalValidationResult || !finalValidationResult.success || creatingTask">
|
||||
{{ creatingTask ? '创建中...' : '创建任务' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧信息面板 -->
|
||||
<div class="right-panel">
|
||||
<h3>任务状态</h3>
|
||||
|
||||
<div v-if="currentStep === 1">
|
||||
<p><strong>步骤1/4:</strong> 选择模型</p>
|
||||
<p>请从已上传的加密模型中选择要使用的模型。可以多选。</p>
|
||||
<p><em>提示:</em> 模型文件必须已经通过上传接口加密上传。</p>
|
||||
</div>
|
||||
|
||||
<div v-if="currentStep === 2">
|
||||
<p><strong>步骤2/4:</strong> 配置密钥</p>
|
||||
<p>为每个模型输入加密密钥。密钥强度要求:</p>
|
||||
<ul>
|
||||
<li>长度至少16位</li>
|
||||
<li>包含大小写字母</li>
|
||||
<li>包含数字</li>
|
||||
<li>包含特殊字符</li>
|
||||
</ul>
|
||||
<p>点击"测试解密"可以验证密钥是否正确。</p>
|
||||
</div>
|
||||
|
||||
<div v-if="currentStep === 3">
|
||||
<p><strong>步骤3/4:</strong> 任务设置</p>
|
||||
<p>配置任务的基本参数:</p>
|
||||
<ul>
|
||||
<li><strong>RTMP流:</strong> 输入视频流地址</li>
|
||||
<li><strong>推流地址:</strong> 处理后的视频推流地址(可选)</li>
|
||||
<li><strong>任务名称:</strong> 用于标识任务的名称</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div v-if="currentStep === 4">
|
||||
<p><strong>步骤4/4:</strong> 验证和创建</p>
|
||||
<p>系统将执行最终验证:</p>
|
||||
<ul>
|
||||
<li>验证所有模型密钥</li>
|
||||
<li>检查系统资源</li>
|
||||
<li>确认任务参数</li>
|
||||
</ul>
|
||||
<p>验证通过后,点击"创建任务"启动检测。</p>
|
||||
</div>
|
||||
|
||||
<div class="task-preview" v-if="createdTaskId">
|
||||
<h4>任务创建成功</h4>
|
||||
<p><strong>任务ID:</strong> {{ createdTaskId }}</p>
|
||||
<p><strong>状态:</strong> {{ taskStatus }}</p>
|
||||
<p><strong>创建时间:</strong> {{ new Date().toLocaleString() }}</p>
|
||||
|
||||
<div class="form-group">
|
||||
<button class="btn btn-sm" @click="goToTaskManagement">查看任务管理</button>
|
||||
<button class="btn btn-sm" @click="copyTaskId">复制任务ID</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="errorMessage" class="error">
|
||||
<h4>错误信息</h4>
|
||||
<p>{{ errorMessage }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
new Vue({
|
||||
el: '#app',
|
||||
data: {
|
||||
currentStep: 1,
|
||||
availableModels: [],
|
||||
selectedModels: [],
|
||||
loadingModels: false,
|
||||
validatingAll: false,
|
||||
validatingFinal: false,
|
||||
creatingTask: false,
|
||||
|
||||
taskConfig: {
|
||||
taskname: `检测任务_${new Date().toLocaleDateString().replace(/\//g, '-')}`,
|
||||
rtmp_url: 'rtmp://localhost:1935/live/stream',
|
||||
push_url: '',
|
||||
AlgoId: ''
|
||||
},
|
||||
|
||||
finalValidationResult: null,
|
||||
createTaskMessage: '',
|
||||
createdTaskId: null,
|
||||
taskStatus: '',
|
||||
errorMessage: ''
|
||||
},
|
||||
computed: {
|
||||
allModelsHaveKeys() {
|
||||
return this.selectedModels.every(model => model.encryption_key && model.encryption_key.trim().length > 0);
|
||||
},
|
||||
allModelsValid() {
|
||||
return this.selectedModels.every(model => model.validation.valid);
|
||||
},
|
||||
validModelsCount() {
|
||||
return this.selectedModels.filter(model => model.validation.valid).length;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 步骤导航
|
||||
nextStep() {
|
||||
if (this.currentStep < 4) {
|
||||
this.currentStep++;
|
||||
}
|
||||
},
|
||||
prevStep() {
|
||||
if (this.currentStep > 1) {
|
||||
this.currentStep--;
|
||||
}
|
||||
},
|
||||
|
||||
// 加载可用模型
|
||||
async loadAvailableModels() {
|
||||
this.loadingModels = true;
|
||||
this.errorMessage = '';
|
||||
|
||||
try {
|
||||
const response = await axios.get('/api/models/encrypted/list_available');
|
||||
if (response.data.status === 'success') {
|
||||
this.availableModels = response.data.data.models.map(model => ({
|
||||
...model,
|
||||
encryption_key: '',
|
||||
validation: {
|
||||
checked: false,
|
||||
valid: false,
|
||||
error: '',
|
||||
model_hash: ''
|
||||
}
|
||||
}));
|
||||
} else {
|
||||
this.errorMessage = response.data.message;
|
||||
}
|
||||
} catch (error) {
|
||||
this.errorMessage = error.response?.data?.message || error.message;
|
||||
} finally {
|
||||
this.loadingModels = false;
|
||||
}
|
||||
},
|
||||
|
||||
// 模型选择
|
||||
toggleModelSelection(model) {
|
||||
const index = this.selectedModels.findIndex(m => m.filename === model.filename);
|
||||
if (index === -1) {
|
||||
this.selectedModels.push({
|
||||
...JSON.parse(JSON.stringify(model)),
|
||||
encryption_key: '',
|
||||
validation: {
|
||||
checked: false,
|
||||
valid: false,
|
||||
error: '',
|
||||
model_hash: ''
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.selectedModels.splice(index, 1);
|
||||
}
|
||||
},
|
||||
|
||||
removeModel(index) {
|
||||
this.selectedModels.splice(index, 1);
|
||||
},
|
||||
|
||||
// 单个模型验证
|
||||
async validateSingleModel(index) {
|
||||
const model = this.selectedModels[index];
|
||||
|
||||
if (!model.encryption_key || model.encryption_key.trim().length < 16) {
|
||||
model.validation = {
|
||||
checked: true,
|
||||
valid: false,
|
||||
error: '密钥长度不足或为空',
|
||||
model_hash: ''
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.post('/api/models/process/verify_key', {
|
||||
model_path: model.path,
|
||||
encryption_key: model.encryption_key
|
||||
});
|
||||
|
||||
if (response.data.status === 'success') {
|
||||
const verification = response.data.data.model_verification;
|
||||
if (verification && verification.success) {
|
||||
model.validation = {
|
||||
checked: true,
|
||||
valid: true,
|
||||
error: '',
|
||||
model_hash: verification.model_hash || ''
|
||||
};
|
||||
} else {
|
||||
model.validation = {
|
||||
checked: true,
|
||||
valid: false,
|
||||
error: verification?.error || '验证失败',
|
||||
model_hash: ''
|
||||
};
|
||||
}
|
||||
} else {
|
||||
model.validation = {
|
||||
checked: true,
|
||||
valid: false,
|
||||
error: response.data.message,
|
||||
model_hash: ''
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
model.validation = {
|
||||
checked: true,
|
||||
valid: false,
|
||||
error: error.response?.data?.message || error.message,
|
||||
model_hash: ''
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// 测试解密
|
||||
async testModelDecryption(index) {
|
||||
const model = this.selectedModels[index];
|
||||
this.errorMessage = '';
|
||||
|
||||
if (!model.encryption_key) {
|
||||
this.errorMessage = '请输入加密密钥';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.post('/api/models/process/test_decrypt', {
|
||||
model_path: model.path,
|
||||
encryption_key: model.encryption_key
|
||||
});
|
||||
|
||||
if (response.data.status === 'success') {
|
||||
alert(`解密测试成功!\n模型哈希: ${response.data.data.model_hash}\n解密时间: ${response.data.data.decryption_time.toFixed(3)}秒`);
|
||||
} else {
|
||||
this.errorMessage = response.data.message;
|
||||
}
|
||||
} catch (error) {
|
||||
this.errorMessage = error.response?.data?.message || error.message;
|
||||
}
|
||||
},
|
||||
|
||||
// 生成新密钥
|
||||
async generateKeyForModel(index) {
|
||||
try {
|
||||
const response = await axios.post('/api/models/generate_key');
|
||||
if (response.data.status === 'success') {
|
||||
this.selectedModels[index].encryption_key = response.data.data.key;
|
||||
this.validateSingleModel(index);
|
||||
}
|
||||
} catch (error) {
|
||||
this.errorMessage = error.response?.data?.message || error.message;
|
||||
}
|
||||
},
|
||||
|
||||
// 验证所有模型
|
||||
async validateAllModels() {
|
||||
this.validatingAll = true;
|
||||
this.errorMessage = '';
|
||||
|
||||
for (let i = 0; i < this.selectedModels.length; i++) {
|
||||
await this.validateSingleModel(i);
|
||||
}
|
||||
|
||||
this.validatingAll = false;
|
||||
|
||||
if (this.allModelsValid) {
|
||||
this.errorMessage = `所有模型验证通过 (${this.validModelsCount}/${this.selectedModels.length})`;
|
||||
} else {
|
||||
this.errorMessage = '部分模型验证失败,请检查密钥';
|
||||
}
|
||||
},
|
||||
|
||||
// 最终验证
|
||||
async performFinalValidation() {
|
||||
this.validatingFinal = true;
|
||||
this.errorMessage = '';
|
||||
this.finalValidationResult = null;
|
||||
|
||||
try {
|
||||
// 准备验证数据
|
||||
const validationData = {
|
||||
models: this.selectedModels.map(model => ({
|
||||
path: model.path,
|
||||
encryption_key: model.encryption_key
|
||||
}))
|
||||
};
|
||||
|
||||
const response = await axios.post('/api/models/process/validate_task', validationData);
|
||||
|
||||
if (response.data.status === 'success') {
|
||||
this.finalValidationResult = response.data.data;
|
||||
this.errorMessage = `验证通过: ${this.finalValidationResult.valid_models}/${this.finalValidationResult.total_models} 个模型有效`;
|
||||
} else {
|
||||
this.finalValidationResult = response.data.data;
|
||||
this.errorMessage = response.data.message;
|
||||
}
|
||||
} catch (error) {
|
||||
this.errorMessage = error.response?.data?.message || error.message;
|
||||
} finally {
|
||||
this.validatingFinal = false;
|
||||
}
|
||||
},
|
||||
|
||||
// 创建任务
|
||||
async createTask() {
|
||||
this.creatingTask = true;
|
||||
this.createTaskMessage = '正在创建任务...';
|
||||
this.errorMessage = '';
|
||||
|
||||
try {
|
||||
// 准备任务数据
|
||||
const taskData = {
|
||||
taskname: this.taskConfig.taskname,
|
||||
rtmp_url: this.taskConfig.rtmp_url,
|
||||
push_url: this.taskConfig.push_url || undefined,
|
||||
AlgoId: this.taskConfig.AlgoId || undefined,
|
||||
models: this.selectedModels.map(model => ({
|
||||
path: model.path,
|
||||
encryption_key: model.encryption_key,
|
||||
tags: model.tags || {},
|
||||
conf_thres: model.conf_thres || 0.25,
|
||||
iou_thres: model.iou_thres || 0.45,
|
||||
imgsz: model.imgsz || 640,
|
||||
device: model.device || 'cuda:0',
|
||||
half: model.half !== undefined ? model.half : true,
|
||||
enabled: true
|
||||
}))
|
||||
};
|
||||
|
||||
this.createTaskMessage = '正在验证模型...';
|
||||
await this.performFinalValidation();
|
||||
|
||||
if (!this.finalValidationResult || !this.finalValidationResult.success) {
|
||||
throw new Error('模型验证失败,无法创建任务');
|
||||
}
|
||||
|
||||
this.createTaskMessage = '正在提交任务创建请求...';
|
||||
const response = await axios.post('/api/tasks/create', taskData);
|
||||
|
||||
if (response.data.status === 'success') {
|
||||
this.createdTaskId = response.data.data.task_id;
|
||||
this.taskStatus = '已创建';
|
||||
this.createTaskMessage = '任务创建成功!';
|
||||
} else {
|
||||
throw new Error(response.data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
this.errorMessage = error.response?.data?.message || error.message;
|
||||
this.createTaskMessage = '任务创建失败';
|
||||
} finally {
|
||||
this.creatingTask = false;
|
||||
}
|
||||
},
|
||||
|
||||
// 辅助函数
|
||||
formatFileSize(bytes) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
},
|
||||
|
||||
formatTimestamp(timestamp) {
|
||||
const date = new Date(timestamp * 1000);
|
||||
return date.toLocaleDateString();
|
||||
},
|
||||
|
||||
copyTaskId() {
|
||||
if (this.createdTaskId) {
|
||||
navigator.clipboard.writeText(this.createdTaskId);
|
||||
alert('任务ID已复制到剪贴板');
|
||||
}
|
||||
},
|
||||
|
||||
goToTaskManagement() {
|
||||
window.location.href = '/';
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadAvailableModels();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
2386
yolo_detection.log
2386
yolo_detection.log
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue