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